diff --git a/src/lib/share-image.ts b/src/lib/share-image.ts index 04017e3..25cbb85 100644 --- a/src/lib/share-image.ts +++ b/src/lib/share-image.ts @@ -18,6 +18,7 @@ export const TRACE_CARD_HEIGHT = 630; const MAX_STACK_FRAMES = 8; const MAX_QUEUE_ITEMS = 7; const SNIPPET_LINES = 4; +const MAX_DPR = 3; const FONT = '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace'; @@ -82,7 +83,8 @@ export function snippetAround( let start = Math.max(0, target - 2); start = Math.min(start, Math.max(0, src.length - SNIPPET_LINES)); const lines = src.slice(start, start + SNIPPET_LINES); - return { startLine: start + 1, lines, focusIndex: target - 1 - start }; + const focusIndex = currentLine === undefined ? -1 : target - 1 - start; + return { startLine: start + 1, lines, focusIndex }; } /* ------------------------------------------------------------------------- * @@ -119,11 +121,18 @@ async function ensureFonts(): Promise { export async function renderTraceCard(input: TraceCardInput): Promise { await ensureFonts(); + // Render at device resolution and scale the context back to logical units, so + // exports stay crisp on HiDPI/retina displays and in full-size previews. + const dpr = Math.max( + 1, + Math.min((typeof window !== "undefined" && window.devicePixelRatio) || 1, MAX_DPR), + ); const canvas = document.createElement("canvas"); - canvas.width = TRACE_CARD_WIDTH; - canvas.height = TRACE_CARD_HEIGHT; + canvas.width = TRACE_CARD_WIDTH * dpr; + canvas.height = TRACE_CARD_HEIGHT * dpr; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Canvas 2D is not available in this browser"); + ctx.scale(dpr, dpr); drawTraceCard(ctx, input); return await new Promise((resolve, reject) => { canvas.toBlob( @@ -286,13 +295,15 @@ function drawTraceCard(ctx: Ctx, input: TraceCardInput): void { const lineH = 24; const textX = innerX + 16 + gutterW; const maxTextW = innerW - 32 - gutterW; + // The active line spans the full inner content width (matching the live + // CodeMirror highlight), never just the width of the rendered text. + const codeLeft = innerX + 16; + const codeRight = innerX + innerW - 16; for (let i = 0; i < excerpt.lines.length; i++) { const y = codeTop + 46 + i * lineH; if (i === excerpt.focusIndex) { ctx.fillStyle = "oklch(0.892 0.181 102.4 / 0.12)"; - ctx.fillRect(textX - 8, y - lineH + 3, maxTextW + 8, lineH - 2); - ctx.fillStyle = C.stack; - ctx.fillRect(textX - 8, y - lineH + 3, 3, lineH - 2); + ctx.fillRect(codeLeft, y - lineH + 3, codeRight - codeLeft, lineH - 2); } ctx.fillStyle = C.muted; ctx.font = `400 12px ${FONT}`; diff --git a/tests/share-image.test.ts b/tests/share-image.test.ts index ed0ab87..d4954ee 100644 --- a/tests/share-image.test.ts +++ b/tests/share-image.test.ts @@ -63,14 +63,22 @@ describe("snippetAround", () => { expect(s.lines[3]).toBe("const v5 = 5;"); }); - test("uses the first line when no current line is known", () => { + test("renders the top of the file without a focus when no current line is known", () => { const code = "a();\nb();\nc();\n"; const s = snippetAround(code, undefined); expect(s.startLine).toBe(1); - expect(s.focusIndex).toBe(0); + expect(s.focusIndex).toBe(-1); expect(s.lines).toEqual(["a();", "b();", "c();"]); }); + test("highlights a line only when a current line is known", () => { + const code = "a();\nb();\nc();\nd();\ne();\nf();"; + expect(snippetAround(code, undefined).focusIndex).toBe(-1); + const withLine = snippetAround(code, 3); + expect(withLine.focusIndex).not.toBe(-1); + expect(withLine.lines[withLine.focusIndex]).toBe("c();"); + }); + test("clamps an out-of-range current line", () => { const code = "a();\nb();\n"; const s = snippetAround(code, 99);