diff --git a/components/elements/response-pdf-download.tsx b/components/elements/response-pdf-download.tsx index 27f8daccf..04954800a 100644 --- a/components/elements/response-pdf-download.tsx +++ b/components/elements/response-pdf-download.tsx @@ -45,6 +45,10 @@ function clonePrintableStyles(targetWindow: Window) { function createPrintOverrides(targetWindow: Window) { const style = targetWindow.document.createElement("style"); style.textContent = ` + * { + box-sizing: border-box; + } + :root { --background: #ffffff; --foreground: #111827; @@ -142,14 +146,32 @@ function createPrintOverrides(targetWindow: Window) { .claw-pdf-content { font-size: 14px; line-height: 1.7; + overflow: visible; + } + + .claw-pdf-content, + .claw-pdf-content * { + max-width: 100%; } [data-pdf-exclude="true"] { display: none !important; } - .chat-card-x-scroll { + .claw-pdf-table-card { + break-inside: auto; + page-break-inside: auto; overflow: visible !important; + width: 100% !important; + max-width: 100% !important; + border-radius: 10px !important; + } + + .chat-card-x-scroll { + display: block !important; + overflow: visible !important; + width: 100% !important; + max-width: 100% !important; padding-bottom: 0 !important; scrollbar-width: none !important; } @@ -159,13 +181,69 @@ function createPrintOverrides(targetWindow: Window) { } table { + width: 100% !important; + min-width: 0 !important; + max-width: 100% !important; + table-layout: fixed !important; + border-collapse: collapse !important; page-break-inside: auto; + break-inside: auto; + font-size: 11px !important; + line-height: 1.45 !important; + } + + .claw-pdf-wide-table { + font-size: 10px !important; + } + + .claw-pdf-extra-wide-table { + font-size: 9px !important; + } + + thead { + display: table-header-group; + } + + tbody { + display: table-row-group; + } + + tr { + page-break-inside: avoid; + break-inside: avoid; + } + + th, + td { + min-width: 0 !important; + width: auto !important; + max-width: none !important; + white-space: normal !important; + overflow-wrap: anywhere !important; + word-break: break-word !important; + text-overflow: clip !important; + vertical-align: top !important; + padding: 5px 7px !important; + line-height: 1.45 !important; + } + + th { + text-align: left !important; + font-weight: 700 !important; + } + + .claw-pdf-wide-table th, + .claw-pdf-wide-table td, + .claw-pdf-extra-wide-table th, + .claw-pdf-extra-wide-table td { + padding-left: 5px !important; + padding-right: 5px !important; } - tr, svg, img { page-break-inside: avoid; + break-inside: avoid; } svg { @@ -189,6 +267,58 @@ function createPrintOverrides(targetWindow: Window) { targetWindow.document.head.appendChild(style); } +function getTableColumnCount(table: HTMLTableElement) { + return Array.from(table.rows).reduce( + (maxColumns, row) => Math.max(maxColumns, row.cells.length), + 0 + ); +} + +function preparePrintableTables(root: HTMLElement) { + root.querySelectorAll("table").forEach((table) => { + const columnCount = getTableColumnCount(table); + table.dataset.pdfColumnCount = String(columnCount); + table.classList.toggle("claw-pdf-wide-table", columnCount >= 5); + table.classList.toggle("claw-pdf-extra-wide-table", columnCount >= 7); + + const scrollWrapper = table.closest(".chat-card-x-scroll"); + scrollWrapper?.parentElement?.classList.add("claw-pdf-table-card"); + }); +} + +function waitForFrame(targetWindow: Window) { + return new Promise((resolve) => { + targetWindow.requestAnimationFrame(() => resolve()); + }); +} + +async function waitForPrintableAssets(targetWindow: Window, root: HTMLElement) { + const fontReady = + "fonts" in targetWindow.document + ? targetWindow.document.fonts.ready.catch(() => undefined) + : Promise.resolve(); + const images = Array.from(root.querySelectorAll("img")); + const imageReady = Promise.all( + images.map((image) => { + if (image.complete && image.naturalWidth > 0) return Promise.resolve(); + if (typeof image.decode === "function") { + return image.decode().catch(() => undefined); + } + return new Promise((resolve) => { + image.addEventListener("load", () => resolve(), { once: true }); + image.addEventListener("error", () => resolve(), { once: true }); + }); + }) + ); + + await Promise.race([ + Promise.all([fontReady, imageReady]), + new Promise((resolve) => targetWindow.setTimeout(resolve, 1800)), + ]); + await waitForFrame(targetWindow); + await waitForFrame(targetWindow); +} + export function ResponsePdfDownloadButton({ contentRef, fileName, @@ -273,15 +403,21 @@ export function ResponsePdfDownloadButton({ clonedContent .querySelectorAll('[data-pdf-exclude="true"]') .forEach((node) => node.remove()); + preparePrintableTables(clonedContent); content.appendChild(clonedContent); shell.appendChild(content); printWindow.document.body.appendChild(shell); - window.setTimeout(() => { - setIsPreparing(false); - printWindow.focus(); - printWindow.print(); - }, 600); + void waitForPrintableAssets(printWindow, shell) + .catch((error) => { + console.warn("[Response PDF] asset readiness check failed:", error); + }) + .finally(() => { + setIsPreparing(false); + if (printWindow.closed) return; + printWindow.focus(); + printWindow.print(); + }); } catch (error) { setIsPreparing(false); printWindow.close();