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.
- package/README.md +2 -2
- package/dist/__tests__/unit/docs-config.test.js +9 -0
- package/dist/__tests__/unit/docs-config.test.js.map +1 -1
- package/dist/__tests__/unit/migrate-mdx.test.js +24 -0
- package/dist/__tests__/unit/migrate-mdx.test.js.map +1 -1
- package/dist/__tests__/unit/migrate-resolve-theme.test.js +6 -0
- package/dist/__tests__/unit/migrate-resolve-theme.test.js.map +1 -1
- package/dist/__tests__/unit/prompt-vendoring.test.d.ts +2 -0
- package/dist/__tests__/unit/prompt-vendoring.test.d.ts.map +1 -0
- package/dist/__tests__/unit/prompt-vendoring.test.js +20 -0
- package/dist/__tests__/unit/prompt-vendoring.test.js.map +1 -0
- package/dist/__tests__/unit/vendored-sync.test.js +3 -0
- package/dist/__tests__/unit/vendored-sync.test.js.map +1 -1
- package/dist/commands/migrate/types.d.ts +1 -1
- package/dist/commands/migrate/types.d.ts.map +1 -1
- package/dist/commands/migrate/types.js +5 -0
- package/dist/commands/migrate/types.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/deps.js +1 -1
- package/dist/lib/docs-config.d.ts +1 -1
- package/dist/lib/docs-config.d.ts.map +1 -1
- package/dist/lib/docs-config.js +1 -1
- package/dist/lib/docs-config.js.map +1 -1
- package/dist/lib/normalize-config.js +1 -1
- package/dist/lib/normalize-config.js.map +1 -1
- package/package.json +2 -2
- package/templates/quickstart.mdx +1 -1
- package/vendored/components/AIActionsMenu.tsx +5 -17
- package/vendored/components/mdx/Card.tsx +1 -0
- package/vendored/components/mdx/MDXComponents.tsx +2 -0
- package/vendored/components/mdx/Prompt.tsx +381 -0
- package/vendored/components/mdx/ai-action-icons.tsx +7 -0
- package/vendored/components/mdx/prompt-actions.ts +137 -0
- package/vendored/components/navigation/Sidebar.tsx +23 -14
- package/vendored/lib/ai-service-urls.ts +8 -0
- package/vendored/lib/build/error-parser.ts +1 -1
- package/vendored/lib/docs-types.ts +3 -3
- package/vendored/lib/extract-highlights.ts +1 -1
- package/vendored/lib/inject-prompt-source.ts +259 -0
- package/vendored/lib/layout-helpers.tsx +30 -1
- package/vendored/lib/mdx-syntax-fixes.ts +143 -0
- package/vendored/lib/mdx.ts +2 -1
- package/vendored/lib/normalize-config.ts +1 -1
- package/vendored/lib/preprocess-mdx.ts +10 -223
- package/vendored/lib/prompt-source-codec.ts +27 -0
- package/vendored/lib/render-doc-page.tsx +4 -1
- package/vendored/lib/snippet-loader-isr.ts +2 -0
- package/vendored/lib/validate-config.ts +4 -4
- package/vendored/schema/docs-schema.json +122 -1
- package/vendored/themes/halo/variables.css +514 -0
- package/vendored/themes/index.ts +26 -0
- package/vendored/themes/types.ts +1 -0
- 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
|
-
//
|
|
437
|
-
//
|
|
438
|
-
// prefetch (see renderGroupContents)
|
|
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
|
|
442
|
-
|
|
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
|
-
|
|
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={() =>
|
|
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(
|
|
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 "
|
|
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
|
|
879
|
-
// 'left': Tabs at top of sidebar (default for
|
|
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
|
+
}
|
package/vendored/lib/mdx.ts
CHANGED
|
@@ -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
|
|