jamdesk 1.1.170 → 1.1.172

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/dist/__tests__/unit/docs-config.test.js +9 -0
  3. package/dist/__tests__/unit/docs-config.test.js.map +1 -1
  4. package/dist/__tests__/unit/migrate-mdx.test.js +24 -0
  5. package/dist/__tests__/unit/migrate-mdx.test.js.map +1 -1
  6. package/dist/__tests__/unit/migrate-resolve-theme.test.js +6 -0
  7. package/dist/__tests__/unit/migrate-resolve-theme.test.js.map +1 -1
  8. package/dist/__tests__/unit/prompt-vendoring.test.d.ts +2 -0
  9. package/dist/__tests__/unit/prompt-vendoring.test.d.ts.map +1 -0
  10. package/dist/__tests__/unit/prompt-vendoring.test.js +20 -0
  11. package/dist/__tests__/unit/prompt-vendoring.test.js.map +1 -0
  12. package/dist/__tests__/unit/vendored-sync.test.js +3 -0
  13. package/dist/__tests__/unit/vendored-sync.test.js.map +1 -1
  14. package/dist/commands/migrate/types.d.ts +1 -1
  15. package/dist/commands/migrate/types.d.ts.map +1 -1
  16. package/dist/commands/migrate/types.js +5 -0
  17. package/dist/commands/migrate/types.js.map +1 -1
  18. package/dist/index.js +2 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/deps.js +1 -1
  21. package/dist/lib/docs-config.d.ts +1 -1
  22. package/dist/lib/docs-config.d.ts.map +1 -1
  23. package/dist/lib/docs-config.js +1 -1
  24. package/dist/lib/docs-config.js.map +1 -1
  25. package/dist/lib/normalize-config.js +1 -1
  26. package/dist/lib/normalize-config.js.map +1 -1
  27. package/package.json +2 -2
  28. package/templates/quickstart.mdx +1 -1
  29. package/vendored/components/AIActionsMenu.tsx +5 -17
  30. package/vendored/components/mdx/Card.tsx +1 -0
  31. package/vendored/components/mdx/MDXComponents.tsx +2 -0
  32. package/vendored/components/mdx/Prompt.tsx +381 -0
  33. package/vendored/components/mdx/ai-action-icons.tsx +7 -0
  34. package/vendored/components/mdx/prompt-actions.ts +137 -0
  35. package/vendored/components/navigation/Sidebar.tsx +23 -14
  36. package/vendored/lib/ai-service-urls.ts +8 -0
  37. package/vendored/lib/build/error-parser.ts +1 -1
  38. package/vendored/lib/docs-types.ts +3 -3
  39. package/vendored/lib/extract-highlights.ts +1 -1
  40. package/vendored/lib/inject-prompt-source.ts +259 -0
  41. package/vendored/lib/layout-helpers.tsx +30 -1
  42. package/vendored/lib/mdx-syntax-fixes.ts +143 -0
  43. package/vendored/lib/mdx.ts +2 -1
  44. package/vendored/lib/normalize-config.ts +1 -1
  45. package/vendored/lib/preprocess-mdx.ts +10 -223
  46. package/vendored/lib/prompt-source-codec.ts +27 -0
  47. package/vendored/lib/render-doc-page.tsx +4 -1
  48. package/vendored/lib/snippet-loader-isr.ts +2 -0
  49. package/vendored/lib/validate-config.ts +4 -4
  50. package/vendored/schema/docs-schema.json +122 -1
  51. package/vendored/themes/halo/variables.css +514 -0
  52. package/vendored/themes/index.ts +26 -0
  53. package/vendored/themes/types.ts +1 -0
  54. package/vendored/workspace-package-lock.json +85 -1
@@ -433,13 +433,28 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
433
433
  // Prevent body scroll when sidebar is open on mobile
434
434
  useBodyScrollLock(isOpen);
435
435
 
436
- // Toggle group expansion; if expanding, navigate to first page. Top-level
437
- // sections never visually collapse — for them the toggled key only gates
438
- // prefetch (see renderGroupContents) while the click still navigates.
436
+ // Top-level section titles never collapse; clicking one navigates to the
437
+ // section's first page. The key is still marked expanded so the section's
438
+ // Links upgrade to prefetch (see renderGroupContents).
439
439
  // The router.push must run AFTER the setState commits — calling it inside the
440
440
  // updater triggers "Cannot update a component (Router) while rendering Sidebar".
441
- const handleGroupClick = useCallback((group: ResolvedGroup, key: string) => {
442
- const willExpand = !expandedGroups.has(key);
441
+ const handleTopLevelGroupClick = useCallback((group: ResolvedGroup, key: string) => {
442
+ setExpandedGroups(prev => {
443
+ if (prev.has(key)) return prev;
444
+ const newSet = new Set(prev);
445
+ newSet.add(key);
446
+ return newSet;
447
+ });
448
+ const firstPagePath = findFirstPageInGroups([group]);
449
+ if (firstPagePath) {
450
+ handleButtonNav(`${linkPrefix}/${firstPagePath}`);
451
+ }
452
+ }, [linkPrefix, handleButtonNav]);
453
+
454
+ // Toggle a nested group's accordion. Expanding only reveals the pages —
455
+ // navigation happens when the visitor clicks a page link, never on the
456
+ // caret itself.
457
+ const handleGroupClick = useCallback((key: string) => {
443
458
  setExpandedGroups(prev => {
444
459
  const newSet = new Set(prev);
445
460
  if (prev.has(key)) {
@@ -449,13 +464,7 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
449
464
  }
450
465
  return newSet;
451
466
  });
452
- if (willExpand) {
453
- const firstPagePath = findFirstPageInGroups([group]);
454
- if (firstPagePath) {
455
- handleButtonNav(`${linkPrefix}/${firstPagePath}`);
456
- }
457
- }
458
- }, [linkPrefix, expandedGroups, handleButtonNav]);
467
+ }, []);
459
468
 
460
469
  // Render the pages and nested groups inside a group. Split out from
461
470
  // renderGroup so a named group can gate its contents behind an expanded panel
@@ -574,7 +583,7 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
574
583
  {group.name && (
575
584
  <button
576
585
  type="button"
577
- onClick={() => handleGroupClick(group, key)}
586
+ onClick={() => handleTopLevelGroupClick(group, key)}
578
587
  className="flex items-center justify-between w-full text-left mb-2 cursor-pointer"
579
588
  >
580
589
  <span className="text-sm font-semibold text-[var(--color-text-tertiary)] flex items-center gap-2">
@@ -606,7 +615,7 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
606
615
  type="button"
607
616
  aria-expanded={isExpanded}
608
617
  aria-controls={panelId}
609
- onClick={() => handleGroupClick(group, key)}
618
+ onClick={() => handleGroupClick(key)}
610
619
  className={buttonClassName}
611
620
  >
612
621
  {group.icon && (
@@ -0,0 +1,8 @@
1
+ export const AI_CHAT_URLS = {
2
+ chatgptSearch: 'https://chatgpt.com/?hints=search&q=',
3
+ chatgpt: 'https://chatgpt.com/?q=',
4
+ claude: 'https://claude.ai/new?q=',
5
+ gemini: 'https://gemini.google.com/app?q=',
6
+ perplexity: 'https://www.perplexity.ai/?q=',
7
+ cursor: 'https://cursor.com/link/prompt?text=',
8
+ } as const;
@@ -262,7 +262,7 @@ export function parseErrorDetails(
262
262
  const requiredFields =
263
263
  'Required fields:\n' +
264
264
  '• "name": Your site name\n' +
265
- '• "theme": One of "jam", "nebula", or "pulsar"\n' +
265
+ '• "theme": One of "jam", "nebula", "pulsar", or "halo"\n' +
266
266
  '• "colors": { "primary": "#hexcolor" }\n' +
267
267
  '• "navigation": one of "tabs", "groups", "pages", "dropdowns", ' +
268
268
  '"versions", "languages", or "products" ' +
@@ -23,7 +23,7 @@ export const ASSET_PREFIX = '/_jd';
23
23
  /**
24
24
  * Available themes
25
25
  */
26
- export type ThemeName = 'jam' | 'nebula' | 'pulsar';
26
+ export type ThemeName = 'jam' | 'nebula' | 'pulsar' | 'halo';
27
27
 
28
28
  // =============================================================================
29
29
  // ICON TYPES
@@ -875,8 +875,8 @@ export interface DocsConfig {
875
875
  logo?: Logo;
876
876
 
877
877
  // Tab position - controls where tabs are displayed
878
- // 'top': Tabs in header/tab bar (default for pulsar)
879
- // 'left': Tabs at top of sidebar (default for jam/nebula)
878
+ // 'top': Tabs in header/tab bar
879
+ // 'left': Tabs at top of sidebar (default for every theme)
880
880
  tabsPosition?: TabsPosition;
881
881
 
882
882
  // External link anchors - appear at top of sidebar on all pages
@@ -11,7 +11,7 @@ import type { DocsConfig } from './docs-types.js';
11
11
  */
12
12
  export interface ExtractedHighlights {
13
13
  siteName: string;
14
- theme: 'jam' | 'nebula' | 'pulsar';
14
+ theme: 'jam' | 'nebula' | 'pulsar' | 'halo';
15
15
  primaryColor?: string;
16
16
  seoIndexable: boolean;
17
17
  analyticsIntegrations: string[];
@@ -0,0 +1,259 @@
1
+ import { createProcessor } from '@mdx-js/mdx';
2
+ import { applyJsxSyntaxFixes } from './mdx-syntax-fixes';
3
+ import { encodePromptSource } from './prompt-source-codec';
4
+
5
+ type Positioned = {
6
+ start?: { offset?: number };
7
+ end?: { offset?: number };
8
+ };
9
+
10
+ type MdxAttribute = {
11
+ type?: string;
12
+ name?: string | null;
13
+ value?: unknown;
14
+ position?: Positioned;
15
+ };
16
+
17
+ type MdxNode = {
18
+ type?: string;
19
+ name?: string | null;
20
+ attributes?: MdxAttribute[];
21
+ children?: MdxNode[];
22
+ position?: Positioned;
23
+ };
24
+
25
+ type OffsetEdit = {
26
+ start: number;
27
+ end: number;
28
+ replacement: string;
29
+ };
30
+
31
+ function offsetAt(
32
+ value: number | undefined,
33
+ description: string,
34
+ ): number {
35
+ if (typeof value !== 'number') {
36
+ throw new Error(`Missing ${description} offset`);
37
+ }
38
+ return value;
39
+ }
40
+
41
+ function nodeStart(node: MdxNode): number {
42
+ return offsetAt(node.position?.start?.offset, 'node start');
43
+ }
44
+
45
+ function nodeEnd(node: MdxNode): number {
46
+ return offsetAt(node.position?.end?.offset, 'node end');
47
+ }
48
+
49
+ function isMdxElement(node: MdxNode, name: string): boolean {
50
+ return (
51
+ (node.type === 'mdxJsxFlowElement' ||
52
+ node.type === 'mdxJsxTextElement') &&
53
+ node.name === name
54
+ );
55
+ }
56
+
57
+ function openingInsertionOffset(source: string, node: MdxNode): number {
58
+ const start = nodeStart(node);
59
+ const name = node.name;
60
+ if (!name) throw new Error('Element name is missing');
61
+
62
+ const attributes = node.attributes ?? [];
63
+ const anchor = attributes.length > 0
64
+ ? offsetAt(
65
+ attributes[attributes.length - 1].position?.end?.offset,
66
+ 'attribute end',
67
+ )
68
+ : start + 1 + name.length;
69
+ const remainder = source.slice(anchor, nodeEnd(node));
70
+ if (!/^\s*\/?>/.test(remainder)) {
71
+ throw new Error('Opening tag delimiter was not found after attributes');
72
+ }
73
+ return anchor;
74
+ }
75
+
76
+ function openingTagEnd(source: string, node: MdxNode): number {
77
+ const anchor = openingInsertionOffset(source, node);
78
+ const delimiter = source.slice(anchor, nodeEnd(node)).match(/^\s*\/?>/);
79
+ if (!delimiter) throw new Error('Opening tag delimiter is missing');
80
+ return anchor + delimiter[0].length;
81
+ }
82
+
83
+ function closingTagRange(
84
+ source: string,
85
+ node: MdxNode,
86
+ ): { start: number; end: number } | null {
87
+ const start = nodeStart(node);
88
+ const openEnd = openingTagEnd(source, node);
89
+ if (source.slice(start, openEnd).trimEnd().endsWith('/>')) return null;
90
+
91
+ const end = nodeEnd(node);
92
+ const tail = source.slice(openEnd, end);
93
+ const closingStartInTail = tail.lastIndexOf('</Visibility');
94
+ if (closingStartInTail < 0) {
95
+ throw new Error('Visibility closing tag is missing');
96
+ }
97
+ const closingStart = openEnd + closingStartInTail;
98
+ if (!/^<\/Visibility\s*>$/.test(source.slice(closingStart, end))) {
99
+ throw new Error('Visibility closing tag is malformed');
100
+ }
101
+ return { start: closingStart, end };
102
+ }
103
+
104
+ function visibilityIsHidden(node: MdxNode): boolean {
105
+ let audience: string | null = null;
106
+ let audienceIsUnknown = false;
107
+
108
+ for (const attribute of node.attributes ?? []) {
109
+ if (attribute.type === 'mdxJsxExpressionAttribute') {
110
+ audience = null;
111
+ audienceIsUnknown = true;
112
+ continue;
113
+ }
114
+ if (attribute.type !== 'mdxJsxAttribute') {
115
+ throw new Error('Unknown Visibility attribute shape');
116
+ }
117
+ if (attribute.name !== 'for') continue;
118
+
119
+ if (typeof attribute.value === 'string') {
120
+ audience = attribute.value;
121
+ audienceIsUnknown = false;
122
+ } else if (attribute.value === null) {
123
+ audience = null;
124
+ audienceIsUnknown = false;
125
+ } else {
126
+ audience = null;
127
+ audienceIsUnknown = true;
128
+ }
129
+ }
130
+
131
+ return audienceIsUnknown || audience === 'agents';
132
+ }
133
+
134
+ function collectVisibilityEdits(
135
+ source: string,
136
+ node: MdxNode,
137
+ edits: OffsetEdit[],
138
+ ): void {
139
+ if (isMdxElement(node, 'Visibility')) {
140
+ if (visibilityIsHidden(node)) {
141
+ edits.push({ start: nodeStart(node), end: nodeEnd(node), replacement: '' });
142
+ return;
143
+ }
144
+
145
+ const openEnd = openingTagEnd(source, node);
146
+ edits.push({ start: nodeStart(node), end: openEnd, replacement: '' });
147
+ const closing = closingTagRange(source, node);
148
+ if (closing) edits.push({ ...closing, replacement: '' });
149
+ }
150
+
151
+ node.children?.forEach((child) => {
152
+ collectVisibilityEdits(source, child, edits);
153
+ });
154
+ }
155
+
156
+ function promptBodySource(source: string, prompt: MdxNode): string {
157
+ const children = prompt.children ?? [];
158
+ if (children.length === 0) return '';
159
+
160
+ const start = children[0].position?.start?.offset;
161
+ const end = children[children.length - 1].position?.end?.offset;
162
+ const promptStart = prompt.position?.start?.offset;
163
+ const promptEnd = prompt.position?.end?.offset;
164
+ if (
165
+ typeof start !== 'number' ||
166
+ typeof end !== 'number' ||
167
+ typeof promptStart !== 'number' ||
168
+ typeof promptEnd !== 'number' ||
169
+ start < promptStart ||
170
+ end < start ||
171
+ end > promptEnd ||
172
+ promptStart < 0 ||
173
+ promptEnd > source.length
174
+ ) {
175
+ return '';
176
+ }
177
+ const edits: OffsetEdit[] = [];
178
+ children.forEach((child) => collectVisibilityEdits(source, child, edits));
179
+
180
+ let body = source.slice(start, end);
181
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
182
+ if (edit.start < start || edit.end > end || edit.start > edit.end) {
183
+ throw new Error('Visibility edit escaped Prompt body');
184
+ }
185
+ const relativeStart = edit.start - start;
186
+ const relativeEnd = edit.end - start;
187
+ body = body.slice(0, relativeStart) + edit.replacement + body.slice(relativeEnd);
188
+ }
189
+ return body;
190
+ }
191
+
192
+ function injectInto(
193
+ source: string,
194
+ tree: MdxNode,
195
+ ): { source: string; promptCount: number } {
196
+ const insertions: Array<{ offset: number; value: string }> = [];
197
+ let promptCount = 0;
198
+
199
+ const visit = (node: MdxNode, hiddenByVisibility: boolean): void => {
200
+ let hidden = hiddenByVisibility;
201
+ if (isMdxElement(node, 'Visibility')) {
202
+ hidden ||= visibilityIsHidden(node);
203
+ }
204
+
205
+ if (isMdxElement(node, 'Prompt')) {
206
+ promptCount++;
207
+ const authored = hidden ? '' : promptBodySource(source, node);
208
+ insertions.push({
209
+ offset: openingInsertionOffset(source, node),
210
+ value: ` __promptSource="${encodePromptSource(authored)}"`,
211
+ });
212
+ }
213
+
214
+ node.children?.forEach((child) => visit(child, hidden));
215
+ };
216
+
217
+ visit(tree, false);
218
+
219
+ let transformed = source;
220
+ for (const insertion of insertions.sort((a, b) => b.offset - a.offset)) {
221
+ transformed =
222
+ transformed.slice(0, insertion.offset) +
223
+ insertion.value +
224
+ transformed.slice(insertion.offset);
225
+ }
226
+ return { source: transformed, promptCount };
227
+ }
228
+
229
+ export function injectPromptSources(source: string): string {
230
+ if (!source.includes('<Prompt')) return source;
231
+
232
+ const processor = createProcessor();
233
+ type ParsedMdxRoot = ReturnType<typeof processor.parse>;
234
+ const accept = (candidate: string, tree: ParsedMdxRoot): string => {
235
+ const result = injectInto(candidate, tree as MdxNode);
236
+ if (result.promptCount === 0) return source;
237
+ processor.parse(result.source);
238
+ return result.source;
239
+ };
240
+
241
+ let rawTree: ParsedMdxRoot;
242
+ try {
243
+ rawTree = processor.parse(source);
244
+ } catch {
245
+ try {
246
+ const repaired = applyJsxSyntaxFixes(source);
247
+ const repairedTree = processor.parse(repaired);
248
+ return accept(repaired, repairedTree);
249
+ } catch {
250
+ return source;
251
+ }
252
+ }
253
+
254
+ try {
255
+ return accept(source, rawTree);
256
+ } catch {
257
+ return source;
258
+ }
259
+ }
@@ -2,7 +2,7 @@
2
2
  // language, project slug) are resolved upstream from middleware headers
3
3
  // or — in non-ISR dev/tests — from URL params; this module owns the
4
4
  // rendering once those have resolved.
5
- import { Inter, JetBrains_Mono } from 'next/font/google';
5
+ import { Inter, JetBrains_Mono, Figtree } from 'next/font/google';
6
6
  import { preinit } from 'react-dom';
7
7
  import fs from 'fs';
8
8
  import path from 'path';
@@ -70,6 +70,32 @@ export const jetbrainsMono = JetBrains_Mono({
70
70
  variable: '--font-mono',
71
71
  });
72
72
 
73
+ // Halo's face. Figtree ships as a variable font (300-900) so no `weight` array
74
+ // is needed — the full heading ladder (600/700/800) and body weight (400) that
75
+ // themes/halo/variables.css asks for are all covered by the single variable
76
+ // axis. Declared with the same `--font-primary` variable as Inter: only one of
77
+ // the two classes is ever applied to <body>, so they never collide.
78
+ export const figtree = Figtree({
79
+ subsets: ['latin'],
80
+ display: 'swap',
81
+ variable: '--font-primary',
82
+ // DELIBERATE, and a real tradeoff — do not flip this to true without reading
83
+ // this through. next/font's `preload` is a static build-time flag, but the
84
+ // theme is only known at runtime (from docs.json), and every theme is served
85
+ // by the same ISR route off this shared module. So preload is necessarily
86
+ // all-pages-or-no-pages; it cannot be per-theme.
87
+ // preload: true -> jam/nebula/pulsar each preload a face they never render.
88
+ // preload: false -> halo alone pays a brief FOUT, and `display: 'swap'`
89
+ // above already covers that; the @font-face still ships
90
+ // and loads as soon as figtree.className applies.
91
+ // False protects the three existing production themes at halo's expense,
92
+ // which is the right side to err on. Known consequence: halo pages still
93
+ // carry Inter's preload link (inter is declared above with preload
94
+ // defaulting to true) for a face they never render. That waste is not
95
+ // fixable while all four themes share one route — accepted, not overlooked.
96
+ preload: false,
97
+ });
98
+
73
99
  export function getLocalFileContent(filename: string): string | null {
74
100
  try {
75
101
  const filePath = path.join(process.cwd(), 'public', filename);
@@ -303,6 +329,9 @@ export function getFontClassName(
303
329
  if (theme.name === 'nebula') {
304
330
  return `${jetbrainsMono.variable} font-mono`;
305
331
  }
332
+ if (theme.name === 'halo') {
333
+ return `${figtree.variable} ${jetbrainsMono.variable} ${figtree.className}`;
334
+ }
306
335
  return `${inter.variable} ${jetbrainsMono.variable} ${inter.className}`;
307
336
  }
308
337
 
@@ -0,0 +1,143 @@
1
+ /** JSX components whose bodies commonly contain Markdown block content. */
2
+ export const JSX_CONTENT_COMPONENTS = [
3
+ 'Card',
4
+ 'Columns',
5
+ 'Note',
6
+ 'Warning',
7
+ 'Tip',
8
+ 'Info',
9
+ 'Check',
10
+ 'Danger',
11
+ 'Accordion',
12
+ 'AccordionGroup',
13
+ 'Tab',
14
+ 'Tabs',
15
+ 'Step',
16
+ 'Steps',
17
+ 'Expandable',
18
+ 'Update',
19
+ 'Frame',
20
+ 'Panel',
21
+ 'View',
22
+ 'Varning',
23
+ 'Avertissement',
24
+ 'Visibility',
25
+ 'Prompt',
26
+ ] as const;
27
+
28
+ export function unindentClosingTagAfterList(content: string): string {
29
+ if (!/^\s+<\/[A-Z]/m.test(content)) return content;
30
+
31
+ const closingTagPattern = /^(\s+)(<\/[A-Z][\w-]*>)\s*$/;
32
+ const listItemPattern = /^\s*(?:[-*+]\s|\d+\.\s)/;
33
+ const lines = content.split('\n');
34
+ const out: string[] = [];
35
+ let lastNonBlank = '';
36
+
37
+ for (const line of lines) {
38
+ const closing = line.match(closingTagPattern);
39
+ if (closing && listItemPattern.test(lastNonBlank)) {
40
+ out.push(closing[2]);
41
+ lastNonBlank = closing[2];
42
+ continue;
43
+ }
44
+ out.push(line);
45
+ if (line.trim() !== '') lastNonBlank = line;
46
+ }
47
+
48
+ return out.join('\n');
49
+ }
50
+
51
+ export function unindentJsxAfterListItems(content: string): string {
52
+ const componentPattern = JSX_CONTENT_COMPONENTS.join('|');
53
+ const lines = content.split('\n');
54
+ const result: string[] = [];
55
+ let inListContext = false;
56
+ let jsxBlockIndent = 0;
57
+ let unindentingJsxBlock = false;
58
+ let jsxBlockDepth = 0;
59
+ let currentComponent = '';
60
+
61
+ for (let i = 0; i < lines.length; i++) {
62
+ const line = lines[i];
63
+ const trimmed = line.trim();
64
+ const currentIndent = line.match(/^(\s*)/)?.[1].length ?? 0;
65
+ const isListItem = /^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed);
66
+
67
+ if (isListItem) {
68
+ inListContext = true;
69
+ result.push(line);
70
+ continue;
71
+ }
72
+
73
+ if (!trimmed) {
74
+ result.push(line);
75
+ continue;
76
+ }
77
+
78
+ const openingTagMatch = trimmed.match(
79
+ new RegExp(`^<(${componentPattern})(?:\\s|>|$|/>)`),
80
+ );
81
+
82
+ if (
83
+ openingTagMatch &&
84
+ inListContext &&
85
+ currentIndent > 0 &&
86
+ !unindentingJsxBlock
87
+ ) {
88
+ unindentingJsxBlock = true;
89
+ jsxBlockIndent = currentIndent;
90
+ jsxBlockDepth = 1;
91
+ currentComponent = openingTagMatch[1];
92
+
93
+ if (trimmed.endsWith('/>')) {
94
+ unindentingJsxBlock = false;
95
+ jsxBlockDepth = 0;
96
+ inListContext = false;
97
+ }
98
+
99
+ const prevLine = result.length > 0 ? result[result.length - 1] : '';
100
+ if (prevLine.trim() !== '') result.push('');
101
+ result.push(trimmed);
102
+ continue;
103
+ }
104
+
105
+ if (unindentingJsxBlock) {
106
+ const nestedOpenMatch = trimmed.match(
107
+ new RegExp(`^<(${currentComponent})(?:\\s[^>]*)?>`),
108
+ );
109
+ if (nestedOpenMatch && !trimmed.endsWith('/>')) jsxBlockDepth++;
110
+
111
+ const closingTagMatch = trimmed.match(
112
+ new RegExp(`^</(${currentComponent})>`),
113
+ );
114
+ if (closingTagMatch) {
115
+ jsxBlockDepth--;
116
+ if (jsxBlockDepth === 0) {
117
+ unindentingJsxBlock = false;
118
+ inListContext = false;
119
+ currentComponent = '';
120
+ result.push(trimmed);
121
+ continue;
122
+ }
123
+ }
124
+
125
+ if (currentIndent >= jsxBlockIndent) {
126
+ const reducedIndent = currentIndent - jsxBlockIndent;
127
+ result.push(' '.repeat(reducedIndent) + trimmed);
128
+ } else {
129
+ result.push(trimmed);
130
+ }
131
+ continue;
132
+ }
133
+
134
+ if (currentIndent === 0) inListContext = false;
135
+ result.push(line);
136
+ }
137
+
138
+ return result.join('\n');
139
+ }
140
+
141
+ export function applyJsxSyntaxFixes(content: string): string {
142
+ return unindentJsxAfterListItems(unindentClosingTagAfterList(content));
143
+ }
@@ -6,6 +6,7 @@ import rehypeShiki from '@shikijs/rehype';
6
6
  import remarkGfm from 'remark-gfm';
7
7
  import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
8
8
  import { getContentDir } from './docs';
9
+ import { injectPromptSources } from './inject-prompt-source';
9
10
  import { mdxSecurityOptions } from './mdx-security-options';
10
11
  import { remarkSvgNamespaceAttrs } from './remark-svg-namespace-attrs';
11
12
  import { getShikiOptions } from './shiki-config';
@@ -41,7 +42,7 @@ export async function getDocContent(slug: string[]): Promise<DocContent | null>
41
42
  const { data, content } = parseFrontmatterLenient(fileContents);
42
43
 
43
44
  const shikiOptions = getShikiOptions();
44
- const mdxSource = await serialize(content, {
45
+ const mdxSource = await serialize(injectPromptSources(content), {
45
46
  // Keep expression props (e.g. cols={2}) compatible under next-mdx-remote v6.
46
47
  ...mdxSecurityOptions,
47
48
  mdxOptions: {
@@ -96,7 +96,7 @@ export function normalizeConfig(config: DocsConfigInput): NormalizeResult {
96
96
  // 3. Warn about layout
97
97
  if (layout) {
98
98
  warnings.push(
99
- 'layout field is ignored. Jamdesk determines layout from theme (pulsar=sidebar, jam/nebula=header).'
99
+ 'layout field is ignored. Jamdesk determines layout from theme (pulsar=sidebar, jam/nebula/halo=header).'
100
100
  );
101
101
  }
102
102