sheets
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 17m33s

This commit is contained in:
2026-01-30 22:10:01 +01:00
parent 757df76f36
commit e4eabd7a86
56 changed files with 484 additions and 475 deletions
+210 -314
View File
@@ -21,234 +21,149 @@ Font.register({
// Industrial/technical/restrained design - STYLEGUIDE.md compliant
const styles = StyleSheet.create({
page: {
// Large margins for engineering documentation feel.
// Extra bottom padding reserves space for the fixed footer so content
// (esp. long descriptions) doesn't render underneath it.
paddingTop: 72,
paddingLeft: 72,
paddingRight: 72,
paddingBottom: 140,
color: '#111827', // Text Primary
lineHeight: 1.5,
backgroundColor: '#FFFFFF',
paddingTop: 0,
paddingBottom: 100,
fontFamily: 'Helvetica',
fontSize: 10,
color: '#1F2933', // Dark gray text
lineHeight: 1.5, // Generous line height
backgroundColor: '#F8F9FA', // Almost white background
},
// Engineering documentation header
// Hero-style header
hero: {
backgroundColor: '#FFFFFF',
paddingTop: 24,
paddingBottom: 0,
paddingHorizontal: 72,
marginBottom: 20,
position: 'relative',
borderBottomWidth: 0,
borderBottomColor: '#e5e7eb',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 48, // Large spacing
paddingBottom: 24,
borderBottom: '2px solid #E6E9ED', // Light gray separator
},
// Logo area - industrial style
logoArea: {
flexDirection: 'column',
alignItems: 'flex-start',
},
// Optional image logo container (keeps header height stable)
logoContainer: {
width: 120,
height: 32,
justifyContent: 'center',
},
// Image logo (preferred when available)
logo: {
width: 110,
height: 28,
objectFit: 'contain',
alignItems: 'center',
marginBottom: 16,
},
logoText: {
fontSize: 20,
fontSize: 24,
fontWeight: 700,
color: '#0E2A47', // Dark navy
color: '#000d26',
letterSpacing: 1,
textTransform: 'uppercase',
},
logoSubtext: {
fontSize: 10,
fontWeight: 400,
color: '#6B7280', // Medium gray
letterSpacing: 0.5,
marginTop: 2,
},
// Document info - technical style
docInfo: {
textAlign: 'right',
alignItems: 'flex-end',
},
docTitle: {
fontSize: 16,
fontSize: 10,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 8,
letterSpacing: 0.5,
color: '#001a4d',
letterSpacing: 2,
textTransform: 'uppercase',
},
skuContainer: {
backgroundColor: '#E6E9ED', // Light gray background
paddingHorizontal: 16,
paddingVertical: 8,
border: '1px solid #E6E9ED',
productRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 20,
},
productInfoCol: {
flex: 1,
justifyContent: 'center',
},
productImageCol: {
flex: 1,
height: 120,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
borderWidth: 1,
borderColor: '#e5e7eb',
backgroundColor: '#FFFFFF',
overflow: 'hidden',
},
skuLabel: {
fontSize: 8,
color: '#6B7280', // Medium gray
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 4,
},
skuValue: {
fontSize: 14,
fontWeight: 700,
color: '#0E2A47', // Dark navy
},
// Product section - technical specification style
productSection: {
marginBottom: 40,
backgroundColor: '#FFFFFF', // White background for content blocks
padding: 24,
border: '1px solid #E6E9ED',
// Product Hero Info
productHero: {
marginTop: 0,
},
productName: {
fontSize: 24,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 12,
lineHeight: 1.2,
color: '#000d26',
marginBottom: 0,
textTransform: 'uppercase',
letterSpacing: 0.5,
letterSpacing: -0.5,
},
productMeta: {
fontSize: 12,
color: '#6B7280', // Medium gray
fontWeight: 500,
fontSize: 10,
color: '#4b5563',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 1,
},
// Content sections - rectangular blocks
heroImage: {
width: '100%',
height: '100%',
objectFit: 'contain',
},
noImage: {
fontSize: 8,
color: '#9ca3af',
textAlign: 'center',
},
// Content Area
content: {
paddingHorizontal: 72,
},
// Content sections
section: {
marginBottom: 32,
backgroundColor: '#FFFFFF',
padding: 24,
border: '1px solid #E6E9ED',
marginBottom: 20,
},
sectionTitle: {
fontSize: 14,
fontWeight: 700,
color: '#0E2A47', // Dark navy
marginBottom: 16,
letterSpacing: 0.5,
color: '#000d26', // Primary Dark
marginBottom: 8,
textTransform: 'uppercase',
borderBottom: '1px solid #E6E9ED',
paddingBottom: 8,
letterSpacing: -0.2,
},
sectionAccent: {
width: 30,
height: 3,
backgroundColor: '#82ed20', // Accent Green
marginBottom: 8,
borderRadius: 1.5,
},
// Description - technical documentation style
description: {
fontSize: 10,
lineHeight: 1.6,
color: '#1F2933', // Dark gray text
marginBottom: 0,
fontSize: 11,
lineHeight: 1.7,
color: '#4b5563', // Text Secondary
},
// Cross-section table - engineering specification style
table: {
marginTop: 16,
borderWidth: 1,
borderColor: '#E6E9ED',
},
tableHeader: {
flexDirection: 'row',
backgroundColor: '#E6E9ED',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
},
tableHeaderCell: {
flex: 1,
padding: 8,
fontSize: 10,
fontWeight: 700,
color: '#0E2A47',
textTransform: 'uppercase',
letterSpacing: 0.3,
},
tableHeaderCellLast: {
borderRightWidth: 0,
},
tableHeaderCellWithDivider: {
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
},
tableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
},
tableCell: {
flex: 1,
padding: 8,
fontSize: 10,
color: '#1F2933',
},
tableCellLast: {
borderRightWidth: 0,
},
tableCellWithDivider: {
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
},
tableRowAlt: {
backgroundColor: '#F8F9FA',
},
// Specifications - technical data style
specsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
// Backwards-compatible alias used by the component markup
specsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
// Technical data table (used for the metagrid)
// Technical data table
specsTable: {
borderWidth: 1,
borderColor: '#E6E9ED',
marginTop: 8,
border: '1px solid #e5e7eb',
borderRadius: 8,
overflow: 'hidden',
},
specsTableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
borderBottomColor: '#e5e7eb',
},
specsTableRowLast: {
@@ -256,63 +171,35 @@ const styles = StyleSheet.create({
},
specsTableLabelCell: {
flex: 3,
paddingVertical: 8,
paddingHorizontal: 8,
backgroundColor: '#F8F9FA',
flex: 1,
paddingVertical: 4,
paddingHorizontal: 16,
backgroundColor: '#f8f9fa',
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
justifyContent: 'center',
borderRightColor: '#e5e7eb',
},
specsTableValueCell: {
flex: 4,
paddingVertical: 8,
paddingHorizontal: 8,
justifyContent: 'center',
flex: 1,
paddingVertical: 4,
paddingHorizontal: 16,
},
specsTableLabelText: {
fontSize: 9,
fontWeight: 700,
color: '#0E2A47',
color: '#000d26',
textTransform: 'uppercase',
letterSpacing: 0.3,
lineHeight: 1.2,
letterSpacing: 0.5,
},
specsTableValueText: {
fontSize: 10,
color: '#1F2933',
lineHeight: 1.4,
color: '#111827',
fontWeight: 500,
},
specColumn: {
width: '48%',
marginRight: '4%',
marginBottom: 16,
},
specItem: {
marginBottom: 12,
},
specLabel: {
fontSize: 9,
fontWeight: 700,
color: '#0E2A47',
marginBottom: 4,
textTransform: 'uppercase',
letterSpacing: 0.3,
},
specValue: {
fontSize: 10,
color: '#1F2933',
lineHeight: 1.4,
},
// Categories - technical classification
// Categories
categories: {
flexDirection: 'row',
flexWrap: 'wrap',
@@ -320,42 +207,48 @@ const styles = StyleSheet.create({
},
categoryTag: {
backgroundColor: '#E6E9ED',
backgroundColor: '#f8f9fa',
paddingHorizontal: 12,
paddingVertical: 6,
border: '1px solid #E6E9ED',
border: '1px solid #e5e7eb',
borderRadius: 100,
},
categoryText: {
fontSize: 9,
color: '#6B7280',
fontWeight: 500,
fontSize: 8,
color: '#4b5563',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 0.3,
letterSpacing: 0.5,
},
// Engineering documentation footer
// Footer
footer: {
position: 'absolute',
bottom: 48,
bottom: 40,
left: 72,
right: 72,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: 24,
borderTop: '2px solid #E6E9ED',
fontSize: 9,
color: '#6B7280',
borderTop: '1px solid #e5e7eb',
},
footerLeft: {
footerText: {
fontSize: 8,
color: '#9ca3af',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: 1,
},
footerBrand: {
fontSize: 10,
fontWeight: 700,
color: '#0E2A47',
},
footerRight: {
color: '#6B7280',
color: '#000d26',
textTransform: 'uppercase',
letterSpacing: 1,
},
});
@@ -364,6 +257,7 @@ interface ProductData {
name: string;
shortDescriptionHtml: string;
descriptionHtml: string;
applicationHtml?: string;
images: string[];
featuredImage: string | null;
sku: string;
@@ -418,99 +312,101 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
return (
<Document>
<Page size="A4" style={styles.page}>
{/* Clean, minimal header */}
<View style={styles.header}>
<View style={styles.logoArea}>
<View style={styles.logoContainer}>
{logoUrl ? (
/* eslint-disable-next-line jsx-a11y/alt-text */
<Image src={logoUrl} style={styles.logo} />
) : (
<View>
<Text style={styles.logoText}>KLZ</Text>
<Text style={styles.logoSubtext}>Cables</Text>
</View>
)}
{/* Hero Header */}
<View style={styles.hero}>
<View style={styles.header}>
<View>
<Text style={styles.logoText}>KLZ</Text>
</View>
</View>
<View style={styles.docInfo}>
<Text style={styles.docTitle}>
{labels.productDatasheet}
</Text>
<View style={styles.skuContainer}>
<Text style={styles.skuLabel}>{labels.sku}</Text>
<Text style={styles.skuValue}>{product.sku}</Text>
</View>
</View>
</View>
{/* Product section - clean and prominent */}
<View style={styles.productSection}>
<Text style={styles.productName}>{product.name}</Text>
<Text style={styles.productMeta}>
{product.categories.map(cat => cat.name).join(' • ')}
</Text>
</View>
{/* Description section */}
{(product.shortDescriptionHtml || product.descriptionHtml) && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.description}</Text>
<Text style={styles.description}>
{stripHtml(product.shortDescriptionHtml || product.descriptionHtml)}
</Text>
</View>
)}
{/* Technical specifications */}
{product.attributes && product.attributes.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
<View style={styles.specsTable}>
{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(', ')}
<View style={styles.productRow}>
<View style={styles.productInfoCol}>
<View style={styles.productHero}>
<View style={styles.categories}>
{product.categories.map((cat, index) => (
<Text key={index} style={styles.productMeta}>
{cat.name}{index < product.categories.length - 1 ? ' • ' : ''}
</Text>
</View>
))}
</View>
))}
<Text style={styles.productName}>{product.name}</Text>
</View>
</View>
<View style={styles.productImageCol}>
{product.featuredImage ? (
<Image src={product.featuredImage} style={styles.heroImage} />
) : (
<Text style={styles.noImage}>{labels.noImage}</Text>
)}
</View>
</View>
)}
</View>
{/* Categories as clean tags */}
{product.categories && product.categories.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.categories}</Text>
<View style={styles.categories}>
{product.categories.map((cat, index) => (
<View key={index} style={styles.categoryTag}>
<Text style={styles.categoryText}>{cat.name}</Text>
</View>
))}
<View style={styles.content}>
{/* Description section */}
{(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.description}</Text>
<View style={styles.sectionAccent} />
<Text style={styles.description}>
{stripHtml(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml)}
</Text>
</View>
</View>
)}
)}
{/* Technical specifications */}
{product.attributes && product.attributes.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
<View style={styles.sectionAccent} />
<View style={styles.specsTable}>
{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>
)}
{/* Categories as clean tags */}
{product.categories && product.categories.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.categories}</Text>
<View style={styles.sectionAccent} />
<View style={styles.categories}>
{product.categories.map((cat, index) => (
<View key={index} style={styles.categoryTag}>
<Text style={styles.categoryText}>{cat.name}</Text>
</View>
))}
</View>
</View>
)}
</View>
{/* Minimal footer */}
<View style={styles.footer} fixed>
<Text style={styles.footerLeft}>
{labels.sku}: {product.sku}
</Text>
<Text style={styles.footerRight}>
<Text style={styles.footerBrand}>KLZ CABLES</Text>
<Text style={styles.footerText}>
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
year: 'numeric',
month: 'long',
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4 -1
View File
@@ -39,6 +39,7 @@ type MdxProduct = {
categories: string[];
images: string[];
descriptionHtml: string;
applicationHtml: string;
};
type MdxIndex = Map<string, MdxProduct>; // key: normalized designation/title
@@ -85,9 +86,10 @@ function buildMdxIndex(locale: 'en' | 'de'): MdxIndex {
const images = Array.isArray(data.images) ? data.images.map((i: any) => normalizeValue(String(i))).filter(Boolean) : [];
const descriptionHtml = extractDescriptionFromMdxFrontmatter(data);
const applicationHtml = normalizeValue(String(data?.application || ''));
const slug = path.basename(file, '.mdx');
idx.set(normalizeExcelKey(title), { slug, title, sku, categories, images, descriptionHtml });
idx.set(normalizeExcelKey(title), { slug, title, sku, categories, images, descriptionHtml, applicationHtml });
}
return idx;
@@ -183,6 +185,7 @@ async function loadProductsFromExcelAndMdx(locale: 'en' | 'de'): Promise<Product
name: title,
shortDescriptionHtml: '',
descriptionHtml,
applicationHtml: mdx?.applicationHtml || '',
images: mdx?.images || [],
featuredImage: (mdx?.images && mdx.images[0]) || null,
sku: mdx?.sku || title,
+66 -17
View File
@@ -1129,7 +1129,7 @@ function buildMediumVoltageCrossSectionTableFromNewExcel(args: {
export function buildDatasheetModel(args: { product: ProductData; locale: 'en' | 'de' }): DatasheetModel {
const labels = getLabels(args.locale);
const categoriesLine = (args.product.categories || []).map(c => stripHtml(c.name)).join(' • ');
const descriptionText = stripHtml(args.product.shortDescriptionHtml || args.product.descriptionHtml || '');
const descriptionText = stripHtml(args.product.applicationHtml || '');
const heroSrc = resolveMediaToLocalPath(args.product.featuredImage || args.product.images?.[0] || null);
const productUrl = getProductUrl(args.product);
@@ -1173,22 +1173,71 @@ export function buildDatasheetModel(args: { product: ProductData; locale: 'en' |
productUrl,
},
labels,
technicalItems: [
...(excelModel.ok ? excelModel.technicalItems : []),
...(isMediumVoltageProduct(args.product)
? args.locale === 'de'
? [
{ label: 'Prüfspannung 6/10 kV', value: '21 kV' },
{ label: 'Prüfspannung 12/20 kV', value: '42 kV' },
{ label: 'Prüfspannung 18/30 kV', value: '63 kV' },
]
: [
{ label: 'Test voltage 6/10 kV', value: '21 kV' },
{ label: 'Test voltage 12/20 kV', value: '42 kV' },
{ label: 'Test voltage 18/30 kV', value: '63 kV' },
]
: []),
],
technicalItems: (() => {
if (!isMediumVoltageProduct(args.product)) {
return excelModel.ok ? excelModel.technicalItems : [];
}
const pn = normalizeDesignation(args.product.name || '');
const isAl = /^NA/.test(pn);
const isFL = pn.includes('FL');
const isF = !isFL && pn.includes('F');
const findExcelVal = (labelPart: string) => {
const found = excelModel.technicalItems.find(it => it.label.toLowerCase().includes(labelPart.toLowerCase()));
return found ? found.value : null;
};
const items: KeyValueItem[] = [];
if (args.locale === 'de') {
items.push({ label: 'Leitermaterial', value: isAl ? 'Aluminium' : 'Kupfer' });
items.push({ label: 'Leiterklasse', value: isAl ? 'Klasse 1' : 'Klasse 2 mehrdrähtig' });
items.push({ label: 'Aderisolation', value: 'VPE DIX8' });
items.push({ label: 'Feldsteuerung', value: 'innere und äußere Leitschicht aus halbleitendem Kunststoff - 3-fach-extrudiert' });
items.push({ label: 'Schirm', value: 'Kupferdrähte + Querleitwendel' });
items.push({ label: 'Längswasserdichtigkeit', value: (isF || isFL) ? 'ja, mit Quellvliess' : 'nein' });
items.push({ label: 'Querwasserdichtigkeit', value: isFL ? 'ja, Al-Band' : 'nein' });
items.push({ label: 'Mantelmaterial', value: 'Polyethylen DMP2' });
items.push({ label: 'Mantelfarbe', value: 'schwarz' });
items.push({ label: 'Flammwidrigkeit', value: 'nein' });
items.push({ label: 'UV-beständig', value: 'ja' });
items.push({ label: 'Max. zulässige Leitertemperatur', value: findExcelVal('Leitertemperatur') || '90°C' });
items.push({ label: 'Zul. Kabelaußentemperatur, fest verlegt', value: findExcelVal('fest verlegt') || '70°C' });
items.push({ label: 'Zul. Kabelaußentemperatur, in Bewegung', value: findExcelVal('in Bewegung') || '-20 °C bis +70 °C' });
items.push({ label: 'Maximale Kurzschlußtemperatur', value: findExcelVal('Kurzschlußtemperatur') || '+250 °C' });
items.push({ label: 'Min. Biegeradius, fest verlegt', value: findExcelVal('Biegeradius') || '15 facher Durchmesser' });
items.push({ label: 'Mindesttemperatur Verlegung', value: findExcelVal('Verlegung') || '-5 °C' });
items.push({ label: 'Metermarkierung', value: 'ja' });
items.push({ label: 'Teilentladung', value: findExcelVal('Teilentladung') || '2 pC' });
items.push({ label: 'Prüfspannung 6/10 kV', value: '21 kV' });
items.push({ label: 'Prüfspannung 12/20 kV', value: '42 kV' });
items.push({ label: 'Prüfspannung 18/30 kV', value: '63 kV' });
} else {
items.push({ label: 'Conductor material', value: isAl ? 'Aluminum' : 'Copper' });
items.push({ label: 'Conductor class', value: isAl ? 'Class 1' : 'Class 2 stranded' });
items.push({ label: 'Core insulation', value: 'XLPE DIX8' });
items.push({ label: 'Field control', value: 'inner and outer semiconducting layer made of semiconducting plastic - 3-fold extruded' });
items.push({ label: 'Screen', value: 'copper wires + transverse conductive helix' });
items.push({ label: 'Longitudinal water tightness', value: (isF || isFL) ? 'yes, with swelling tape' : 'no' });
items.push({ label: 'Transverse water tightness', value: isFL ? 'yes, Al-tape' : 'no' });
items.push({ label: 'Sheath material', value: 'Polyethylene DMP2' });
items.push({ label: 'Sheath color', value: 'black' });
items.push({ label: 'Flame retardancy', value: 'no' });
items.push({ label: 'UV resistant', value: 'yes' });
items.push({ label: 'Max. permissible conductor temperature', value: findExcelVal('conductor temperature') || '90°C' });
items.push({ label: 'Permissible cable outer temperature, fixed', value: findExcelVal('fixed') || '70°C' });
items.push({ label: 'Permissible cable outer temperature, in motion', value: findExcelVal('in motion') || '-20 °C to +70 °C' });
items.push({ label: 'Maximum short-circuit temperature', value: findExcelVal('short-circuit temperature') || '+250 °C' });
items.push({ label: 'Min. bending radius, fixed', value: findExcelVal('bending radius') || '15 times diameter' });
items.push({ label: 'Minimum laying temperature', value: findExcelVal('laying temperature') || '-5 °C' });
items.push({ label: 'Meter marking', value: 'yes' });
items.push({ label: 'Partial discharge', value: findExcelVal('Partial discharge') || '2 pC' });
items.push({ label: 'Test voltage 6/10 kV', value: '21 kV' });
items.push({ label: 'Test voltage 12/20 kV', value: '42 kV' });
items.push({ label: 'Test voltage 18/30 kV', value: '63 kV' });
}
return items;
})(),
voltageTables,
legendItems: crossSectionModel.legendItems || [],
};
+1
View File
@@ -3,6 +3,7 @@ export interface ProductData {
name: string;
shortDescriptionHtml: string;
descriptionHtml: string;
applicationHtml: string;
images: string[];
featuredImage: string | null;
sku: string;
+46 -40
View File
@@ -26,56 +26,62 @@ export function DatasheetDocument(props: { model: DatasheetModel; assets: Assets
return (
<Document>
<Page size="A4" style={styles.page}>
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} />
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
<Text style={styles.h1}>{model.product.name}</Text>
{model.product.categoriesLine ? <Text style={styles.subhead}>{model.product.categoriesLine}</Text> : null}
<View style={styles.heroBox}>
{assets.heroDataUrl ? (
<Image src={assets.heroDataUrl} style={styles.heroImage} />
) : (
<Text style={styles.noImage}>{model.labels.noImage}</Text>
)}
<View style={styles.hero}>
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} isHero={true} />
<View style={styles.productRow}>
<View style={styles.productInfoCol}>
<View style={styles.productHero}>
{model.product.categoriesLine ? <Text style={styles.productMeta}>{model.product.categoriesLine}</Text> : null}
<Text style={styles.productName}>{model.product.name}</Text>
</View>
</View>
<View style={styles.productImageCol}>
{assets.heroDataUrl ? (
<Image src={assets.heroDataUrl} style={styles.heroImage} />
) : (
<Text style={styles.noImage}>{model.labels.noImage}</Text>
)}
</View>
</View>
</View>
{model.product.descriptionText ? (
<Section title={model.labels.description} minPresenceAhead={24}>
<Text style={styles.body}>{model.product.descriptionText}</Text>
</Section>
) : null}
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
{model.technicalItems.length ? (
<Section title={model.labels.technicalData} minPresenceAhead={24}>
<KeyValueGrid items={model.technicalItems} />
</Section>
) : null}
<View style={styles.content}>
{model.product.descriptionText ? (
<Section title={model.labels.description} minPresenceAhead={24}>
<Text style={styles.body}>{model.product.descriptionText}</Text>
</Section>
) : null}
{model.technicalItems.length ? (
<Section title={model.labels.technicalData} minPresenceAhead={24}>
<KeyValueGrid items={model.technicalItems} />
</Section>
) : null}
</View>
</Page>
{/*
Render all voltage sections in a single flow so React-PDF can paginate naturally.
This avoids hard page breaks that waste remaining whitespace at the bottom of a page.
Each table section has break={false} to prevent breaking within individual tables,
but the overall flow allows tables to move to the next page if needed.
*/}
<Page size="A4" style={styles.page}>
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} />
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
{model.voltageTables.map((t: DatasheetVoltageTable) => (
<View key={t.voltageLabel} style={{ marginBottom: 14 }} break={false} minPresenceAhead={24}>
<Text style={styles.sectionTitle}>{`${model.labels.crossSection} — ${t.voltageLabel}`}</Text>
<View style={styles.content}>
{model.voltageTables.map((t: DatasheetVoltageTable) => (
<View key={t.voltageLabel} style={{ marginBottom: 14 }} break={false} minPresenceAhead={24}>
<Text style={styles.sectionTitle}>{`${model.labels.crossSection} — ${t.voltageLabel}`}</Text>
<View style={styles.sectionAccent} />
<DenseTable table={{ columns: t.columns, rows: t.rows }} firstColLabel={firstColLabel} />
</View>
))}
<DenseTable table={{ columns: t.columns, rows: t.rows }} firstColLabel={firstColLabel} />
</View>
))}
{model.legendItems.length ? (
<Section title={model.locale === 'de' ? 'ABKÜRZUNGEN' : 'ABBREVIATIONS'} minPresenceAhead={24}>
<KeyValueGrid items={model.legendItems} />
</Section>
) : null}
{model.legendItems.length ? (
<Section title={model.locale === 'de' ? 'ABKÜRZUNGEN' : 'ABBREVIATIONS'} minPresenceAhead={24}>
<KeyValueGrid items={model.legendItems} />
</Section>
) : null}
</View>
</Page>
</Document>
);
+3 -3
View File
@@ -14,9 +14,9 @@ export function Footer(props: { locale: 'en' | 'de'; siteUrl?: string }): React.
return (
<View style={styles.footer} fixed>
<Text>{siteUrl}</Text>
<Text>{date}</Text>
<Text render={({ pageNumber, totalPages }) => `${pageNumber}/${totalPages}`} />
<Text style={styles.footerBrand}>KLZ CABLES</Text>
<Text style={styles.footerText}>{date}</Text>
<Text style={styles.footerText} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} />
</View>
);
}
+5 -6
View File
@@ -3,17 +3,16 @@ import { Image, Text, View } from '@react-pdf/renderer';
import { styles } from '../styles';
export function Header(props: { title: string; logoDataUrl?: string | null; qrDataUrl?: string | null }): React.ReactElement {
export function Header(props: { title: string; logoDataUrl?: string | null; qrDataUrl?: string | null; isHero?: boolean }): React.ReactElement {
const { isHero = false } = props;
return (
<View style={styles.header} fixed>
<View style={isHero ? styles.header : [styles.header, { paddingHorizontal: 0, backgroundColor: 'transparent', borderBottomWidth: 0, marginBottom: 24, paddingTop: 40 }]}>
<View style={styles.headerLeft}>
{props.logoDataUrl ? (
<Image src={props.logoDataUrl} style={styles.logo} />
) : (
<View style={styles.brandFallback}>
<Text style={styles.brandFallbackKlz}>KLZ</Text>
<Text style={styles.brandFallbackCables}>Cables</Text>
</View>
<Text style={styles.brandFallback}>KLZ</Text>
)}
</View>
<View style={styles.headerRight}>
@@ -8,37 +8,25 @@ export function KeyValueGrid(props: { items: KeyValueItem[] }): React.ReactEleme
const items = (props.items || []).filter(i => i.label && i.value);
if (!items.length) return null;
// 4-column layout: (label, value, label, value)
const rows: Array<[KeyValueItem, KeyValueItem | null]> = [];
for (let i = 0; i < items.length; i += 2) {
rows.push([items[i], items[i + 1] || null]);
}
// 2-column layout: (label, value)
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) : '';
{items.map((item, rowIndex) => {
const isLast = rowIndex === items.length - 1;
const value = item.unit ? `${item.value} ${item.unit}` : item.value;
return (
<View
key={`${left.label}-${rowIndex}`}
key={`${item.label}-${rowIndex}`}
style={[styles.kvRow, rowIndex % 2 === 0 ? styles.kvRowAlt : null, isLast ? styles.kvRowLast : null]}
wrap={false}
minPresenceAhead={12}
>
<View style={[styles.kvCell, { width: '18%' }]}>
<Text style={styles.kvLabelText}>{left.label}</Text>
<View style={[styles.kvCell, { width: '50%' }]}>
<Text style={styles.kvLabelText}>{item.label}</Text>
</View>
<View style={[styles.kvCell, styles.kvMidDivider, { width: '32%' }]}>
<Text style={styles.kvValueText}>{leftValue}</Text>
</View>
<View style={[styles.kvCell, { width: '18%' }]}>
<Text style={styles.kvLabelText}>{right?.label || ''}</Text>
</View>
<View style={[styles.kvCell, { width: '32%' }]}>
<Text style={styles.kvValueText}>{rightValue}</Text>
<View style={[styles.kvCell, { width: '50%' }]}>
<Text style={styles.kvValueText}>{value}</Text>
</View>
</View>
);
+2 -1
View File
@@ -11,8 +11,9 @@ export function Section(props: {
}): React.ReactElement {
const boxed = props.boxed ?? true;
return (
<View style={boxed ? styles.section : styles.sectionPlain} minPresenceAhead={props.minPresenceAhead}>
<View style={styles.section} minPresenceAhead={props.minPresenceAhead}>
<Text style={styles.sectionTitle}>{props.title}</Text>
<View style={styles.sectionAccent} />
{props.children}
</View>
);
+138 -72
View File
@@ -5,146 +5,212 @@ import { Font, StyleSheet } from '@react-pdf/renderer';
Font.registerHyphenationCallback(word => [word]);
export const COLORS = {
navy: '#0E2A47',
mediumGray: '#6B7280',
darkGray: '#1F2933',
lightGray: '#E6E9ED',
almostWhite: '#F8F9FA',
headerBg: '#F6F8FB',
primary: '#001a4d',
primaryDark: '#000d26',
accent: '#82ed20',
textPrimary: '#111827',
textSecondary: '#4b5563',
textLight: '#9ca3af',
neutral: '#f8f9fa',
border: '#e5e7eb',
} as const;
export const styles = StyleSheet.create({
page: {
paddingTop: 54,
paddingLeft: 54,
paddingRight: 54,
paddingBottom: 72,
paddingTop: 0,
paddingLeft: 30,
paddingRight: 30,
paddingBottom: 60,
fontFamily: 'Helvetica',
fontSize: 10,
color: COLORS.darkGray,
color: COLORS.textPrimary,
backgroundColor: '#FFFFFF',
},
// Hero-style header
hero: {
backgroundColor: '#FFFFFF',
paddingTop: 30,
paddingBottom: 0,
paddingHorizontal: 0,
marginBottom: 20,
position: 'relative',
borderBottomWidth: 0,
borderBottomColor: COLORS.border,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: COLORS.headerBg,
borderBottomWidth: 1,
borderBottomColor: COLORS.lightGray,
marginBottom: 16,
marginBottom: 24,
paddingHorizontal: 0,
},
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
logo: { width: 110, height: 24, objectFit: 'contain' },
brandFallback: { flexDirection: 'row', alignItems: 'baseline', gap: 6 },
brandFallbackKlz: { fontSize: 18, fontWeight: 700, color: COLORS.navy },
brandFallbackCables: { fontSize: 10, color: COLORS.mediumGray },
logo: { width: 100, height: 22, objectFit: 'contain' },
brandFallback: { fontSize: 20, fontWeight: 700, color: COLORS.primaryDark, letterSpacing: 1, textTransform: 'uppercase' },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 10 },
headerTitle: { fontSize: 9, fontWeight: 700, color: COLORS.navy, letterSpacing: 0.2 },
qr: { width: 34, height: 34, objectFit: 'contain' },
headerTitle: { fontSize: 9, fontWeight: 700, color: COLORS.primary, letterSpacing: 1.5, textTransform: 'uppercase' },
qr: { width: 30, height: 30, objectFit: 'contain' },
productRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 20,
},
productInfoCol: {
flex: 1,
justifyContent: 'center',
},
productImageCol: {
flex: 1,
height: 120,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
borderWidth: 1,
borderColor: COLORS.border,
backgroundColor: '#FFFFFF',
overflow: 'hidden',
},
productHero: {
marginTop: 0,
paddingHorizontal: 0,
},
productName: {
fontSize: 24,
fontWeight: 700,
color: COLORS.primaryDark,
marginBottom: 0,
textTransform: 'uppercase',
letterSpacing: -0.5,
},
productMeta: {
fontSize: 9,
color: COLORS.textSecondary,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 1,
marginBottom: 4,
},
content: {
paddingHorizontal: 0,
},
footer: {
position: 'absolute',
left: 54,
right: 54,
bottom: 36,
paddingTop: 10,
left: 30,
right: 30,
bottom: 30,
paddingTop: 16,
borderTopWidth: 1,
borderTopColor: COLORS.lightGray,
borderTopColor: COLORS.border,
flexDirection: 'row',
justifyContent: 'space-between',
fontSize: 8,
color: COLORS.mediumGray,
alignItems: 'center',
},
footerBrand: { fontSize: 9, fontWeight: 700, color: COLORS.primaryDark, textTransform: 'uppercase', letterSpacing: 1 },
footerText: { fontSize: 8, color: COLORS.textLight, fontWeight: 500, textTransform: 'uppercase', letterSpacing: 0.5 },
h1: { fontSize: 18, fontWeight: 700, color: COLORS.navy, marginBottom: 6 },
subhead: { fontSize: 10.5, color: COLORS.mediumGray, marginBottom: 14 },
h1: { fontSize: 22, fontWeight: 700, color: COLORS.primaryDark, marginBottom: 8, textTransform: 'uppercase' },
subhead: { fontSize: 10, fontWeight: 700, color: COLORS.textSecondary, marginBottom: 16, textTransform: 'uppercase', letterSpacing: 0.5 },
heroBox: {
height: 110,
height: 180,
borderRadius: 12,
borderWidth: 1,
borderColor: COLORS.lightGray,
backgroundColor: COLORS.almostWhite,
marginBottom: 16,
borderColor: COLORS.border,
backgroundColor: '#FFFFFF',
marginBottom: 24,
justifyContent: 'center',
overflow: 'hidden',
padding: 0,
},
heroImage: { width: '100%', height: '100%', objectFit: 'contain' },
noImage: { fontSize: 8, color: COLORS.mediumGray, paddingHorizontal: 12 },
noImage: { fontSize: 8, color: COLORS.textLight, textAlign: 'center' },
section: {
borderWidth: 1,
borderColor: COLORS.lightGray,
padding: 14,
marginBottom: 14,
},
sectionPlain: {
paddingVertical: 2,
marginBottom: 12,
marginBottom: 10,
},
sectionTitle: {
fontSize: 10,
fontSize: 14,
fontWeight: 700,
color: COLORS.navy,
color: COLORS.primaryDark,
marginBottom: 8,
letterSpacing: 0.2,
textTransform: 'uppercase',
letterSpacing: -0.2,
},
body: { fontSize: 10, lineHeight: 1.5, color: COLORS.darkGray },
sectionAccent: {
width: 30,
height: 3,
backgroundColor: COLORS.accent,
marginBottom: 8,
borderRadius: 1.5,
},
body: { fontSize: 10, lineHeight: 1.6, color: COLORS.textSecondary },
kvGrid: {
width: '100%',
borderWidth: 1,
borderColor: COLORS.lightGray,
borderColor: COLORS.border,
borderRadius: 8,
overflow: 'hidden',
},
kvRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: COLORS.lightGray,
borderBottomColor: COLORS.border,
},
kvRowAlt: { backgroundColor: COLORS.almostWhite },
kvRowAlt: { backgroundColor: COLORS.neutral },
kvRowLast: { borderBottomWidth: 0 },
kvCell: { paddingVertical: 6, paddingHorizontal: 8 },
// Visual separator between (label,value) pairs in the 4-col KV grid.
// Matches the engineering-table look and improves scanability.
kvCell: { paddingVertical: 3, paddingHorizontal: 12 },
kvMidDivider: {
borderRightWidth: 1,
borderRightColor: COLORS.lightGray,
borderRightColor: COLORS.border,
},
kvLabelText: { fontSize: 8.5, fontWeight: 700, color: COLORS.mediumGray },
kvValueText: { fontSize: 9.5, color: COLORS.darkGray },
kvLabelText: { fontSize: 8, fontWeight: 700, color: COLORS.primaryDark, textTransform: 'uppercase', letterSpacing: 0.3 },
kvValueText: { fontSize: 9, color: COLORS.textPrimary, fontWeight: 500 },
tableWrap: { width: '100%', borderWidth: 1, borderColor: COLORS.lightGray, marginBottom: 14 },
tableWrap: {
width: '100%',
borderWidth: 1,
borderColor: COLORS.border,
borderRadius: 8,
overflow: 'hidden',
marginBottom: 16,
},
tableHeader: {
width: '100%',
flexDirection: 'row',
backgroundColor: '#FFFFFF',
backgroundColor: COLORS.neutral,
borderBottomWidth: 1,
borderBottomColor: COLORS.lightGray,
borderBottomColor: COLORS.border,
},
tableHeaderCell: {
paddingVertical: 5,
paddingHorizontal: 4,
fontSize: 6.6,
paddingVertical: 8,
paddingHorizontal: 6,
fontSize: 7,
fontWeight: 700,
color: COLORS.navy,
color: COLORS.primaryDark,
textTransform: 'uppercase',
letterSpacing: 0.2,
},
tableHeaderCellCfg: {
paddingHorizontal: 6,
paddingHorizontal: 8,
},
tableHeaderCellDivider: {
borderRightWidth: 1,
borderRightColor: COLORS.lightGray,
borderRightColor: COLORS.border,
},
tableRow: { width: '100%', flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.lightGray },
tableRowAlt: { backgroundColor: COLORS.almostWhite },
tableCell: { paddingVertical: 4, paddingHorizontal: 4, fontSize: 6.6, color: COLORS.darkGray },
tableRow: { width: '100%', flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.border },
tableRowAlt: { backgroundColor: '#FFFFFF' },
tableCell: { paddingVertical: 6, paddingHorizontal: 6, fontSize: 7, color: COLORS.textSecondary, fontWeight: 500 },
tableCellCfg: {
paddingHorizontal: 6,
paddingHorizontal: 8,
},
tableCellDivider: {
borderRightWidth: 1,
borderRightColor: COLORS.lightGray,
borderRightColor: COLORS.border,
},
});