From 00f16ca400a4c6b809f5e4a0064003ad6ee20ef7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:33:23 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/forms/custom-form-builder.tsx | 131 +++++++++++++++---- 1 file changed, 106 insertions(+), 25 deletions(-) diff --git a/src/components/forms/custom-form-builder.tsx b/src/components/forms/custom-form-builder.tsx index 0f8e38f..b183669 100644 --- a/src/components/forms/custom-form-builder.tsx +++ b/src/components/forms/custom-form-builder.tsx @@ -529,6 +529,84 @@ export function CustomFormBuilder() { doc.setFontSize(fontSize); const lineH = fontSize * 1.4; + // Group runs into visual lines (split where applied text contains "\n"). + const runsToLines = (runs: InlineRun[]): InlineRun[][] => { + const lines: InlineRun[][] = [[]]; + for (const r of runs) { + const parts = applyVariables(r.text, ctx).split("\n"); + parts.forEach((part, i) => { + if (i > 0) lines.push([]); + if (part) lines[lines.length - 1].push({ ...r, text: part }); + }); + } + return lines; + }; + + // Draw inline runs on one visual line, honoring word-wrap and per-run font style. + const drawRunsLine = ( + runs: InlineRun[], + startX: number, + startY: number, + widthLimit: number, + align: "left" | "center" | "right" | "justify", + ): number => { + type Token = { text: string; bold: boolean; italic: boolean; underline: boolean; isSpace: boolean }; + const tokens: Token[] = []; + for (const r of runs) { + const bold = !!r.bold; + const italic = !!r.italic; + const underline = !!r.underline; + const parts = r.text.split(/(\s+)/); + for (const p of parts) { + if (!p) continue; + tokens.push({ text: p, bold, italic, underline, isSpace: /^\s+$/.test(p) }); + } + } + const styleFor = (t: Token) => + t.bold && t.italic ? "bolditalic" : t.bold ? "bold" : t.italic ? "italic" : "normal"; + const widthOf = (t: Token) => { + doc.setFont(pdfFont, styleFor(t)); + return doc.getTextWidth(t.text); + }; + // Wrap into lines + const lines: Token[][] = [[]]; + let cur = 0; + for (const t of tokens) { + const w = widthOf(t); + if (cur + w > widthLimit && lines[lines.length - 1].length > 0) { + lines.push([]); + cur = 0; + if (t.isSpace) continue; + } + lines[lines.length - 1].push(t); + cur += w; + } + let yy = startY; + for (const ln of lines) { + while (ln.length && ln[ln.length - 1].isSpace) ln.pop(); + if (yy > pageH - margin) { + doc.addPage(); + yy = margin; + } + const lineW = ln.reduce((a, t) => a + widthOf(t), 0); + let x = startX; + if (align === "center") x = startX + (widthLimit - lineW) / 2; + else if (align === "right") x = startX + (widthLimit - lineW); + for (const t of ln) { + doc.setFont(pdfFont, styleFor(t)); + doc.text(t.text, x, yy); + if (t.underline && !t.isSpace) { + const w = widthOf(t); + doc.setLineWidth(0.5); + doc.line(x, yy + 1.5, x + w, yy + 1.5); + } + x += widthOf(t); + } + yy += lineH; + } + return yy; + }; + const drawTablePdf = (rows: TableCellSeg[][]) => { if (!rows.length) return; const cols = Math.max(...rows.map((r) => r.length)); @@ -536,11 +614,19 @@ export function CustomFormBuilder() { const cellPad = 4; for (let ri = 0; ri < rows.length; ri++) { const row = rows[ri]; - // Compute row height based on tallest cell - const cellLines = row.map((c) => - doc.splitTextToSize(applyVariables(c.text, ctx) || " ", colW - cellPad * 2), - ); - const rowH = Math.max(...cellLines.map((l) => l.length)) * lineH + cellPad * 2; + // Estimate row height by wrapping each cell's visual lines as plain text. + const cellLineCounts = row.map((c) => { + const ls = runsToLines(c?.runs ?? []); + let total = 0; + for (const lineRuns of ls) { + doc.setFont(pdfFont, "normal"); + const flat = lineRuns.map((r) => r.text).join(""); + const wrapped = doc.splitTextToSize(flat || " ", colW - cellPad * 2); + total += Math.max(1, wrapped.length); + } + return Math.max(1, total); + }); + const rowH = Math.max(...cellLineCounts) * lineH + cellPad * 2; if (y + rowH > pageH - margin) { doc.addPage(); y = margin; @@ -555,13 +641,11 @@ export function CustomFormBuilder() { if (borders.b) doc.line(x, y + rowH, x + colW, y + rowH); if (borders.l) doc.line(x, y, x, y + rowH); if (borders.r) doc.line(x + colW, y, x + colW, y + rowH); - const isHeader = cell?.isHeader ?? false; - doc.setFont(pdfFont, isHeader ? "bold" : "normal"); - const lines = cellLines[ci] ?? [""]; + // Render runs — no auto-bold for headers; user controls formatting. + const visualLines = runsToLines(cell?.runs ?? []); let ty = y + cellPad + lineH * 0.8; - for (const ln of lines) { - doc.text(ln, x + cellPad, ty); - ty += lineH; + for (const lineRuns of visualLines) { + ty = drawRunsLine(lineRuns, x + cellPad, ty, colW - cellPad * 2, "left"); } } y += rowH; @@ -574,24 +658,21 @@ export function CustomFormBuilder() { drawTablePdf(seg.rows); continue; } - const text = applyVariables(seg.text, ctx) || " "; - doc.setFont(pdfFont, seg.heading && seg.heading > 0 ? "bold" : "normal"); const indentPx = seg.indent || 0; const indentPt = indentPx * 0.75; // px → pt const segMaxW = maxW - indentPt; - const lines = doc.splitTextToSize(text, segMaxW); - for (const line of lines) { - if (y > pageH - margin) { - doc.addPage(); - y = margin; - } - let x = margin + indentPt; - if (seg.align === "center") x = pageW / 2; - else if (seg.align === "right") x = pageW - margin; - doc.text(line, x, y, { - align: seg.align === "center" ? "center" : seg.align === "right" ? "right" : "left", - }); + const startX = margin + indentPt; + const visualLines = runsToLines(seg.runs); + if (visualLines.length === 0 || (visualLines.length === 1 && visualLines[0].length === 0)) { y += lineH; + } else { + for (const lineRuns of visualLines) { + if (lineRuns.length === 0) { + y += lineH; + continue; + } + y = drawRunsLine(lineRuns, startX, y, segMaxW, seg.align); + } } y += seg.caption ? 2 : 4; }