fix: restore complex table components in lib/pdf-datasheet.tsx
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 2m16s
Build & Deploy / 🏗️ Build (push) Successful in 3m41s
Build & Deploy / 🚀 Deploy (push) Successful in 1m14s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 4m25s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 2m16s
Build & Deploy / 🏗️ Build (push) Successful in 3m41s
Build & Deploy / 🚀 Deploy (push) Successful in 1m14s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 4m25s
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
@@ -1,16 +1,10 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Document, Page, View, Text, Image, StyleSheet, Font } from '@react-pdf/renderer';
|
import { Document, Page, View, Text, Image, StyleSheet, Font } from '@react-pdf/renderer';
|
||||||
|
import type { DatasheetVoltageTable, KeyValueItem } from '../scripts/pdf/model/types';
|
||||||
|
|
||||||
// Register fonts (using system fonts for now, can be customized)
|
// Standard built-in fonts are used.
|
||||||
Font.register({
|
Font.registerHyphenationCallback((word) => [word]);
|
||||||
family: 'Helvetica',
|
|
||||||
fonts: [
|
|
||||||
{ src: '/fonts/Helvetica.ttf', fontWeight: 400 },
|
|
||||||
{ src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Brand Tokens (matching brochure) ────────────────────────────────────────
|
|
||||||
const C = {
|
const C = {
|
||||||
navy: '#001a4d',
|
navy: '#001a4d',
|
||||||
navyDeep: '#000d26',
|
navyDeep: '#000d26',
|
||||||
@@ -30,32 +24,20 @@ const MARGIN = 56;
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
page: {
|
page: {
|
||||||
color: C.gray900,
|
|
||||||
lineHeight: 1.5,
|
|
||||||
backgroundColor: C.white,
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 80,
|
|
||||||
fontFamily: 'Helvetica',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Hero-style header
|
|
||||||
hero: {
|
|
||||||
backgroundColor: C.white,
|
|
||||||
paddingTop: 24,
|
|
||||||
paddingBottom: 0,
|
|
||||||
paddingHorizontal: MARGIN,
|
paddingHorizontal: MARGIN,
|
||||||
marginBottom: 20,
|
paddingBottom: 80,
|
||||||
position: 'relative',
|
paddingTop: 40,
|
||||||
borderBottomWidth: 0,
|
fontFamily: 'Helvetica',
|
||||||
|
backgroundColor: C.white,
|
||||||
|
color: C.gray900,
|
||||||
},
|
},
|
||||||
|
hero: { paddingBottom: 20, marginBottom: 10 },
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
|
|
||||||
logoText: {
|
logoText: {
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
@@ -63,7 +45,6 @@ const styles = StyleSheet.create({
|
|||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
|
|
||||||
docTitle: {
|
docTitle: {
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
@@ -71,16 +52,8 @@ const styles = StyleSheet.create({
|
|||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
|
productRow: { flexDirection: 'row', alignItems: 'center', gap: 20 },
|
||||||
productRow: {
|
productInfoCol: { flex: 1, justifyContent: 'center' },
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 20,
|
|
||||||
},
|
|
||||||
productInfoCol: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
productImageCol: {
|
productImageCol: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: 120,
|
height: 120,
|
||||||
@@ -92,51 +65,26 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: C.white,
|
backgroundColor: C.white,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Product Hero Info
|
|
||||||
productHero: {
|
|
||||||
marginTop: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
productName: {
|
productName: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: C.navyDeep,
|
color: C.navyDeep,
|
||||||
marginBottom: 0,
|
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: -0.5,
|
letterSpacing: -0.5,
|
||||||
},
|
},
|
||||||
|
|
||||||
productMeta: {
|
productMeta: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: C.gray600,
|
color: C.gray600,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
|
marginBottom: 2,
|
||||||
},
|
},
|
||||||
|
heroImage: { width: '100%', height: '100%', objectFit: 'contain' },
|
||||||
|
noImage: { fontSize: 8, color: C.gray400, textAlign: 'center' },
|
||||||
|
|
||||||
heroImage: {
|
content: {},
|
||||||
width: '100%',
|
section: { marginBottom: 20 },
|
||||||
height: '100%',
|
|
||||||
objectFit: 'contain',
|
|
||||||
},
|
|
||||||
|
|
||||||
noImage: {
|
|
||||||
fontSize: 8,
|
|
||||||
color: C.gray400,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Content Area
|
|
||||||
content: {
|
|
||||||
paddingHorizontal: MARGIN,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Content sections
|
|
||||||
section: {
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
|
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
@@ -145,7 +93,6 @@ const styles = StyleSheet.create({
|
|||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 1.5,
|
letterSpacing: 1.5,
|
||||||
},
|
},
|
||||||
|
|
||||||
sectionAccent: {
|
sectionAccent: {
|
||||||
width: 30,
|
width: 30,
|
||||||
height: 2,
|
height: 2,
|
||||||
@@ -153,67 +100,9 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
},
|
},
|
||||||
|
description: { fontSize: 10, lineHeight: 1.7, color: C.gray600 },
|
||||||
|
|
||||||
description: {
|
categories: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 },
|
||||||
fontSize: 10,
|
|
||||||
lineHeight: 1.7,
|
|
||||||
color: C.gray600,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Technical data table
|
|
||||||
specsTable: {
|
|
||||||
marginTop: 4,
|
|
||||||
borderWidth: 0,
|
|
||||||
borderRadius: 0,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
|
|
||||||
specsTableRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
borderBottomWidth: 0.5,
|
|
||||||
borderBottomColor: C.gray200,
|
|
||||||
},
|
|
||||||
|
|
||||||
specsTableRowLast: {
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
specsTableLabelCell: {
|
|
||||||
flex: 1,
|
|
||||||
paddingVertical: 5,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
backgroundColor: C.offWhite,
|
|
||||||
borderRightWidth: 0.5,
|
|
||||||
borderRightColor: C.gray200,
|
|
||||||
},
|
|
||||||
|
|
||||||
specsTableValueCell: {
|
|
||||||
flex: 1,
|
|
||||||
paddingVertical: 5,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
},
|
|
||||||
|
|
||||||
specsTableLabelText: {
|
|
||||||
fontSize: 8,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: C.navyDeep,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
|
||||||
|
|
||||||
specsTableValueText: {
|
|
||||||
fontSize: 9,
|
|
||||||
color: C.gray900,
|
|
||||||
fontWeight: 400,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Categories
|
|
||||||
categories: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 6,
|
|
||||||
},
|
|
||||||
|
|
||||||
categoryTag: {
|
categoryTag: {
|
||||||
backgroundColor: C.offWhite,
|
backgroundColor: C.offWhite,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
@@ -222,7 +111,6 @@ const styles = StyleSheet.create({
|
|||||||
borderColor: C.gray200,
|
borderColor: C.gray200,
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
},
|
},
|
||||||
|
|
||||||
categoryText: {
|
categoryText: {
|
||||||
fontSize: 7,
|
fontSize: 7,
|
||||||
color: C.gray600,
|
color: C.gray600,
|
||||||
@@ -231,7 +119,6 @@ const styles = StyleSheet.create({
|
|||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Footer — matches brochure style
|
|
||||||
footer: {
|
footer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 28,
|
bottom: 28,
|
||||||
@@ -244,7 +131,6 @@ const styles = StyleSheet.create({
|
|||||||
borderTopWidth: 2,
|
borderTopWidth: 2,
|
||||||
borderTopColor: C.green,
|
borderTopColor: C.green,
|
||||||
},
|
},
|
||||||
|
|
||||||
footerText: {
|
footerText: {
|
||||||
fontSize: 7,
|
fontSize: 7,
|
||||||
color: C.gray400,
|
color: C.gray400,
|
||||||
@@ -252,7 +138,6 @@ const styles = StyleSheet.create({
|
|||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 0.8,
|
letterSpacing: 0.8,
|
||||||
},
|
},
|
||||||
|
|
||||||
footerBrand: {
|
footerBrand: {
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
@@ -260,6 +145,43 @@ const styles = StyleSheet.create({
|
|||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 1.5,
|
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 {
|
interface ProductData {
|
||||||
@@ -276,29 +198,22 @@ interface ProductData {
|
|||||||
images?: string[];
|
images?: string[];
|
||||||
featuredImage?: string | null;
|
featuredImage?: string | null;
|
||||||
categories?: Array<{ name: string }>;
|
categories?: Array<{ name: string }>;
|
||||||
attributes?: Array<{
|
attributes?: Array<{ name: string; options: string[] }>;
|
||||||
name: string;
|
|
||||||
options: string[];
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PDFDatasheetProps {
|
export interface PDFDatasheetProps {
|
||||||
product: ProductData;
|
product: ProductData;
|
||||||
locale: 'en' | 'de';
|
locale: 'en' | 'de';
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
technicalItems?: any[];
|
technicalItems?: KeyValueItem[];
|
||||||
voltageTables?: any[];
|
voltageTables?: DatasheetVoltageTable[];
|
||||||
legendItems?: any[];
|
legendItems?: KeyValueItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to strip HTML tags
|
const stripHtml = (html: string): string => html.replace(/<[^>]*>/g, '');
|
||||||
const stripHtml = (html: string): string => {
|
|
||||||
return html.replace(/<[^>]*>/g, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper to get translated labels
|
const getLabels = (locale: 'en' | 'de') =>
|
||||||
const getLabels = (locale: 'en' | 'de') => {
|
({
|
||||||
const labels = {
|
|
||||||
en: {
|
en: {
|
||||||
productDatasheet: 'Technical Datasheet',
|
productDatasheet: 'Technical Datasheet',
|
||||||
description: 'APPLICATION',
|
description: 'APPLICATION',
|
||||||
@@ -306,6 +221,9 @@ const getLabels = (locale: 'en' | 'de') => {
|
|||||||
categories: 'CATEGORIES',
|
categories: 'CATEGORIES',
|
||||||
sku: 'SKU',
|
sku: 'SKU',
|
||||||
noImage: 'No image available',
|
noImage: 'No image available',
|
||||||
|
crossSection: 'Configurations',
|
||||||
|
slug_cs: 'Cores & CS',
|
||||||
|
abbreviations: 'ABBREVIATIONS',
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
productDatasheet: 'Technisches Datenblatt',
|
productDatasheet: 'Technisches Datenblatt',
|
||||||
@@ -314,18 +232,257 @@ const getLabels = (locale: 'en' | 'de') => {
|
|||||||
categories: 'KATEGORIEN',
|
categories: 'KATEGORIEN',
|
||||||
sku: 'ARTIKELNUMMER',
|
sku: 'ARTIKELNUMMER',
|
||||||
noImage: 'Kein Bild verfügbar',
|
noImage: 'Kein Bild verfügbar',
|
||||||
|
crossSection: 'Konfigurationen',
|
||||||
|
slug_cs: 'Adern & QS',
|
||||||
|
abbreviations: 'ABKÜRZUNGEN',
|
||||||
},
|
},
|
||||||
};
|
})[locale];
|
||||||
return labels[locale];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ product, 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 (
|
||||||
|
<View style={styles.kvGrid}>
|
||||||
|
{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 (
|
||||||
|
<View
|
||||||
|
key={`${left.label}-${rowIndex}`}
|
||||||
|
style={[
|
||||||
|
styles.kvRow,
|
||||||
|
rowIndex % 2 === 0 ? styles.kvRowAlt : null,
|
||||||
|
isLast ? styles.kvRowLast : null,
|
||||||
|
]}
|
||||||
|
wrap={false}
|
||||||
|
>
|
||||||
|
<View style={[styles.kvCell, { width: '23%' }]}>
|
||||||
|
<Text style={styles.kvLabelText}>{left.label}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.kvCell, styles.kvMidDivider, { width: '27%' }]}>
|
||||||
|
<Text style={styles.kvValueText}>{leftValue}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.kvCell, { width: '23%' }]}>
|
||||||
|
<Text style={styles.kvLabelText}>{right?.label || ''}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.kvCell, { width: '27%' }]}>
|
||||||
|
<Text style={styles.kvValueText}>{rightValue}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DenseTable({
|
||||||
|
table,
|
||||||
|
firstColLabel,
|
||||||
|
}: {
|
||||||
|
table: Pick<DatasheetVoltageTable, 'columns' | 'rows'>;
|
||||||
|
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 (
|
||||||
|
<View style={styles.tableWrap} break={false}>
|
||||||
|
<View style={styles.tableHeader} wrap={false}>
|
||||||
|
<View style={{ width: cfgW }}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.tableHeaderCell,
|
||||||
|
styles.tableHeaderCellCfg,
|
||||||
|
{ fontSize: headerFontSize, paddingHorizontal: 3 },
|
||||||
|
cols.length ? styles.tableHeaderCellDivider : null,
|
||||||
|
]}
|
||||||
|
wrap={false}
|
||||||
|
>
|
||||||
|
{headerText(firstColLabel)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{cols.map((c, idx) => {
|
||||||
|
const isLast = idx === cols.length - 1;
|
||||||
|
return (
|
||||||
|
<View key={c.key} style={{ width: dataWs[idx] }}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.tableHeaderCell,
|
||||||
|
{ fontSize: headerFontSize, paddingHorizontal: 3 },
|
||||||
|
!isLast ? styles.tableHeaderCellDivider : null,
|
||||||
|
]}
|
||||||
|
wrap={false}
|
||||||
|
>
|
||||||
|
{headerText(c.label)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
{rows.map((r, ri) => (
|
||||||
|
<View
|
||||||
|
key={`${r.configuration}-${ri}`}
|
||||||
|
style={[styles.tableRow, ri % 2 === 0 ? styles.tableRowAlt : null]}
|
||||||
|
wrap={false}
|
||||||
|
minPresenceAhead={16}
|
||||||
|
>
|
||||||
|
<View style={{ width: cfgW }} wrap={false}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.tableCell,
|
||||||
|
styles.tableCellCfg,
|
||||||
|
{ fontSize: 6.2, paddingHorizontal: 3 },
|
||||||
|
cols.length ? styles.tableCellDivider : null,
|
||||||
|
]}
|
||||||
|
wrap={false}
|
||||||
|
>
|
||||||
|
{r.configuration}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{r.cells.map((cell, ci) => (
|
||||||
|
<View key={`${cols[ci]?.key || ci}`} style={{ width: dataWs[ci] }} wrap={false}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.tableCell,
|
||||||
|
ci !== r.cells.length - 1 ? styles.tableCellDivider : null,
|
||||||
|
]}
|
||||||
|
wrap={false}
|
||||||
|
>
|
||||||
|
{cell}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
|
||||||
|
product,
|
||||||
|
locale,
|
||||||
|
technicalItems = [],
|
||||||
|
voltageTables = [],
|
||||||
|
legendItems = [],
|
||||||
|
}) => {
|
||||||
const labels = getLabels(locale);
|
const labels = getLabels(locale);
|
||||||
|
const description = stripHtml(
|
||||||
|
product.applicationHtml ||
|
||||||
|
product.shortDescriptionHtml ||
|
||||||
|
product.descriptionHtml ||
|
||||||
|
product.descriptionText ||
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page size="A4" style={styles.page}>
|
<Page size="A4" style={styles.page}>
|
||||||
{/* Hero Header */}
|
|
||||||
<View style={styles.hero}>
|
<View style={styles.hero}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View>
|
<View>
|
||||||
@@ -333,18 +490,13 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ product, locale }) =
|
|||||||
</View>
|
</View>
|
||||||
<Text style={styles.docTitle}>{labels.productDatasheet}</Text>
|
<Text style={styles.docTitle}>{labels.productDatasheet}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.productRow}>
|
<View style={styles.productRow}>
|
||||||
<View style={styles.productInfoCol}>
|
<View style={styles.productInfoCol}>
|
||||||
<View style={styles.productHero}>
|
<Text style={styles.productMeta}>
|
||||||
<View style={styles.categories}>
|
{product.categoriesLine ||
|
||||||
<Text style={styles.productMeta}>
|
(product.categories || []).map((c) => c.name).join(' • ')}
|
||||||
{product.categoriesLine ||
|
</Text>
|
||||||
(product.categories || []).map((c) => c.name).join(' • ')}
|
<Text style={styles.productName}>{product.name}</Text>
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.productName}>{product.name}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.productImageCol}>
|
<View style={styles.productImageCol}>
|
||||||
{product.featuredImage ? (
|
{product.featuredImage ? (
|
||||||
@@ -357,64 +509,82 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ product, locale }) =
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{/* Description section */}
|
{description && (
|
||||||
{(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && (
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>{labels.description}</Text>
|
<Text style={styles.sectionTitle}>{labels.description}</Text>
|
||||||
<View style={styles.sectionAccent} />
|
<View style={styles.sectionAccent} />
|
||||||
<Text style={styles.description}>
|
<Text style={styles.description}>{description}</Text>
|
||||||
{stripHtml(
|
|
||||||
product.applicationHtml ||
|
|
||||||
product.shortDescriptionHtml ||
|
|
||||||
product.descriptionHtml,
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Technical specifications */}
|
{technicalItems.length > 0 && (
|
||||||
{product.attributes && product.attributes.length > 0 && (
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
|
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
|
||||||
<View style={styles.sectionAccent} />
|
<View style={styles.sectionAccent} />
|
||||||
<View style={styles.specsTable}>
|
<KeyValueGrid items={technicalItems} />
|
||||||
{product.attributes.map((attr, index) => (
|
|
||||||
<View
|
|
||||||
key={index}
|
|
||||||
style={[
|
|
||||||
styles.specsTableRow,
|
|
||||||
index === product.attributes.length - 1 && styles.specsTableRowLast,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={styles.specsTableLabelCell}>
|
|
||||||
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.specsTableValueCell}>
|
|
||||||
<Text style={styles.specsTableValueText}>{attr.options.join(', ')}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Categories as clean tags */}
|
{voltageTables.map((table, idx) => (
|
||||||
{product.categories && product.categories.length > 0 && (
|
<View key={idx} style={styles.section} break={false}>
|
||||||
<View style={styles.section}>
|
<Text
|
||||||
<Text style={styles.sectionTitle}>{labels.categories}</Text>
|
style={styles.sectionTitle}
|
||||||
|
>{`${labels.crossSection} — ${table.voltageLabel}`}</Text>
|
||||||
<View style={styles.sectionAccent} />
|
<View style={styles.sectionAccent} />
|
||||||
<View style={styles.categories}>
|
<DenseTable table={table} firstColLabel={labels.slug_cs} />
|
||||||
{product.categories.map((cat, index) => (
|
</View>
|
||||||
<View key={index} style={styles.categoryTag}>
|
))}
|
||||||
<Text style={styles.categoryText}>{cat.name}</Text>
|
|
||||||
</View>
|
{legendItems.length > 0 && (
|
||||||
))}
|
<View style={styles.section} break={false}>
|
||||||
</View>
|
<Text style={styles.sectionTitle}>{labels.abbreviations}</Text>
|
||||||
|
<View style={styles.sectionAccent} />
|
||||||
|
<KeyValueGrid items={legendItems} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!technicalItems.length &&
|
||||||
|
!voltageTables.length &&
|
||||||
|
product.attributes &&
|
||||||
|
product.attributes.length > 0 && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
|
||||||
|
<View style={styles.sectionAccent} />
|
||||||
|
<View style={{ borderWidth: 1, borderColor: C.gray200 }}>
|
||||||
|
{product.attributes.map((attr, index) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
borderBottomWidth: index === product.attributes!.length - 1 ? 0 : 1,
|
||||||
|
borderBottomColor: C.gray200,
|
||||||
|
backgroundColor: index % 2 === 0 ? C.offWhite : C.white,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: 6,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: C.gray200,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 8.5, fontWeight: 700, color: C.gray600 }}>
|
||||||
|
{attr.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1, padding: 6 }}>
|
||||||
|
<Text style={{ fontSize: 9.5, color: C.gray900 }}>
|
||||||
|
{attr.options.join(', ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Minimal footer */}
|
|
||||||
<View style={styles.footer} fixed>
|
<View style={styles.footer} fixed>
|
||||||
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
||||||
<Text style={styles.footerText}>
|
<Text style={styles.footerText}>
|
||||||
|
|||||||
Reference in New Issue
Block a user