jamdesk 1.1.174 → 1.1.176
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/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts +2 -0
- package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts.map +1 -0
- package/dist/__tests__/unit/dev-workspace-symlinks.test.js +112 -0
- package/dist/__tests__/unit/dev-workspace-symlinks.test.js.map +1 -0
- package/dist/__tests__/unit/language-filter.test.d.ts +2 -0
- package/dist/__tests__/unit/language-filter.test.d.ts.map +1 -0
- package/dist/__tests__/unit/language-filter.test.js +166 -0
- package/dist/__tests__/unit/language-filter.test.js.map +1 -0
- package/dist/lib/language-filter.d.ts +31 -0
- package/dist/lib/language-filter.d.ts.map +1 -0
- package/dist/lib/language-filter.js +14 -0
- package/dist/lib/language-filter.js.map +1 -0
- package/package.json +1 -1
- package/vendored/components/mdx/Prompt.tsx +383 -122
- package/vendored/components/navigation/Sidebar.tsx +5 -1
- package/vendored/lib/screenshot-capture.ts +21 -11
- package/vendored/themes/halo/variables.css +72 -2
- package/vendored/workspace-package-lock.json +76 -76
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type ReactNode,
|
|
13
13
|
} from 'react';
|
|
14
14
|
|
|
15
|
+
import { useBodyScrollLock } from '@/hooks/useBodyScrollLock';
|
|
15
16
|
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
|
|
16
17
|
import { renderInlineMarkdown } from '@/lib/inline-markdown';
|
|
17
18
|
import { decodePromptSource } from '@/lib/prompt-source-codec';
|
|
@@ -70,6 +71,197 @@ function ActionIcon({ action }: { action: ResolvedPromptAction['action'] }): Rea
|
|
|
70
71
|
return null;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
interface PromptCopyButtonProps {
|
|
75
|
+
copyState: CopyState;
|
|
76
|
+
className: string;
|
|
77
|
+
onCopy: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function PromptCopyButton({
|
|
81
|
+
copyState,
|
|
82
|
+
className,
|
|
83
|
+
onCopy,
|
|
84
|
+
}: PromptCopyButtonProps): ReactElement {
|
|
85
|
+
const label = copyState === 'copied'
|
|
86
|
+
? 'Copied'
|
|
87
|
+
: copyState === 'error'
|
|
88
|
+
? 'Copy failed'
|
|
89
|
+
: 'Copy prompt';
|
|
90
|
+
return (
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
aria-label="Copy prompt"
|
|
94
|
+
title={label}
|
|
95
|
+
className={className}
|
|
96
|
+
data-copy-state={copyState}
|
|
97
|
+
onClick={onCopy}
|
|
98
|
+
>
|
|
99
|
+
<i
|
|
100
|
+
className={copyState === 'copied'
|
|
101
|
+
? 'fa-solid fa-check text-[var(--color-success-text)]'
|
|
102
|
+
: copyState === 'error'
|
|
103
|
+
? 'fa-solid fa-triangle-exclamation text-[var(--color-error-text)]'
|
|
104
|
+
: 'fa-regular fa-copy'}
|
|
105
|
+
aria-hidden="true"
|
|
106
|
+
/>
|
|
107
|
+
</button>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface PromptDialogProps {
|
|
112
|
+
isOpen: boolean;
|
|
113
|
+
prompt: string;
|
|
114
|
+
title?: string;
|
|
115
|
+
copyState: CopyState;
|
|
116
|
+
onCopy: () => void;
|
|
117
|
+
onClose: () => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const contrastSafeFocusClassName = [
|
|
121
|
+
'focus-visible:outline-none focus-visible:ring-2',
|
|
122
|
+
'focus-visible:ring-[var(--color-text-primary)] focus-visible:ring-offset-2',
|
|
123
|
+
'focus-visible:ring-offset-[var(--color-bg-primary)]',
|
|
124
|
+
].join(' ');
|
|
125
|
+
|
|
126
|
+
const promptUtilityClassName = [
|
|
127
|
+
'inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-[var(--radius-lg)]',
|
|
128
|
+
'text-[var(--color-text-tertiary)] transition-colors',
|
|
129
|
+
'hover:bg-[var(--color-bg-hover)]',
|
|
130
|
+
'hover:text-[var(--color-primary)]',
|
|
131
|
+
contrastSafeFocusClassName,
|
|
132
|
+
].join(' ');
|
|
133
|
+
|
|
134
|
+
const primaryActionClassName = [
|
|
135
|
+
'inline-flex min-h-10 min-w-0 items-center gap-2 px-4 py-2 text-sm font-semibold',
|
|
136
|
+
'prompt-primary-action bg-[var(--color-primary)] text-white transition-opacity cursor-pointer',
|
|
137
|
+
'hover:opacity-90',
|
|
138
|
+
contrastSafeFocusClassName,
|
|
139
|
+
].join(' ');
|
|
140
|
+
|
|
141
|
+
function PromptDialog({
|
|
142
|
+
isOpen,
|
|
143
|
+
prompt,
|
|
144
|
+
title,
|
|
145
|
+
copyState,
|
|
146
|
+
onCopy,
|
|
147
|
+
onClose,
|
|
148
|
+
}: PromptDialogProps): ReactElement {
|
|
149
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
150
|
+
const focusFrameRef = useRef<number | null>(null);
|
|
151
|
+
useBodyScrollLock(isOpen);
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const dialog = dialogRef.current;
|
|
155
|
+
if (!dialog) return;
|
|
156
|
+
if (isOpen && !dialog.open) {
|
|
157
|
+
dialog.showModal();
|
|
158
|
+
focusFrameRef.current = requestAnimationFrame(() => {
|
|
159
|
+
dialog.querySelector<HTMLButtonElement>('[aria-label="Close prompt"]')?.focus();
|
|
160
|
+
});
|
|
161
|
+
} else if (!isOpen && dialog.open) {
|
|
162
|
+
dialog.close();
|
|
163
|
+
}
|
|
164
|
+
return () => {
|
|
165
|
+
if (focusFrameRef.current !== null) cancelAnimationFrame(focusFrameRef.current);
|
|
166
|
+
};
|
|
167
|
+
}, [isOpen]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!isOpen) return;
|
|
171
|
+
const handleKeyDown = (event: KeyboardEvent): void => {
|
|
172
|
+
if (event.key === 'Escape') {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
event.stopPropagation();
|
|
175
|
+
onClose();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (event.key !== 'Tab' || !dialogRef.current) return;
|
|
179
|
+
const focusable = Array.from(dialogRef.current.querySelectorAll<HTMLElement>(
|
|
180
|
+
'button, [href], [tabindex]:not([tabindex="-1"])',
|
|
181
|
+
)).filter((element) => !element.hasAttribute('disabled'));
|
|
182
|
+
if (focusable.length === 0) return;
|
|
183
|
+
const first = focusable[0];
|
|
184
|
+
const last = focusable[focusable.length - 1];
|
|
185
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
last.focus();
|
|
188
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
first.focus();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
window.addEventListener('keydown', handleKeyDown, true);
|
|
194
|
+
return () => window.removeEventListener('keydown', handleKeyDown, true);
|
|
195
|
+
}, [isOpen, onClose]);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<dialog
|
|
199
|
+
ref={dialogRef}
|
|
200
|
+
aria-modal="true"
|
|
201
|
+
aria-label={title ?? 'Prompt'}
|
|
202
|
+
className="prompt-dialog m-auto max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-4xl overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-bg-primary)] p-0 text-[var(--color-text-primary)] shadow-[var(--shadow-lg)]"
|
|
203
|
+
onCancel={(event) => {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
onClose();
|
|
206
|
+
}}
|
|
207
|
+
onMouseDown={(event) => {
|
|
208
|
+
if (event.target === event.currentTarget) onClose();
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<div className="flex max-h-[calc(100dvh-2rem)] min-h-0 flex-col">
|
|
212
|
+
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-[var(--color-border)] px-4 py-3">
|
|
213
|
+
<h2 className="prompt-dialog-title m-0 min-w-0 truncate text-sm font-semibold text-[var(--color-text-primary)]">
|
|
214
|
+
{title ?? 'Prompt'}
|
|
215
|
+
</h2>
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
aria-label="Close prompt"
|
|
219
|
+
className={promptUtilityClassName}
|
|
220
|
+
onClick={onClose}
|
|
221
|
+
>
|
|
222
|
+
<i className="fa-solid fa-xmark" aria-hidden="true" />
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
<div className="flex min-h-0 flex-1 gap-3 overflow-hidden p-4">
|
|
226
|
+
<pre
|
|
227
|
+
data-testid="prompt-dialog-source"
|
|
228
|
+
role="region"
|
|
229
|
+
aria-label="Prompt source"
|
|
230
|
+
tabIndex={0}
|
|
231
|
+
className={`m-0 min-h-0 min-w-0 flex-1 overflow-auto overscroll-contain whitespace-pre-wrap break-words font-mono text-sm leading-6 text-[var(--color-text-primary)] ${contrastSafeFocusClassName}`}
|
|
232
|
+
>
|
|
233
|
+
{prompt}
|
|
234
|
+
</pre>
|
|
235
|
+
<PromptCopyButton
|
|
236
|
+
copyState={copyState}
|
|
237
|
+
className={promptUtilityClassName}
|
|
238
|
+
onCopy={onCopy}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
<span role="status" aria-live="polite" className="sr-only">
|
|
242
|
+
{copyState === 'copied' ? 'Copied' : copyState === 'error' ? 'Copy failed' : ''}
|
|
243
|
+
</span>
|
|
244
|
+
</div>
|
|
245
|
+
<style jsx>{`
|
|
246
|
+
.prompt-dialog::backdrop {
|
|
247
|
+
background: var(--color-text-primary);
|
|
248
|
+
opacity: 0.6;
|
|
249
|
+
backdrop-filter: blur(4px);
|
|
250
|
+
}
|
|
251
|
+
/* Outranks the docs themes' .prose h2 rules, which reach into the
|
|
252
|
+
dialog and would inflate this chrome heading. */
|
|
253
|
+
.prompt-dialog .prompt-dialog-title {
|
|
254
|
+
margin: 0;
|
|
255
|
+
font-size: 0.875rem;
|
|
256
|
+
line-height: 1.5;
|
|
257
|
+
font-weight: 600;
|
|
258
|
+
letter-spacing: normal;
|
|
259
|
+
}
|
|
260
|
+
`}</style>
|
|
261
|
+
</dialog>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
73
265
|
export function Prompt({
|
|
74
266
|
children,
|
|
75
267
|
title,
|
|
@@ -84,9 +276,12 @@ export function Prompt({
|
|
|
84
276
|
}: PromptProps): ReactElement {
|
|
85
277
|
const [copyState, setCopyState] = useState<CopyState>('idle');
|
|
86
278
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
279
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
87
280
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
88
281
|
const copyAttemptRef = useRef(0);
|
|
89
282
|
const mountedRef = useRef(false);
|
|
283
|
+
const expandTriggerRef = useRef<HTMLButtonElement>(null);
|
|
284
|
+
const returnFocusFrameRef = useRef<number | null>(null);
|
|
90
285
|
const menuBoundaryRef = useRef<HTMLDivElement>(null);
|
|
91
286
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
92
287
|
const itemRefs = useRef<Array<HTMLAnchorElement | null>>([]);
|
|
@@ -105,6 +300,8 @@ export function Prompt({
|
|
|
105
300
|
const hasTitle = Boolean(title?.trim());
|
|
106
301
|
const shouldHidePrompt = hasTitle && (hidePrompt === true || hidePrompt === 'true');
|
|
107
302
|
const authorIcon = resolveAuthorIcon(icon, iconType);
|
|
303
|
+
const hasDescription = description !== undefined;
|
|
304
|
+
const hasHeader = hasTitle || resolvedActions.length > 0;
|
|
108
305
|
|
|
109
306
|
const closeMenu = useCallback(() => {
|
|
110
307
|
setMenuOpen(false);
|
|
@@ -115,6 +312,13 @@ export function Prompt({
|
|
|
115
312
|
triggerRef.current?.focus();
|
|
116
313
|
}, []);
|
|
117
314
|
|
|
315
|
+
const closeDialog = useCallback(() => {
|
|
316
|
+
setDialogOpen(false);
|
|
317
|
+
returnFocusFrameRef.current = requestAnimationFrame(() => {
|
|
318
|
+
expandTriggerRef.current?.focus();
|
|
319
|
+
});
|
|
320
|
+
}, []);
|
|
321
|
+
|
|
118
322
|
useOnClickOutside(menuBoundaryRef, closeMenu, menuOpen);
|
|
119
323
|
|
|
120
324
|
useEffect(() => {
|
|
@@ -126,6 +330,12 @@ export function Prompt({
|
|
|
126
330
|
};
|
|
127
331
|
}, []);
|
|
128
332
|
|
|
333
|
+
useEffect(() => () => {
|
|
334
|
+
if (returnFocusFrameRef.current !== null) {
|
|
335
|
+
cancelAnimationFrame(returnFocusFrameRef.current);
|
|
336
|
+
}
|
|
337
|
+
}, []);
|
|
338
|
+
|
|
129
339
|
useEffect(() => {
|
|
130
340
|
if (menuOpen) itemRefs.current[0]?.focus();
|
|
131
341
|
}, [menuOpen]);
|
|
@@ -193,157 +403,208 @@ export function Prompt({
|
|
|
193
403
|
}, [closeMenuAndFocusTrigger, secondary.length]);
|
|
194
404
|
|
|
195
405
|
const rootClassName = [
|
|
196
|
-
'not-prose my-4 min-w-0 max-w-full overflow-visible',
|
|
197
|
-
'rounded-[var(--radius-lg)] border
|
|
198
|
-
'bg-[var(--color-bg-secondary)] text-[var(--color-text-primary)]',
|
|
199
|
-
'shadow-[var(--shadow-sm)]',
|
|
406
|
+
'prompt-card-fern not-prose my-4 min-w-0 max-w-full overflow-visible',
|
|
407
|
+
'rounded-[calc(var(--radius-lg)+0.25rem)] border text-[var(--color-text-primary)]',
|
|
200
408
|
className,
|
|
201
409
|
].filter(Boolean).join(' ');
|
|
202
410
|
|
|
203
|
-
const actionClassName = [
|
|
204
|
-
'inline-flex min-w-0 max-w-full items-center gap-2 rounded-[var(--radius-lg)]',
|
|
205
|
-
'border border-[var(--color-border)] px-3 py-2 text-sm font-medium',
|
|
206
|
-
'text-[var(--color-text-primary)] transition-colors cursor-pointer',
|
|
207
|
-
'hover:border-[var(--color-primary)] hover:text-[var(--color-primary)]',
|
|
208
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)]',
|
|
209
|
-
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
210
|
-
].join(' ');
|
|
211
|
-
|
|
212
411
|
return (
|
|
213
412
|
<section data-testid="prompt-card" className={rootClassName}>
|
|
214
|
-
|
|
215
|
-
data-testid={!hasTitle ? 'prompt-body-row' : undefined}
|
|
216
|
-
className={hasTitle
|
|
217
|
-
? 'contents'
|
|
218
|
-
: 'flex min-w-0 max-w-full flex-wrap items-start gap-3 px-4 py-4'}
|
|
219
|
-
>
|
|
413
|
+
{hasHeader ? (
|
|
220
414
|
<div
|
|
221
|
-
data-testid=
|
|
222
|
-
className=
|
|
223
|
-
? 'flex min-w-0 max-w-full items-center gap-3 px-4 pt-4'
|
|
224
|
-
: 'contents'}
|
|
415
|
+
data-testid="prompt-header"
|
|
416
|
+
className="flex min-w-0 flex-col items-stretch gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5"
|
|
225
417
|
>
|
|
226
|
-
<span
|
|
227
|
-
data-testid="prompt-icon"
|
|
228
|
-
className="shrink-0 text-[var(--color-primary)]"
|
|
229
|
-
aria-hidden="true"
|
|
230
|
-
>
|
|
231
|
-
<Icon icon={authorIcon} size={18} />
|
|
232
|
-
</span>
|
|
233
418
|
{hasTitle ? (
|
|
234
|
-
<h3 className="m-0
|
|
419
|
+
<h3 className="prompt-title m-0 w-full min-w-0 break-words [overflow-wrap:anywhere] text-base font-semibold text-[var(--color-text-primary)] sm:w-auto sm:flex-1">
|
|
235
420
|
{title}
|
|
236
421
|
</h3>
|
|
237
422
|
) : null}
|
|
238
|
-
</div>
|
|
239
|
-
|
|
240
|
-
{!shouldHidePrompt ? (
|
|
241
423
|
<div
|
|
242
|
-
data-testid="prompt-
|
|
243
|
-
className=
|
|
424
|
+
data-testid="prompt-actions"
|
|
425
|
+
className="flex min-w-0 max-w-full items-stretch justify-end self-end sm:ml-auto sm:self-auto"
|
|
244
426
|
>
|
|
245
|
-
{
|
|
427
|
+
{primary ? (
|
|
428
|
+
<a
|
|
429
|
+
href={primary.href}
|
|
430
|
+
target="_blank"
|
|
431
|
+
rel="noopener noreferrer"
|
|
432
|
+
aria-label={primary.label}
|
|
433
|
+
className={`${primaryActionClassName} ${secondary.length > 0 ? 'rounded-l-[var(--radius-lg)]' : 'rounded-[var(--radius-lg)]'}`}
|
|
434
|
+
>
|
|
435
|
+
<span aria-hidden="true"><ActionIcon action={primary.action} /></span>
|
|
436
|
+
<span className="min-w-0 truncate">{primary.label}</span>
|
|
437
|
+
</a>
|
|
438
|
+
) : null}
|
|
439
|
+
{secondary.length > 0 ? (
|
|
440
|
+
<div ref={menuBoundaryRef} className="relative flex shrink-0">
|
|
441
|
+
<button
|
|
442
|
+
ref={triggerRef}
|
|
443
|
+
type="button"
|
|
444
|
+
aria-label="More prompt actions"
|
|
445
|
+
aria-haspopup="menu"
|
|
446
|
+
aria-expanded={menuOpen}
|
|
447
|
+
aria-controls={menuId}
|
|
448
|
+
className={`${primaryActionClassName} min-w-10 rounded-r-[var(--radius-lg)] border-l border-[var(--color-bg-primary)] px-3`}
|
|
449
|
+
onMouseDown={(event) => {
|
|
450
|
+
if (menuOpen) event.preventDefault();
|
|
451
|
+
}}
|
|
452
|
+
onClick={() => {
|
|
453
|
+
setMenuOpen((open) => !open);
|
|
454
|
+
if (menuOpen) triggerRef.current?.focus();
|
|
455
|
+
}}
|
|
456
|
+
>
|
|
457
|
+
<i className="fa-solid fa-chevron-down text-xs" aria-hidden="true" />
|
|
458
|
+
</button>
|
|
459
|
+
{menuOpen ? (
|
|
460
|
+
<div
|
|
461
|
+
id={menuId}
|
|
462
|
+
role="menu"
|
|
463
|
+
aria-label="More prompt actions"
|
|
464
|
+
className="absolute right-0 top-full z-20 mt-2 w-max max-w-[min(20rem,calc(100vw-2rem))] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-bg-primary)] p-1 shadow-[var(--shadow-sm)]"
|
|
465
|
+
onKeyDown={handleMenuKeyDown}
|
|
466
|
+
onBlur={(event) => {
|
|
467
|
+
if (!event.currentTarget.contains(event.relatedTarget)) closeMenu();
|
|
468
|
+
}}
|
|
469
|
+
>
|
|
470
|
+
{secondary.map((action, index) => (
|
|
471
|
+
<a
|
|
472
|
+
key={`${action.label}-${action.href}-${index}`}
|
|
473
|
+
ref={(node) => { itemRefs.current[index] = node; }}
|
|
474
|
+
role="menuitem"
|
|
475
|
+
tabIndex={-1}
|
|
476
|
+
href={action.href}
|
|
477
|
+
target="_blank"
|
|
478
|
+
rel="noopener noreferrer"
|
|
479
|
+
aria-label={action.label}
|
|
480
|
+
className={`prompt-menu-item flex min-h-10 min-w-0 max-w-full cursor-pointer items-center gap-2 rounded-[var(--radius-lg)] px-3 py-2 text-sm text-[var(--color-text-primary)] hover:bg-[var(--color-border)] ${contrastSafeFocusClassName}`}
|
|
481
|
+
onClick={closeMenuAndFocusTrigger}
|
|
482
|
+
>
|
|
483
|
+
<span aria-hidden="true"><ActionIcon action={action.action} /></span>
|
|
484
|
+
<span className="min-w-0 truncate">{action.label}</span>
|
|
485
|
+
</a>
|
|
486
|
+
))}
|
|
487
|
+
</div>
|
|
488
|
+
) : null}
|
|
489
|
+
</div>
|
|
490
|
+
) : null}
|
|
491
|
+
{shouldHidePrompt && hasPrompt ? (
|
|
492
|
+
<PromptCopyButton
|
|
493
|
+
copyState={copyState}
|
|
494
|
+
className={`${promptUtilityClassName} ml-2`}
|
|
495
|
+
onCopy={copyPrompt}
|
|
496
|
+
/>
|
|
497
|
+
) : null}
|
|
246
498
|
</div>
|
|
247
|
-
|
|
499
|
+
</div>
|
|
500
|
+
) : null}
|
|
248
501
|
|
|
502
|
+
{!shouldHidePrompt ? (
|
|
249
503
|
<div
|
|
250
|
-
data-testid="prompt-
|
|
251
|
-
className={`flex min-w-0
|
|
504
|
+
data-testid="prompt-well"
|
|
505
|
+
className={`prompt-well mx-3 mb-3 flex min-w-0 items-start gap-3 rounded-[var(--radius-lg)] border bg-[var(--color-bg-primary)] px-3 py-3 sm:mx-4 sm:mb-4 sm:px-4 ${hasHeader ? '' : 'mt-3 sm:mt-4'}`}
|
|
252
506
|
>
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
data-copy-state={copyState}
|
|
258
|
-
disabled={!hasPrompt}
|
|
259
|
-
onClick={copyPrompt}
|
|
507
|
+
<span
|
|
508
|
+
data-testid="prompt-icon"
|
|
509
|
+
className="mt-1 shrink-0 text-[var(--color-primary)]"
|
|
510
|
+
aria-hidden="true"
|
|
260
511
|
>
|
|
261
|
-
<
|
|
262
|
-
<span>{copyState === 'copied' ? 'Copied' : copyState === 'error' ? 'Copy failed' : 'Copy'}</span>
|
|
263
|
-
</button>
|
|
264
|
-
|
|
265
|
-
<span role="status" aria-live="polite" className="sr-only">
|
|
266
|
-
{copyState === 'copied' ? 'Copied' : copyState === 'error' ? 'Copy failed' : ''}
|
|
512
|
+
<Icon icon={authorIcon} size={18} />
|
|
267
513
|
</span>
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
>
|
|
277
|
-
<span aria-hidden="true"><ActionIcon action={primary.action} /></span>
|
|
278
|
-
<span className="min-w-0 truncate">{primary.label}</span>
|
|
279
|
-
</a>
|
|
280
|
-
) : null}
|
|
281
|
-
|
|
282
|
-
{secondary.length > 0 ? (
|
|
283
|
-
<div
|
|
284
|
-
ref={menuBoundaryRef}
|
|
285
|
-
className="relative flex min-w-0 max-w-full basis-full w-full shrink-0 justify-end sm:ml-auto sm:basis-auto sm:w-auto sm:shrink"
|
|
286
|
-
>
|
|
514
|
+
<div
|
|
515
|
+
data-testid="prompt-preview"
|
|
516
|
+
className={`prompt-preview min-w-0 flex-1 text-sm leading-6 text-[var(--color-text-primary)]${hasPrompt && !hasDescription ? ' font-mono whitespace-pre-wrap' : ''}${isSingleLine ? ' prompt-preview-single-line' : ''}`}
|
|
517
|
+
>
|
|
518
|
+
{hasDescription ? renderInlineMarkdown(description) : prompt || children}
|
|
519
|
+
</div>
|
|
520
|
+
{hasPrompt ? (
|
|
521
|
+
<div data-testid="prompt-utilities" className="flex shrink-0 items-center gap-1">
|
|
287
522
|
<button
|
|
288
|
-
ref={
|
|
523
|
+
ref={expandTriggerRef}
|
|
289
524
|
type="button"
|
|
290
|
-
aria-label="
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
className={actionClassName}
|
|
295
|
-
onMouseDown={(event) => {
|
|
296
|
-
if (menuOpen) event.preventDefault();
|
|
297
|
-
}}
|
|
298
|
-
onClick={() => {
|
|
299
|
-
setMenuOpen((open) => !open);
|
|
300
|
-
if (menuOpen) triggerRef.current?.focus();
|
|
301
|
-
}}
|
|
525
|
+
aria-label="Expand prompt"
|
|
526
|
+
title="Expand prompt"
|
|
527
|
+
className={promptUtilityClassName}
|
|
528
|
+
onClick={() => setDialogOpen(true)}
|
|
302
529
|
>
|
|
303
|
-
<
|
|
304
|
-
<i className="fa-solid fa-chevron-down text-xs" aria-hidden="true" />
|
|
530
|
+
<i className="fa-solid fa-expand" aria-hidden="true" />
|
|
305
531
|
</button>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
aria-label="More prompt actions"
|
|
312
|
-
className="absolute top-full right-0 z-20 mt-2 w-max max-w-[min(20rem,calc(100vw-2rem))] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-bg-secondary)] p-1 shadow-[var(--shadow-sm)]"
|
|
313
|
-
onKeyDown={handleMenuKeyDown}
|
|
314
|
-
onBlur={(event) => {
|
|
315
|
-
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
316
|
-
closeMenu();
|
|
317
|
-
}
|
|
318
|
-
}}
|
|
319
|
-
>
|
|
320
|
-
{secondary.map((action, index) => (
|
|
321
|
-
<a
|
|
322
|
-
key={`${action.label}-${action.href}-${index}`}
|
|
323
|
-
ref={(node) => { itemRefs.current[index] = node; }}
|
|
324
|
-
role="menuitem"
|
|
325
|
-
tabIndex={-1}
|
|
326
|
-
href={action.href}
|
|
327
|
-
target="_blank"
|
|
328
|
-
rel="noopener noreferrer"
|
|
329
|
-
aria-label={action.label}
|
|
330
|
-
className="flex min-w-0 max-w-full cursor-pointer items-center gap-2 rounded-[var(--radius-lg)] px-3 py-2 text-sm text-[var(--color-text-primary)] hover:bg-[var(--color-border)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary)]"
|
|
331
|
-
onClick={closeMenuAndFocusTrigger}
|
|
332
|
-
>
|
|
333
|
-
<span aria-hidden="true"><ActionIcon action={action.action} /></span>
|
|
334
|
-
<span className="min-w-0 truncate">{action.label}</span>
|
|
335
|
-
</a>
|
|
336
|
-
))}
|
|
337
|
-
</div>
|
|
338
|
-
) : null}
|
|
532
|
+
<PromptCopyButton
|
|
533
|
+
copyState={copyState}
|
|
534
|
+
className={promptUtilityClassName}
|
|
535
|
+
onCopy={copyPrompt}
|
|
536
|
+
/>
|
|
339
537
|
</div>
|
|
340
538
|
) : null}
|
|
341
539
|
</div>
|
|
342
|
-
|
|
540
|
+
) : null}
|
|
541
|
+
{!dialogOpen ? (
|
|
542
|
+
<span role="status" aria-live="polite" className="sr-only">
|
|
543
|
+
{copyState === 'copied' ? 'Copied' : copyState === 'error' ? 'Copy failed' : ''}
|
|
544
|
+
</span>
|
|
545
|
+
) : null}
|
|
546
|
+
|
|
547
|
+
<PromptDialog
|
|
548
|
+
isOpen={dialogOpen}
|
|
549
|
+
prompt={prompt}
|
|
550
|
+
title={hasTitle ? title!.trim() : undefined}
|
|
551
|
+
copyState={copyState}
|
|
552
|
+
onCopy={copyPrompt}
|
|
553
|
+
onClose={closeDialog}
|
|
554
|
+
/>
|
|
343
555
|
|
|
344
556
|
<style jsx>{`
|
|
557
|
+
.prompt-card-fern {
|
|
558
|
+
border-color: var(--color-primary);
|
|
559
|
+
background: var(--color-bg-secondary);
|
|
560
|
+
}
|
|
561
|
+
.prompt-well {
|
|
562
|
+
border-color: var(--color-border);
|
|
563
|
+
}
|
|
564
|
+
.prompt-primary-action {
|
|
565
|
+
background: var(--color-primary);
|
|
566
|
+
color: #fff;
|
|
567
|
+
}
|
|
568
|
+
.prompt-title {
|
|
569
|
+
color: var(--color-text-primary);
|
|
570
|
+
}
|
|
571
|
+
/* The docs themes' prose CSS is not Tailwind Typography — .prose h3 and
|
|
572
|
+
.prose a match descendants even inside .not-prose, and the jam theme's
|
|
573
|
+
body[data-theme] variants outrank Tailwind utilities. These scoped
|
|
574
|
+
rules win on specificity to keep the header compact and link-free. */
|
|
575
|
+
.prompt-card-fern .prompt-title {
|
|
576
|
+
margin: 0;
|
|
577
|
+
font-size: 1rem;
|
|
578
|
+
line-height: 1.5;
|
|
579
|
+
font-weight: 600;
|
|
580
|
+
letter-spacing: normal;
|
|
581
|
+
}
|
|
582
|
+
.prompt-card-fern .prompt-primary-action {
|
|
583
|
+
text-decoration: none;
|
|
584
|
+
font-weight: 600;
|
|
585
|
+
}
|
|
586
|
+
.prompt-card-fern .prompt-menu-item {
|
|
587
|
+
text-decoration: none;
|
|
588
|
+
font-weight: 400;
|
|
589
|
+
color: var(--color-text-primary);
|
|
590
|
+
}
|
|
591
|
+
@supports (color: color-mix(in srgb, black, white)) {
|
|
592
|
+
.prompt-card-fern {
|
|
593
|
+
border-color: color-mix(in srgb, var(--color-primary) 48%, var(--color-border));
|
|
594
|
+
background: color-mix(in srgb, var(--color-primary) 7%, var(--color-bg-primary));
|
|
595
|
+
}
|
|
596
|
+
.prompt-well {
|
|
597
|
+
border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-primary));
|
|
598
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 3%, transparent);
|
|
599
|
+
}
|
|
600
|
+
/* 40% primary keeps >=4.5:1 against the card background even when
|
|
601
|
+
--color-primary is set to the mode's background extreme; 45% fails. */
|
|
602
|
+
.prompt-card-fern .prompt-title {
|
|
603
|
+
color: color-mix(in srgb, var(--color-primary) 40%, var(--color-text-primary));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
345
606
|
.prompt-preview {
|
|
346
|
-
max-height:
|
|
607
|
+
max-height: 10rem;
|
|
347
608
|
overflow-y: auto;
|
|
348
609
|
overflow-wrap: anywhere;
|
|
349
610
|
}
|
|
@@ -760,7 +760,11 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
760
760
|
const visibilityClass = showTabsInSidebar ? '' : 'lg:hidden';
|
|
761
761
|
|
|
762
762
|
return (
|
|
763
|
-
|
|
763
|
+
// Inline-start padding is 12px (pl-3), not 16px, so each tab's px-3
|
|
764
|
+
// padding lands its icon at 24px — flush with the sidebar group icons
|
|
765
|
+
// (ml-6) below — and the active tab's highlight left edge lands at 12px
|
|
766
|
+
// to match the lower nav items' pill (.sidebar-scroll ...::before).
|
|
767
|
+
<div className={`sidebar-tabs flex flex-col gap-2 pl-3 pr-4 pt-2 pb-6 ${visibilityClass}`}>
|
|
764
768
|
{resolvedNav.tabs.map((tab) => {
|
|
765
769
|
const isActive = tab.name === resolvedNav.activeTab;
|
|
766
770
|
|
|
@@ -30,9 +30,10 @@ interface CaptureResult {
|
|
|
30
30
|
error?: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// Headroom for the retry path: two 15s page.goto budgets + 3s
|
|
34
|
-
//
|
|
35
|
-
// (closed-target only) fail fast
|
|
33
|
+
// Headroom for the retry path: two 15s page.goto('load') budgets + 3s retry
|
|
34
|
+
// wait + 2s best-effort networkidle + 1s settle total ~36s worst case before
|
|
35
|
+
// the screenshot is taken. Launch retries (closed-target only) fail fast
|
|
36
|
+
// (<~2s) so they stay inside this 45s race.
|
|
36
37
|
const SCREENSHOT_TIMEOUT_MS = 45000;
|
|
37
38
|
|
|
38
39
|
// Playwright launch flakes intermittently in the container — the Chromium
|
|
@@ -201,22 +202,31 @@ export async function captureHomepageScreenshot(options: CaptureOptions): Promis
|
|
|
201
202
|
// Cold render after a project-wide revalidation can transiently 404 —
|
|
202
203
|
// retry once on 4xx/5xx, then capture whatever the page shows so the
|
|
203
204
|
// dashboard preview reflects the real state of the site.
|
|
205
|
+
//
|
|
206
|
+
// Wait for 'load' (document + images/CSS/fonts), NOT 'networkidle'. A
|
|
207
|
+
// custom-domain-only subdomain 404s the client's RSC nav prefetches
|
|
208
|
+
// (`/docs/<page>?_rsc=…`) — they're built from the route and carry no
|
|
209
|
+
// ?jd_proxy=1 marker — so Next keeps retrying them and a large docs site
|
|
210
|
+
// (jamdesk-docs) NEVER goes network-idle. 'networkidle' timed out at 15s
|
|
211
|
+
// and every capture/Retry failed. 'load' fires before the prefetch storm.
|
|
212
|
+
const gotoOpts = { waitUntil: 'load', timeout: 15000 } as const;
|
|
204
213
|
console.log(`Capturing screenshot from: ${homepageUrl}`);
|
|
205
|
-
const firstResponse = await page.goto(homepageUrl,
|
|
206
|
-
waitUntil: 'networkidle',
|
|
207
|
-
timeout: 15000,
|
|
208
|
-
});
|
|
214
|
+
const firstResponse = await page.goto(homepageUrl, gotoOpts);
|
|
209
215
|
const firstStatus = firstResponse?.status();
|
|
210
216
|
|
|
211
217
|
if (firstStatus !== undefined && firstStatus >= 400) {
|
|
212
218
|
console.log(`Screenshot got ${firstStatus} on first attempt, retrying once after 3s`);
|
|
213
219
|
await page.waitForTimeout(3000);
|
|
214
|
-
await page.goto(homepageUrl,
|
|
215
|
-
waitUntil: 'networkidle',
|
|
216
|
-
timeout: 15000,
|
|
217
|
-
});
|
|
220
|
+
await page.goto(homepageUrl, gotoOpts);
|
|
218
221
|
}
|
|
219
222
|
|
|
223
|
+
// Short best-effort settle for late images/fonts — the page above already
|
|
224
|
+
// reached 'load', and a never-idle page (see the prefetch note above)
|
|
225
|
+
// would otherwise burn the full timeout for nothing. Never fail the
|
|
226
|
+
// capture on it. Mirrors the pdf-service networkidle fallback
|
|
227
|
+
// (worker-pool.ts).
|
|
228
|
+
await page.waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
|
|
229
|
+
|
|
220
230
|
// Additional wait for any late-loading content
|
|
221
231
|
await page.waitForTimeout(1000);
|
|
222
232
|
|