Skip to content

Commit 6d29d2e

Browse files
committed
Preserve logs on timeout, float copy button, hide editor line numbers
The Output panel keeps the full log trail and renders any error or timeout message beneath it instead of replacing the logs. A copy button floats in the panel's top-right corner: a quiet icon that lifts on hover and flips to a check on success, copying the logs plus any trailing error. The Monaco example editor hides line numbers and collapses the gutter to reclaim horizontal and vertical space, notably on mobile.
1 parent e2c4903 commit 6d29d2e

3 files changed

Lines changed: 126 additions & 29 deletions

File tree

playground/src/app/globals.css

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,7 @@ button {
780780
/* Inline console (no outer head, since the tab bar provides it) */
781781

782782
.console--inline {
783+
position: relative;
783784
background: var(--console-bg);
784785
border: 1px solid var(--console-rule);
785786
border-radius: var(--radius);
@@ -1004,6 +1005,43 @@ button {
10041005
max-height: 420px;
10051006
overflow: auto;
10061007
}
1008+
.console__copy {
1009+
position: absolute;
1010+
top: 8px;
1011+
right: 8px;
1012+
z-index: 1;
1013+
display: inline-flex;
1014+
align-items: center;
1015+
justify-content: center;
1016+
width: 30px;
1017+
height: 30px;
1018+
padding: 0;
1019+
color: var(--console-mute);
1020+
background: color-mix(in srgb, var(--console-bg) 72%, transparent);
1021+
border: none;
1022+
border-radius: 8px;
1023+
cursor: pointer;
1024+
opacity: 0.55;
1025+
backdrop-filter: blur(4px);
1026+
transition: opacity 120ms ease, color 120ms ease, background 120ms ease;
1027+
}
1028+
.console__copy svg {
1029+
width: 15px;
1030+
height: 15px;
1031+
}
1032+
.console--inline:hover .console__copy,
1033+
.console__copy:focus-visible {
1034+
opacity: 1;
1035+
}
1036+
.console__copy:hover {
1037+
opacity: 1;
1038+
color: var(--console-ink);
1039+
background: color-mix(in srgb, var(--console-ink) 10%, transparent);
1040+
}
1041+
.console__copy[data-copied="true"] {
1042+
opacity: 1;
1043+
color: var(--good);
1044+
}
10071045
.console__body--error {
10081046
color: #ffb4b4;
10091047
}

playground/src/components/ExampleEditor.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ export function ExampleEditor({
6666
options={{
6767
minimap: { enabled: false },
6868
fontSize: 13,
69+
lineNumbers: "off",
70+
lineDecorationsWidth: 0,
71+
lineNumbersMinChars: 0,
72+
glyphMargin: false,
6973
scrollBeyondLastLine: false,
7074
tabSize: 2,
7175
padding: { top: 12, bottom: 12 },

playground/src/components/MethodView.tsx

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export function MethodView({
6666
const [running, setRunning] = useState(false);
6767
const [error, setError] = useState("");
6868
const [tab, setTab] = useState<"example" | "output">("example");
69+
const [copied, setCopied] = useState(false);
6970
const callAbortRef = useRef<((reason: string) => void) | null>(null);
7071
const cancelRunRef = useRef<(() => void) | null>(null);
7172

@@ -150,6 +151,20 @@ export function MethodView({
150151
callAbortRef.current?.("Call aborted");
151152
};
152153

154+
const handleCopyLogs = async () => {
155+
const text = [...logs.map((entry) => entry.text), ...(error ? [error] : [])]
156+
.join("\n")
157+
.trim();
158+
if (!text) return;
159+
try {
160+
await navigator.clipboard.writeText(text);
161+
setCopied(true);
162+
setTimeout(() => setCopied(false), 1500);
163+
} catch {
164+
/* clipboard unavailable */
165+
}
166+
};
167+
153168
const kind = methodInfo?.type ?? "unary";
154169

155170
const status: Status = error
@@ -305,41 +320,81 @@ export function MethodView({
305320
</>
306321
) : (
307322
<div className="console console--inline" data-status={status}>
308-
{error ? (
309-
<div
310-
className="console__body console__body--error"
311-
data-testid="error-display"
312-
>
313-
{error}
314-
{isHostMissingError(error) && (
315-
<div className="console__cta">
316-
<a
317-
className="open-in-dotli"
318-
href={hostedPlaygroundUrl(service, method)}
319-
target="_blank"
320-
rel="noreferrer"
321-
title="Open this example in the host-backed playground"
323+
{logs.length > 0 || error ? (
324+
<>
325+
<button
326+
type="button"
327+
className="console__copy"
328+
data-copied={copied}
329+
onClick={handleCopyLogs}
330+
aria-label="Copy output to clipboard"
331+
title="Copy output"
332+
>
333+
{copied ? (
334+
<svg
335+
viewBox="0 0 24 24"
336+
fill="none"
337+
stroke="currentColor"
338+
strokeWidth="2"
339+
strokeLinecap="round"
340+
strokeLinejoin="round"
341+
aria-hidden
342+
>
343+
<path d="M20 6 9 17l-5-5" />
344+
</svg>
345+
) : (
346+
<svg
347+
viewBox="0 0 24 24"
348+
fill="none"
349+
stroke="currentColor"
350+
strokeWidth="2"
351+
strokeLinecap="round"
352+
strokeLinejoin="round"
353+
aria-hidden
322354
>
323-
Run in hosted playground ↗
324-
</a>
355+
<rect x="8" y="8" width="14" height="14" rx="2" ry="2" />
356+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
357+
</svg>
358+
)}
359+
</button>
360+
{logs.length > 0 && (
361+
<div className="console__body" data-testid="stream-log">
362+
{logs.map((entry, i) => (
363+
<div
364+
key={i}
365+
className={`console__entry console__entry--${entry.level}`}
366+
data-testid="stream-entry"
367+
>
368+
<span className="console__entry-i">
369+
{String(i + 1).padStart(2, "0")}
370+
</span>
371+
<span className="console__entry-body">{entry.text}</span>
372+
</div>
373+
))}
325374
</div>
326375
)}
327-
</div>
328-
) : logs.length > 0 ? (
329-
<div className="console__body" data-testid="stream-log">
330-
{logs.map((entry, i) => (
376+
{error && (
331377
<div
332-
key={i}
333-
className={`console__entry console__entry--${entry.level}`}
334-
data-testid="stream-entry"
378+
className="console__body console__body--error"
379+
data-testid="error-display"
335380
>
336-
<span className="console__entry-i">
337-
{String(i + 1).padStart(2, "0")}
338-
</span>
339-
<span className="console__entry-body">{entry.text}</span>
381+
{error}
382+
{isHostMissingError(error) && (
383+
<div className="console__cta">
384+
<a
385+
className="open-in-dotli"
386+
href={hostedPlaygroundUrl(service, method)}
387+
target="_blank"
388+
rel="noreferrer"
389+
title="Open this example in the host-backed playground"
390+
>
391+
Run in hosted playground ↗
392+
</a>
393+
</div>
394+
)}
340395
</div>
341-
))}
342-
</div>
396+
)}
397+
</>
343398
) : (
344399
<div className="console__body console__body--empty">
345400
{!runnable

0 commit comments

Comments
 (0)