diff --git a/public/datasheets/h1z2z2-k-de.pdf b/public/datasheets/h1z2z2-k-de.pdf index b6868f29..2e29cdf1 100644 Binary files a/public/datasheets/h1z2z2-k-de.pdf and b/public/datasheets/h1z2z2-k-de.pdf differ diff --git a/public/datasheets/h1z2z2-k-en.pdf b/public/datasheets/h1z2z2-k-en.pdf index 0f577faa..07a02c18 100644 Binary files a/public/datasheets/h1z2z2-k-en.pdf and b/public/datasheets/h1z2z2-k-en.pdf differ diff --git a/public/datasheets/n2x2y-2-de.pdf b/public/datasheets/n2x2y-2-de.pdf index ab23356b..eff5688d 100644 Binary files a/public/datasheets/n2x2y-2-de.pdf and b/public/datasheets/n2x2y-2-de.pdf differ diff --git a/public/datasheets/n2x2y-en.pdf b/public/datasheets/n2x2y-en.pdf index cd7ee0e7..3ad96bc5 100644 Binary files a/public/datasheets/n2x2y-en.pdf and b/public/datasheets/n2x2y-en.pdf differ diff --git a/public/datasheets/n2xfk2y-de.pdf b/public/datasheets/n2xfk2y-de.pdf index 1782dbdf..d02e2676 100644 Binary files a/public/datasheets/n2xfk2y-de.pdf and b/public/datasheets/n2xfk2y-de.pdf differ diff --git a/public/datasheets/n2xfk2y-en.pdf b/public/datasheets/n2xfk2y-en.pdf index e7f249e1..b2f42a06 100644 Binary files a/public/datasheets/n2xfk2y-en.pdf and b/public/datasheets/n2xfk2y-en.pdf differ diff --git a/public/datasheets/n2xfkld2y-de.pdf b/public/datasheets/n2xfkld2y-de.pdf index 24d0d26b..4ded6191 100644 Binary files a/public/datasheets/n2xfkld2y-de.pdf and b/public/datasheets/n2xfkld2y-de.pdf differ diff --git a/public/datasheets/n2xfkld2y-en.pdf b/public/datasheets/n2xfkld2y-en.pdf index cad85c96..5e4f2562 100644 Binary files a/public/datasheets/n2xfkld2y-en.pdf and b/public/datasheets/n2xfkld2y-en.pdf differ diff --git a/public/datasheets/n2xs2y-2-de.pdf b/public/datasheets/n2xs2y-2-de.pdf index c3028e4f..a87c9401 100644 Binary files a/public/datasheets/n2xs2y-2-de.pdf and b/public/datasheets/n2xs2y-2-de.pdf differ diff --git a/public/datasheets/n2xs2y-en.pdf b/public/datasheets/n2xs2y-en.pdf index 1056542d..356d6583 100644 Binary files a/public/datasheets/n2xs2y-en.pdf and b/public/datasheets/n2xs2y-en.pdf differ diff --git a/public/datasheets/n2xsf2y-2-de.pdf b/public/datasheets/n2xsf2y-2-de.pdf index d0806902..ebd90387 100644 Binary files a/public/datasheets/n2xsf2y-2-de.pdf and b/public/datasheets/n2xsf2y-2-de.pdf differ diff --git a/public/datasheets/n2xsf2y-en.pdf b/public/datasheets/n2xsf2y-en.pdf index 645d2471..7120f57b 100644 Binary files a/public/datasheets/n2xsf2y-en.pdf and b/public/datasheets/n2xsf2y-en.pdf differ diff --git a/public/datasheets/n2xsfl2y-2-de.pdf b/public/datasheets/n2xsfl2y-2-de.pdf index 32becbb3..53d0aff9 100644 Binary files a/public/datasheets/n2xsfl2y-2-de.pdf and b/public/datasheets/n2xsfl2y-2-de.pdf differ diff --git a/public/datasheets/n2xsfl2y-3-en.pdf b/public/datasheets/n2xsfl2y-3-en.pdf index 50dba779..d6d325b0 100644 Binary files a/public/datasheets/n2xsfl2y-3-en.pdf and b/public/datasheets/n2xsfl2y-3-en.pdf differ diff --git a/public/datasheets/n2xsfl2y-de.pdf b/public/datasheets/n2xsfl2y-de.pdf index df049932..b48c7c76 100644 Binary files a/public/datasheets/n2xsfl2y-de.pdf and b/public/datasheets/n2xsfl2y-de.pdf differ diff --git a/public/datasheets/n2xsfl2y-en.pdf b/public/datasheets/n2xsfl2y-en.pdf index 7ed565c3..0fc8b98a 100644 Binary files a/public/datasheets/n2xsfl2y-en.pdf and b/public/datasheets/n2xsfl2y-en.pdf differ diff --git a/public/datasheets/n2xsy-2-de.pdf b/public/datasheets/n2xsy-2-de.pdf index 14af4749..f478547c 100644 Binary files a/public/datasheets/n2xsy-2-de.pdf and b/public/datasheets/n2xsy-2-de.pdf differ diff --git a/public/datasheets/n2xsy-en.pdf b/public/datasheets/n2xsy-en.pdf index e5c46fe8..db13bb4d 100644 Binary files a/public/datasheets/n2xsy-en.pdf and b/public/datasheets/n2xsy-en.pdf differ diff --git a/public/datasheets/n2xy-2-de.pdf b/public/datasheets/n2xy-2-de.pdf index 3f00da94..cdaa1701 100644 Binary files a/public/datasheets/n2xy-2-de.pdf and b/public/datasheets/n2xy-2-de.pdf differ diff --git a/public/datasheets/n2xy-en.pdf b/public/datasheets/n2xy-en.pdf index 6ee338c9..3ba6f8cd 100644 Binary files a/public/datasheets/n2xy-en.pdf and b/public/datasheets/n2xy-en.pdf differ diff --git a/public/datasheets/na2x2y-2-de.pdf b/public/datasheets/na2x2y-2-de.pdf index 7763ccfc..98bda1d1 100644 Binary files a/public/datasheets/na2x2y-2-de.pdf and b/public/datasheets/na2x2y-2-de.pdf differ diff --git a/public/datasheets/na2x2y-en.pdf b/public/datasheets/na2x2y-en.pdf index b1755fe9..2fb60b93 100644 Binary files a/public/datasheets/na2x2y-en.pdf and b/public/datasheets/na2x2y-en.pdf differ diff --git a/public/datasheets/na2xfk2y-de.pdf b/public/datasheets/na2xfk2y-de.pdf index 4096d6d4..363d3db5 100644 Binary files a/public/datasheets/na2xfk2y-de.pdf and b/public/datasheets/na2xfk2y-de.pdf differ diff --git a/public/datasheets/na2xfk2y-en.pdf b/public/datasheets/na2xfk2y-en.pdf index 26eeb9be..732bcb88 100644 Binary files a/public/datasheets/na2xfk2y-en.pdf and b/public/datasheets/na2xfk2y-en.pdf differ diff --git a/public/datasheets/na2xfkld2y-de.pdf b/public/datasheets/na2xfkld2y-de.pdf index 62cb89c7..b6d4c17c 100644 Binary files a/public/datasheets/na2xfkld2y-de.pdf and b/public/datasheets/na2xfkld2y-de.pdf differ diff --git a/public/datasheets/na2xfkld2y-en.pdf b/public/datasheets/na2xfkld2y-en.pdf index 3ac1dbbc..120efd69 100644 Binary files a/public/datasheets/na2xfkld2y-en.pdf and b/public/datasheets/na2xfkld2y-en.pdf differ diff --git a/public/datasheets/na2xs2y-2-de.pdf b/public/datasheets/na2xs2y-2-de.pdf index 9f6fb6fe..138c3bb2 100644 Binary files a/public/datasheets/na2xs2y-2-de.pdf and b/public/datasheets/na2xs2y-2-de.pdf differ diff --git a/public/datasheets/na2xs2y-en.pdf b/public/datasheets/na2xs2y-en.pdf index 6d5c2626..8453e3c8 100644 Binary files a/public/datasheets/na2xs2y-en.pdf and b/public/datasheets/na2xs2y-en.pdf differ diff --git a/public/datasheets/na2xsf2y-2-de.pdf b/public/datasheets/na2xsf2y-2-de.pdf index 9379dfb5..e26debdf 100644 Binary files a/public/datasheets/na2xsf2y-2-de.pdf and b/public/datasheets/na2xsf2y-2-de.pdf differ diff --git a/public/datasheets/na2xsf2y-en.pdf b/public/datasheets/na2xsf2y-en.pdf index f9d3b1a5..73a09084 100644 Binary files a/public/datasheets/na2xsf2y-en.pdf and b/public/datasheets/na2xsf2y-en.pdf differ diff --git a/public/datasheets/na2xsfl2y-2-de.pdf b/public/datasheets/na2xsfl2y-2-de.pdf index b263f60a..5d271107 100644 Binary files a/public/datasheets/na2xsfl2y-2-de.pdf and b/public/datasheets/na2xsfl2y-2-de.pdf differ diff --git a/public/datasheets/na2xsfl2y-3-en.pdf b/public/datasheets/na2xsfl2y-3-en.pdf index 86635857..4e74daf3 100644 Binary files a/public/datasheets/na2xsfl2y-3-en.pdf and b/public/datasheets/na2xsfl2y-3-en.pdf differ diff --git a/public/datasheets/na2xsfl2y-de.pdf b/public/datasheets/na2xsfl2y-de.pdf index d6b68837..d9a9d8de 100644 Binary files a/public/datasheets/na2xsfl2y-de.pdf and b/public/datasheets/na2xsfl2y-de.pdf differ diff --git a/public/datasheets/na2xsfl2y-en.pdf b/public/datasheets/na2xsfl2y-en.pdf index 14435997..432e8013 100644 Binary files a/public/datasheets/na2xsfl2y-en.pdf and b/public/datasheets/na2xsfl2y-en.pdf differ diff --git a/public/datasheets/na2xsy-2-de.pdf b/public/datasheets/na2xsy-2-de.pdf index 5a4bea36..b72cbea9 100644 Binary files a/public/datasheets/na2xsy-2-de.pdf and b/public/datasheets/na2xsy-2-de.pdf differ diff --git a/public/datasheets/na2xsy-en.pdf b/public/datasheets/na2xsy-en.pdf index 8b8e0104..68688a73 100644 Binary files a/public/datasheets/na2xsy-en.pdf and b/public/datasheets/na2xsy-en.pdf differ diff --git a/public/datasheets/na2xy-2-de.pdf b/public/datasheets/na2xy-2-de.pdf index d1eae6ad..4d7476e0 100644 Binary files a/public/datasheets/na2xy-2-de.pdf and b/public/datasheets/na2xy-2-de.pdf differ diff --git a/public/datasheets/na2xy-en.pdf b/public/datasheets/na2xy-en.pdf index 0358ee2d..ea436402 100644 Binary files a/public/datasheets/na2xy-en.pdf and b/public/datasheets/na2xy-en.pdf differ diff --git a/public/datasheets/nay2y-2-de.pdf b/public/datasheets/nay2y-2-de.pdf index d54a1668..c259f073 100644 Binary files a/public/datasheets/nay2y-2-de.pdf and b/public/datasheets/nay2y-2-de.pdf differ diff --git a/public/datasheets/nay2y-en.pdf b/public/datasheets/nay2y-en.pdf index b49a9c47..e481a258 100644 Binary files a/public/datasheets/nay2y-en.pdf and b/public/datasheets/nay2y-en.pdf differ diff --git a/public/datasheets/naycwy-2-de.pdf b/public/datasheets/naycwy-2-de.pdf index fa442004..4229c824 100644 Binary files a/public/datasheets/naycwy-2-de.pdf and b/public/datasheets/naycwy-2-de.pdf differ diff --git a/public/datasheets/naycwy-en.pdf b/public/datasheets/naycwy-en.pdf index 6b718241..55402ad0 100644 Binary files a/public/datasheets/naycwy-en.pdf and b/public/datasheets/naycwy-en.pdf differ diff --git a/public/datasheets/nayy-2-de.pdf b/public/datasheets/nayy-2-de.pdf index cd09e220..4c192844 100644 Binary files a/public/datasheets/nayy-2-de.pdf and b/public/datasheets/nayy-2-de.pdf differ diff --git a/public/datasheets/nayy-en.pdf b/public/datasheets/nayy-en.pdf index 8ae067ec..c82dcd97 100644 Binary files a/public/datasheets/nayy-en.pdf and b/public/datasheets/nayy-en.pdf differ diff --git a/public/datasheets/ny2y-2-de.pdf b/public/datasheets/ny2y-2-de.pdf index 90375ccc..c5a98e4e 100644 Binary files a/public/datasheets/ny2y-2-de.pdf and b/public/datasheets/ny2y-2-de.pdf differ diff --git a/public/datasheets/ny2y-en.pdf b/public/datasheets/ny2y-en.pdf index e40934af..eb17a660 100644 Binary files a/public/datasheets/ny2y-en.pdf and b/public/datasheets/ny2y-en.pdf differ diff --git a/public/datasheets/nycwy-2-de.pdf b/public/datasheets/nycwy-2-de.pdf index 098c4837..dca418da 100644 Binary files a/public/datasheets/nycwy-2-de.pdf and b/public/datasheets/nycwy-2-de.pdf differ diff --git a/public/datasheets/nycwy-en.pdf b/public/datasheets/nycwy-en.pdf index ce0419f3..06913f37 100644 Binary files a/public/datasheets/nycwy-en.pdf and b/public/datasheets/nycwy-en.pdf differ diff --git a/public/datasheets/nyy-2-de.pdf b/public/datasheets/nyy-2-de.pdf index e153ec4b..a9fb66d1 100644 Binary files a/public/datasheets/nyy-2-de.pdf and b/public/datasheets/nyy-2-de.pdf differ diff --git a/public/datasheets/nyy-en.pdf b/public/datasheets/nyy-en.pdf index 9f861cfc..1c7b8fa4 100644 Binary files a/public/datasheets/nyy-en.pdf and b/public/datasheets/nyy-en.pdf differ diff --git a/scripts/generate-pdf-datasheets.ts b/scripts/generate-pdf-datasheets.ts index 2923a27a..da0f12e4 100644 --- a/scripts/generate-pdf-datasheets.ts +++ b/scripts/generate-pdf-datasheets.ts @@ -248,9 +248,30 @@ function wrapText(text: string, font: PDFFont, fontSize: number, maxWidth: numbe const lines: string[] = []; let currentLine = ''; - for (const word of words) { + const isOrphanWord = (w: string) => { + // Avoid ugly single short words on their own line in DE/EN (e.g. “im”, “in”, “to”). + // This is a typography/UX improvement for datasheets. + const s = w.trim(); + return s.length > 0 && s.length <= 2; + }; + + for (let i = 0; i < words.length; i++) { + const word = words[i]; + const next = i + 1 < words.length ? words[i + 1] : ''; const testLine = currentLine ? `${currentLine} ${word}` : word; + if (font.widthOfTextAtSize(testLine, fontSize) <= maxWidth) { + // Orphan control: if adding the *next* word would overflow, don't end the line with a tiny orphan. + // Example: "... mechanischen im" + "Belastungen" should become "... mechanischen" / "im Belastungen ...". + if (currentLine && next && isOrphanWord(word)) { + const testWithNext = `${testLine} ${next}`; + if (font.widthOfTextAtSize(testWithNext, fontSize) > maxWidth) { + lines.push(currentLine); + currentLine = word; + continue; + } + } + currentLine = testLine; } else { if (currentLine) lines.push(currentLine); @@ -685,29 +706,18 @@ function drawHeader(ctx: SectionDrawContext, yStart: number): number { const metaRightEdge = width - margin - rightReserved; const metaTitle = labels.datasheet; const metaTitleSize = 9; - const metaSkuSize = 8; - const skuText = product.sku ? `${labels.sku}: ${stripHtml(product.sku)}` : ''; const mtW = fonts.bold.widthOfTextAtSize(metaTitle, metaTitleSize); + // With SKU removed, vertically center the title within the header block. + const metaY = dividerY + Math.round(headerH / 2 - metaTitleSize / 2); page.drawText(metaTitle, { x: metaRightEdge - mtW, - y: dividerY + 38, + y: metaY, size: metaTitleSize, font: fonts.bold, color: colors.navy, }); - if (skuText) { - const skuW = fonts.regular.widthOfTextAtSize(skuText, metaSkuSize); - page.drawText(skuText, { - x: metaRightEdge - skuW, - y: dividerY + 24, - size: metaSkuSize, - font: fonts.regular, - color: colors.mediumGray, - }); - } - if (qrImage) { const qrX = width - margin - qrSize; const qrY = dividerY + Math.round((headerH - qrSize) / 2); @@ -745,6 +755,7 @@ function drawCrossSectionChipsRow(args: { title: string; configRows: string[]; locale: 'en' | 'de'; + maxLinesCap?: number; getPage: () => PDFPage; page: PDFPage; y: number; @@ -814,7 +825,10 @@ function drawCrossSectionChipsRow(args: { const chipPadTop = 5; const startY = y - chipH; // baseline for first chip row - const maxLines = Math.max(1, Math.floor((startY - contentMinY + lineGap) / (chipH + lineGap))); + const maxLinesAvailable = Math.max(1, Math.floor((startY - contentMinY + lineGap) / (chipH + lineGap))); + // UX/Content priority: don't let cross-section tags consume the whole sheet. + // When technical data is dense, we cap this to keep specs visible. + const maxLines = Math.min(args.maxLinesCap ?? 2, maxLinesAvailable); const chipWidth = (text: string) => font.widthOfTextAtSize(text, chipFontSize) + padX * 2; @@ -1045,6 +1059,137 @@ function normalizeValue(value: string): string { return stripHtml(value).replace(/\s+/g, ' ').trim(); } +function summarizeOptions(options: string[] | undefined, maxItems: number = 3): string { + const vals = (options || []).map(normalizeValue).filter(Boolean); + if (vals.length === 0) return ''; + const uniq = Array.from(new Set(vals)); + if (uniq.length === 1) return uniq[0]; + if (uniq.length <= maxItems) return uniq.join(' / '); + return `${uniq.slice(0, maxItems).join(' / ')} (+${uniq.length - maxItems})`; +} + +function parseNumericOption(value: string): number | null { + const v = normalizeValue(value).replace(/,/g, '.'); + // First numeric token (works for "12.3", "12.3 mm", "-35", "26.5 kg/km"). + const m = v.match(/-?\d+(?:\.\d+)?/); + if (!m) return null; + const n = Number(m[0]); + return Number.isFinite(n) ? n : null; +} + +function formatNumber(n: number): string { + const s = Number.isInteger(n) ? String(n) : String(n); + return s.replace(/\.0+$/, ''); +} + +function summarizeNumericRange(options: string[] | undefined): { ok: boolean; text: string } { + const vals = (options || []).map(parseNumericOption).filter((n): n is number => n !== null); + if (vals.length < 3) return { ok: false, text: '' }; + const uniq = Array.from(new Set(vals)); + if (uniq.length < 2) return { ok: false, text: '' }; + uniq.sort((a, b) => a - b); + const min = uniq[0]; + const max = uniq[uniq.length - 1]; + return { ok: true, text: `${formatNumber(min)}–${formatNumber(max)} (n=${uniq.length})` }; +} + +function summarizeSmartOptions(label: string, options: string[] | undefined): string { + // Prefer numeric ranges when an attribute has many numeric-ish entries (typical for row-specific data). + const range = summarizeNumericRange(options); + if (range.ok) return range.text; + return summarizeOptions(options, 3); +} + +function looksNumeric(value: string): boolean { + const v = normalizeValue(value).replace(/,/g, '.'); + return /^-?\d+(?:\.\d+)?$/.test(v); +} + +function formatMaybeWithUnit(value: string, unit: string): string { + const v = normalizeValue(value); + if (!v) return ''; + return looksNumeric(v) ? `${v} ${unit}` : v; +} + +function drawRowPreviewTable(args: { + title: string; + rows: Array<{ config: string; col1: string; col2: string }>; + headers: { config: string; col1: string; col2: string }; + getPage: () => PDFPage; + page: PDFPage; + y: number; + margin: number; + contentWidth: number; + contentMinY: number; + font: PDFFont; + fontBold: PDFFont; + navy: ReturnType; + darkGray: ReturnType; + lightGray: ReturnType; + almostWhite: ReturnType; +}): number { + let { title, rows, headers, getPage, page, y, margin, contentWidth, contentMinY, font, fontBold, navy, darkGray, lightGray, almostWhite } = args; + + const titleH = 16; + const headerH = 16; + const rowH = 13; + const padAfter = 18; + + // One-page rule: require at least 2 data rows. + const minNeeded = titleH + headerH + rowH * 2 + padAfter; + if (y - minNeeded < contentMinY) return contentMinY - 1; + + page = getPage(); + page.drawText(title, { x: margin, y, size: 10, font: fontBold, color: navy }); + y -= titleH; + + // How many rows fit? + const availableForRows = y - contentMinY - padAfter - headerH; + const maxRows = Math.max(2, Math.floor(availableForRows / rowH)); + const shown = rows.slice(0, Math.max(0, maxRows)); + + const hasCol2 = shown.some(r => Boolean(r.col2)); + + // Widths: favor configuration readability. + const wCfg = 0.46; + const w1 = hasCol2 ? 0.27 : 0.54; + const w2 = hasCol2 ? 0.27 : 0; + + const x0 = margin; + const x1 = margin + contentWidth * wCfg; + const x2 = margin + contentWidth * (wCfg + w1); + + const drawHeader = () => { + page = getPage(); + page.drawRectangle({ x: margin, y: y - headerH, width: contentWidth, height: headerH, color: lightGray }); + page.drawText(headers.config, { x: x0 + 6, y: y - 11, size: 8, font: fontBold, color: navy, maxWidth: contentWidth * wCfg - 12 }); + page.drawText(headers.col1, { x: x1 + 6, y: y - 11, size: 8, font: fontBold, color: navy, maxWidth: contentWidth * w1 - 12 }); + if (hasCol2) { + page.drawText(headers.col2, { x: x2 + 6, y: y - 11, size: 8, font: fontBold, color: navy, maxWidth: contentWidth * w2 - 12 }); + } + y -= headerH; + }; + + drawHeader(); + + for (let i = 0; i < shown.length; i++) { + if (y - rowH < contentMinY) return contentMinY - 1; + page = getPage(); + if (i % 2 === 0) { + page.drawRectangle({ x: margin, y: y - rowH, width: contentWidth, height: rowH, color: almostWhite }); + } + page.drawText(shown[i].config, { x: x0 + 6, y: y - 10, size: 8, font, color: darkGray, maxWidth: contentWidth * wCfg - 12 }); + page.drawText(shown[i].col1, { x: x1 + 6, y: y - 10, size: 8, font, color: darkGray, maxWidth: contentWidth * w1 - 12 }); + if (hasCol2) { + page.drawText(shown[i].col2, { x: x2 + 6, y: y - 10, size: 8, font, color: darkGray, maxWidth: contentWidth * w2 - 12 }); + } + y -= rowH; + } + + y -= padAfter; + return y; +} + function splitConfig(config: string): { crossSection: string; voltage: string } { const raw = normalizeValue(config); const parts = raw.split(/\s*-\s*/); @@ -1110,11 +1255,11 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise= 18; + let heroH = hasLotsOfTech ? 120 : 160; const afterHeroGap = DS.space.xl; if (!hasSpace(heroH + afterHeroGap)) { // Shrink to remaining space (but keep it usable). - heroH = Math.max(120, Math.floor(y - contentMinY - afterHeroGap)); + heroH = Math.max(96, Math.floor(y - contentMinY - afterHeroGap)); } const heroBoxX = margin; @@ -1348,11 +1495,59 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise = [ + { re: /standard|norm|vde|iec/i, fallbackLabel: locale === 'de' ? 'Norm' : 'Standard' }, + { re: /rated\s*voltage|voltage\s*rating|nennspannung/i, fallbackLabel: locale === 'de' ? 'Nennspannung' : 'Rated voltage' }, + { re: /test\s*voltage|pr\u00fcfspannung/i, fallbackLabel: locale === 'de' ? 'Pr\u00fcfspannung' : 'Test voltage' }, + { re: /temperature\s*range|operating\s*temperature|betriebstemperatur/i, fallbackLabel: locale === 'de' ? 'Temperaturbereich' : 'Temperature range' }, + { re: /bending\s*radius|biegeradius/i, fallbackLabel: locale === 'de' ? 'Biegeradius' : 'Bending radius' }, + { re: /cpr\s*class/i, fallbackLabel: locale === 'de' ? 'CPR-Klasse' : 'CPR class' }, + ]; + + const picked = new Set(); + const techItemsPreferred = preferredTechAttrs + .map(({ re, fallbackLabel }) => { + const a = findAttr(product, re); + if (!a) return null; + const label = normalizeValue(a.name) || fallbackLabel; + const value = summarizeSmartOptions(label, a.options); + if (!label || !value) return null; + picked.add(label.toLowerCase()); + return { label, value }; + }) + .filter(Boolean) as Array<{ label: string; value: string }>; + + const isConfigLikeAttr = (name: string) => + /configuration|konfiguration|aufbau|bezeichnung|number of cores and cross-section|querschnitt|cross.?section|mm²|mm2/i.test(name); + const isClearlyMetaAttr = (name: string) => /\bsku\b|artikelnummer|\bid\b|product\s*id/i.test(name); + + // Provide additional technical attributes as compact summaries. + // - numeric-heavy arrays become ranges (min–max with count) + // - non-numeric arrays become short lists + // This is what fills the “missing important technical data” without breaking 1-page. + const techItemsMore = (product.attributes || []) + .filter(a => (a.options?.length || 0) > 1) + .filter(a => !isConfigLikeAttr(a.name)) + .filter(a => !isClearlyMetaAttr(a.name)) + .map(a => { + const label = normalizeValue(a.name); + if (!label) return null; + if (picked.has(label.toLowerCase())) return null; + const value = summarizeSmartOptions(label, a.options); + if (!value) return null; + return { label, value }; + }) + .filter(Boolean) as Array<{ label: string; value: string }>; + const constantAttrs = (product.attributes || []).filter(a => a.options.length === 1); - const constantItemsAll = constantAttrs + const techItemsFill = constantAttrs .map(a => ({ label: normalizeValue(a.name), value: normalizeValue(a.options[0]) })) .filter(i => i.label && i.value) - .slice(0, 12); + .filter(i => !picked.has(i.label.toLowerCase())); + + const constantItemsAll = [...techItemsPreferred, ...techItemsMore, ...techItemsFill].slice(0, 20); // Intentionally do NOT include SKU/categories here (they are already shown in the product header). @@ -1368,7 +1563,15 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise= 1; n--) { if (canFitTechWith(n)) { @@ -1464,21 +1667,16 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise = [ - { key: 'outerDiameter', label: locale === 'de' ? 'Außen-Ø' : 'Outer Ø', re: /outer\s*diameter|außen\s*durchmesser|außen-?ø/i }, - { key: 'weight', label: locale === 'de' ? 'Gewicht' : 'Weight', re: /\bweight\b|gewicht/i }, - { key: 'maxResistance', label: locale === 'de' ? 'Max. Leiterwiderstand' : 'Max. conductor resistance', re: /maximum\s+resistance\s+of\s+conductor|max\.?\s*resistance|leiterwiderstand/i }, - { key: 'current', label: locale === 'de' ? 'Strombelastbarkeit' : 'Current rating', re: /current\s*(rating|carrying)|ampacity|strombelastbarkeit/i }, - ]; - - // NOTE: One-page requirement: cross sections render as a dense list only. - // Row-specific values are intentionally omitted to keep the sheet compact. - const columns: Array<{ label: string; get: (rowIndex: number) => string }> = []; + // Pull the two most important row-specific columns and show a small excerpt table. + const rowOuter = findRowAttr(/outer\s*diameter|außen\s*durchmesser|außen-?ø/i); + const rowWeight = findRowAttr(/\bweight\b|gewicht/i); const yAfterCross = drawCrossSectionChipsRow({ title: labels.crossSection, configRows, locale, + // keep chips as a fallback, but prefer the dense list section below + maxLinesCap: 2, getPage: () => page, page, y, @@ -1512,6 +1710,39 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise ({ + config: normalizeValue(cfg), + col1: formatMaybeWithUnit(getAttrCellValue(rowOuter ?? undefined, i, rowCount), 'mm'), + col2: formatMaybeWithUnit(getAttrCellValue(rowWeight ?? undefined, i, rowCount), 'kg/km'), + })); + + const previewTitle = locale === 'de' ? 'Konfigurationswerte (Auszug)' : 'Configuration values (excerpt)'; + const yAfterPreview = drawRowPreviewTable({ + title: previewTitle, + rows: previewRows, + headers: { + config: locale === 'de' ? 'Konfiguration' : 'Configuration', + col1: locale === 'de' ? 'Außen-Ø' : 'Outer Ø', + col2: locale === 'de' ? 'Gewicht' : 'Weight', + }, + getPage: () => page, + page, + y, + margin, + contentWidth, + contentMinY, + font, + fontBold, + navy, + darkGray, + lightGray, + almostWhite, + }); + if (yAfterPreview >= contentMinY) y = yAfterPreview; + } } else { // If there is no cross-section data, do not render the section at all. }