import * as React from 'react'; import { Document, Page, View, Text, Image, StyleSheet, Font } from '@react-pdf/renderer'; import type { DatasheetVoltageTable, KeyValueItem } from '../scripts/pdf/model/types'; // Standard built-in fonts are used. Font.registerHyphenationCallback((word) => [word]); const C = { navy: '#001a4d', navyDeep: '#000d26', green: '#4da612', greenLight: '#e8f5d8', white: '#FFFFFF', offWhite: '#f8f9fa', gray100: '#f3f4f6', gray200: '#e5e7eb', gray300: '#d1d5db', gray400: '#9ca3af', gray600: '#4b5563', gray900: '#111827', }; const MARGIN = 56; const styles = StyleSheet.create({ page: { paddingHorizontal: MARGIN, paddingBottom: 80, paddingTop: 40, fontFamily: 'Helvetica', backgroundColor: C.white, color: C.gray900, }, hero: { paddingBottom: 20, marginBottom: 10 }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, logoText: { fontSize: 22, fontWeight: 700, color: C.navyDeep, letterSpacing: 2, textTransform: 'uppercase', }, docTitle: { fontSize: 8, fontWeight: 700, color: C.green, letterSpacing: 2, textTransform: 'uppercase', }, productRow: { flexDirection: 'row', alignItems: 'center', gap: 20 }, productInfoCol: { flex: 1, justifyContent: 'center' }, productImageCol: { flex: 1, height: 120, justifyContent: 'center', alignItems: 'center', borderRadius: 4, borderWidth: 1, borderColor: C.gray200, backgroundColor: C.white, overflow: 'hidden', }, productName: { fontSize: 24, fontWeight: 700, color: C.navyDeep, textTransform: 'uppercase', letterSpacing: -0.5, }, productMeta: { fontSize: 10, color: C.gray600, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1, marginBottom: 2, }, heroImage: { width: '100%', height: '100%', objectFit: 'contain' }, noImage: { fontSize: 8, color: C.gray400, textAlign: 'center' }, content: {}, section: { marginBottom: 20 }, sectionTitle: { fontSize: 8, fontWeight: 700, color: C.green, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1.5, }, sectionAccent: { width: 30, height: 2, backgroundColor: C.green, marginBottom: 8, borderRadius: 1, }, description: { fontSize: 10, lineHeight: 1.7, color: C.gray600 }, categories: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 }, categoryTag: { backgroundColor: C.offWhite, paddingHorizontal: 10, paddingVertical: 4, borderWidth: 0.5, borderColor: C.gray200, borderRadius: 3, }, categoryText: { fontSize: 7, color: C.gray600, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, }, footer: { position: 'absolute', bottom: 28, left: MARGIN, right: MARGIN, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingTop: 12, borderTopWidth: 2, borderTopColor: C.green, }, footerText: { fontSize: 7, color: C.gray400, fontWeight: 400, textTransform: 'uppercase', letterSpacing: 0.8, }, footerBrand: { fontSize: 9, fontWeight: 700, color: C.navyDeep, textTransform: 'uppercase', letterSpacing: 1.5, }, kvGrid: { width: '100%', borderWidth: 1, borderColor: C.gray200 }, kvRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: C.gray200 }, kvRowAlt: { backgroundColor: C.offWhite }, kvRowLast: { borderBottomWidth: 0 }, kvCell: { paddingVertical: 6, paddingHorizontal: 8 }, kvMidDivider: { borderRightWidth: 1, borderRightColor: C.gray200 }, kvLabelText: { fontSize: 8.5, fontWeight: 700, color: C.gray600 }, kvValueText: { fontSize: 9.5, color: C.gray900 }, tableWrap: { width: '100%', borderWidth: 1, borderColor: C.gray200, marginBottom: 14 }, tableHeader: { width: '100%', flexDirection: 'row', backgroundColor: C.white, borderBottomWidth: 1, borderBottomColor: C.gray200, }, tableHeaderCell: { paddingVertical: 5, paddingHorizontal: 4, fontSize: 6.6, fontWeight: 700, color: C.navyDeep, }, tableHeaderCellCfg: { paddingHorizontal: 6 }, tableHeaderCellDivider: { borderRightWidth: 1, borderRightColor: C.gray200 }, tableRow: { width: '100%', flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: C.gray200, }, tableRowAlt: { backgroundColor: C.offWhite }, tableCell: { paddingVertical: 4, paddingHorizontal: 4, fontSize: 6.6, color: C.gray900 }, tableCellCfg: { paddingHorizontal: 6 }, tableCellDivider: { borderRightWidth: 1, borderRightColor: C.gray200 }, }); interface ProductData { id: number; name: string; sku: string; categoriesLine?: string; descriptionText?: string; heroSrc?: string | null; productUrl?: string; shortDescriptionHtml?: string; descriptionHtml?: string; applicationHtml?: string; images?: string[]; featuredImage?: string | null; logoDataUrl?: string | null; categories?: Array<{ name: string }>; attributes?: Array<{ name: string; options: string[] }>; } export interface PDFDatasheetProps { product: ProductData; locale: 'en' | 'de'; logoDataUrl?: string | null; technicalItems?: KeyValueItem[]; voltageTables?: DatasheetVoltageTable[]; legendItems?: KeyValueItem[]; } const stripHtml = (html: string): string => html.replace(/<[^>]*>/g, ''); const getLabels = (locale: 'en' | 'de') => ({ en: { productDatasheet: 'Technical Datasheet', description: 'APPLICATION', specifications: 'TECHNICAL DATA', categories: 'CATEGORIES', sku: 'SKU', noImage: 'No image available', crossSection: 'Configurations', slug_cs: 'Cores & CS', abbreviations: 'ABBREVIATIONS', }, de: { productDatasheet: 'Technisches Datenblatt', description: 'ANWENDUNG', specifications: 'TECHNISCHE DATEN', categories: 'KATEGORIEN', sku: 'ARTIKELNUMMER', noImage: 'Kein Bild verfügbar', crossSection: 'Konfigurationen', slug_cs: 'Adern & QS', abbreviations: 'ABKÜRZUNGEN', }, })[locale]; function clamp(n: number, min: number, max: number) { return Math.max(min, Math.min(max, n)); } function normTextForMeasure(v: unknown) { return String(v ?? '') .replace(/\s+/g, ' ') .trim(); } function textLen(v: unknown) { return normTextForMeasure(v).length; } function distributeWithMinMax( weights: number[], total: number, minEach: number, maxEach: number, ): number[] { const n = weights.length; if (!n) return []; const mins = Array.from({ length: n }, () => minEach); const maxs = Array.from({ length: n }, () => maxEach); const minSum = mins.reduce((a, b) => a + b, 0); if (minSum > total) return mins.map((m) => m * (total / minSum)); const result = mins.slice(); let remaining = total - minSum; let remainingIdx = Array.from({ length: n }, (_, i) => i); while (remaining > 1e-9 && remainingIdx.length) { const wSum = remainingIdx.reduce((acc, i) => acc + Math.max(0, weights[i] || 0), 0); if (wSum <= 1e-9) { const even = remaining / remainingIdx.length; for (const i of remainingIdx) result[i] += even; remaining = 0; break; } const nextIdx: number[] = []; for (const i of remainingIdx) { const w = Math.max(0, weights[i] || 0); const add = (w / wSum) * remaining; const capped = Math.min(result[i] + add, maxs[i]); const used = capped - result[i]; result[i] = capped; remaining -= used; if (result[i] + 1e-9 < maxs[i]) nextIdx.push(i); } remainingIdx = nextIdx; } const sum = result.reduce((a, b) => a + b, 0); const drift = total - sum; if (Math.abs(drift) > 1e-9) result[result.length - 1] += drift; return result; } function KeyValueGrid({ items }: { items: KeyValueItem[] }) { const filtered = (items || []).filter((i) => i.label && i.value); if (!filtered.length) return null; const rows: Array<[KeyValueItem, KeyValueItem | null]> = []; for (let i = 0; i < filtered.length; i += 2) rows.push([filtered[i], filtered[i + 1] || null]); return ( {rows.map(([left, right], rowIndex) => { const isLast = rowIndex === rows.length - 1; const leftValue = left.unit ? `${left.value} ${left.unit}` : left.value; const rightValue = right ? (right.unit ? `${right.value} ${right.unit}` : right.value) : ''; return ( {left.label} {leftValue} {right?.label || ''} {rightValue} ); })} ); } function DenseTable({ table, firstColLabel, }: { table: Pick; firstColLabel: string; }) { const cols = table.columns; const rows = table.rows; const headerText = (label: string) => String(label || '') .replace(/\s+/g, '\u00A0') .trim(); const cfgMin = 0.14, cfgMax = 0.23; const cfgContentLen = Math.max( textLen(firstColLabel), ...rows.map((r) => textLen(r.configuration)), 8, ); const dataContentLens = cols.map((c, ci) => { const headerL = textLen(c.label); let cellMax = 0; for (const r of rows) cellMax = Math.max(cellMax, textLen(r.cells[ci])); return Math.max(headerL * 1.15, cellMax, 3); }); const cfgWeight = cfgContentLen * 1.05; const dataWeights = dataContentLens.map((l) => l); const dataWeightSum = dataWeights.reduce((a, b) => a + b, 0); const rawCfgPct = dataWeightSum > 0 ? cfgWeight / (cfgWeight + dataWeightSum) : 0.28; let cfgPct = clamp(rawCfgPct, cfgMin, cfgMax); const minDataPct = cols.length >= 14 ? 0.045 : cols.length >= 12 ? 0.05 : cols.length >= 10 ? 0.055 : 0.06; const cfgPctMaxForMinData = 1 - cols.length * minDataPct; if (Number.isFinite(cfgPctMaxForMinData)) cfgPct = Math.min(cfgPct, cfgPctMaxForMinData); cfgPct = clamp(cfgPct, cfgMin, cfgMax); const dataTotal = Math.max(0, 1 - cfgPct); const maxDataPct = Math.min(0.24, Math.max(minDataPct * 2.8, dataTotal * 0.55)); const dataPcts = distributeWithMinMax(dataWeights, dataTotal, minDataPct, maxDataPct); const cfgW = `${(cfgPct * 100).toFixed(4)}%`; const dataWs = dataPcts.map((p, idx) => { if (idx === dataPcts.length - 1) { const used = dataPcts.slice(0, -1).reduce((a, b) => a + b, 0); const remainder = Math.max(0, dataTotal - used); return `${(remainder * 100).toFixed(4)}%`; } return `${(p * 100).toFixed(4)}%`; }); const headerFontSize = cols.length >= 14 ? 5.7 : cols.length >= 12 ? 5.9 : cols.length >= 10 ? 6.2 : 6.6; return ( {headerText(firstColLabel)} {cols.map((c, idx) => { const isLast = idx === cols.length - 1; return ( {headerText(c.label)} ); })} {rows.map((r, ri) => ( {r.configuration} {r.cells.map((cell, ci) => ( {cell} ))} ))} ); } export const PDFDatasheet: React.FC = ({ product, locale, technicalItems = [], voltageTables = [], legendItems = [], }) => { const labels = getLabels(locale); const description = stripHtml( product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml || product.descriptionText || '', ); return ( {product.logoDataUrl || (product as any).logoDataUrl ? ( ) : ( KLZ )} {labels.productDatasheet} {product.categoriesLine || (product.categories || []).map((c) => c.name).join(' • ')} {product.name} {product.featuredImage ? ( ) : ( {labels.noImage} )} {description && ( {labels.description} {description} )} {technicalItems.length > 0 && ( {labels.specifications} )} {voltageTables.map((table, idx) => ( {`${labels.crossSection} — ${table.voltageLabel}`} ))} {legendItems.length > 0 && ( {labels.abbreviations} )} {!technicalItems.length && !voltageTables.length && product.attributes && product.attributes.length > 0 && ( {labels.specifications} {product.attributes.map((attr, index) => ( {attr.name} {attr.options.join(', ')} ))} )} {product.logoDataUrl || (product as any).logoDataUrl ? ( ) : ( KLZ CABLES )} {new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', { year: 'numeric', month: 'long', day: 'numeric', })} ); };