This commit is contained in:
@@ -21,234 +21,149 @@ Font.register({
|
|||||||
// Industrial/technical/restrained design - STYLEGUIDE.md compliant
|
// Industrial/technical/restrained design - STYLEGUIDE.md compliant
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
page: {
|
page: {
|
||||||
// Large margins for engineering documentation feel.
|
color: '#111827', // Text Primary
|
||||||
// Extra bottom padding reserves space for the fixed footer so content
|
lineHeight: 1.5,
|
||||||
// (esp. long descriptions) doesn't render underneath it.
|
backgroundColor: '#FFFFFF',
|
||||||
paddingTop: 72,
|
paddingTop: 0,
|
||||||
paddingLeft: 72,
|
paddingBottom: 100,
|
||||||
paddingRight: 72,
|
|
||||||
paddingBottom: 140,
|
|
||||||
fontFamily: 'Helvetica',
|
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: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'center',
|
||||||
marginBottom: 48, // Large spacing
|
marginBottom: 16,
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
logoText: {
|
logoText: {
|
||||||
fontSize: 20,
|
fontSize: 24,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: '#0E2A47', // Dark navy
|
color: '#000d26',
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
textTransform: 'uppercase',
|
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: {
|
docTitle: {
|
||||||
fontSize: 16,
|
fontSize: 10,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: '#0E2A47', // Dark navy
|
color: '#001a4d',
|
||||||
marginBottom: 8,
|
letterSpacing: 2,
|
||||||
letterSpacing: 0.5,
|
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
|
|
||||||
skuContainer: {
|
productRow: {
|
||||||
backgroundColor: '#E6E9ED', // Light gray background
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 16,
|
alignItems: 'center',
|
||||||
paddingVertical: 8,
|
gap: 20,
|
||||||
border: '1px solid #E6E9ED',
|
},
|
||||||
|
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: {
|
// Product Hero Info
|
||||||
fontSize: 8,
|
productHero: {
|
||||||
color: '#6B7280', // Medium gray
|
marginTop: 0,
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
productName: {
|
productName: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: '#0E2A47', // Dark navy
|
color: '#000d26',
|
||||||
marginBottom: 12,
|
marginBottom: 0,
|
||||||
lineHeight: 1.2,
|
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 0.5,
|
letterSpacing: -0.5,
|
||||||
},
|
},
|
||||||
|
|
||||||
productMeta: {
|
productMeta: {
|
||||||
fontSize: 12,
|
fontSize: 10,
|
||||||
color: '#6B7280', // Medium gray
|
color: '#4b5563',
|
||||||
fontWeight: 500,
|
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: {
|
section: {
|
||||||
marginBottom: 32,
|
marginBottom: 20,
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
padding: 24,
|
|
||||||
border: '1px solid #E6E9ED',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: '#0E2A47', // Dark navy
|
color: '#000d26', // Primary Dark
|
||||||
marginBottom: 16,
|
marginBottom: 8,
|
||||||
letterSpacing: 0.5,
|
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
borderBottom: '1px solid #E6E9ED',
|
letterSpacing: -0.2,
|
||||||
paddingBottom: 8,
|
},
|
||||||
|
|
||||||
|
sectionAccent: {
|
||||||
|
width: 30,
|
||||||
|
height: 3,
|
||||||
|
backgroundColor: '#82ed20', // Accent Green
|
||||||
|
marginBottom: 8,
|
||||||
|
borderRadius: 1.5,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Description - technical documentation style
|
|
||||||
description: {
|
description: {
|
||||||
fontSize: 10,
|
fontSize: 11,
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.7,
|
||||||
color: '#1F2933', // Dark gray text
|
color: '#4b5563', // Text Secondary
|
||||||
marginBottom: 0,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Cross-section table - engineering specification style
|
// Technical data table
|
||||||
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)
|
|
||||||
specsTable: {
|
specsTable: {
|
||||||
borderWidth: 1,
|
marginTop: 8,
|
||||||
borderColor: '#E6E9ED',
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
|
|
||||||
specsTableRow: {
|
specsTableRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#E6E9ED',
|
borderBottomColor: '#e5e7eb',
|
||||||
},
|
},
|
||||||
|
|
||||||
specsTableRowLast: {
|
specsTableRowLast: {
|
||||||
@@ -256,63 +171,35 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
specsTableLabelCell: {
|
specsTableLabelCell: {
|
||||||
flex: 3,
|
flex: 1,
|
||||||
paddingVertical: 8,
|
paddingVertical: 4,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 16,
|
||||||
backgroundColor: '#F8F9FA',
|
backgroundColor: '#f8f9fa',
|
||||||
borderRightWidth: 1,
|
borderRightWidth: 1,
|
||||||
borderRightColor: '#E6E9ED',
|
borderRightColor: '#e5e7eb',
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
specsTableValueCell: {
|
specsTableValueCell: {
|
||||||
flex: 4,
|
flex: 1,
|
||||||
paddingVertical: 8,
|
paddingVertical: 4,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 16,
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
specsTableLabelText: {
|
specsTableLabelText: {
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: '#0E2A47',
|
color: '#000d26',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.5,
|
||||||
lineHeight: 1.2,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
specsTableValueText: {
|
specsTableValueText: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: '#1F2933',
|
color: '#111827',
|
||||||
lineHeight: 1.4,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
|
|
||||||
specColumn: {
|
// Categories
|
||||||
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',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
@@ -320,42 +207,48 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
categoryTag: {
|
categoryTag: {
|
||||||
backgroundColor: '#E6E9ED',
|
backgroundColor: '#f8f9fa',
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
border: '1px solid #E6E9ED',
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: 100,
|
||||||
},
|
},
|
||||||
|
|
||||||
categoryText: {
|
categoryText: {
|
||||||
fontSize: 9,
|
fontSize: 8,
|
||||||
color: '#6B7280',
|
color: '#4b5563',
|
||||||
fontWeight: 500,
|
fontWeight: 700,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.5,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Engineering documentation footer
|
// Footer
|
||||||
footer: {
|
footer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 48,
|
bottom: 40,
|
||||||
left: 72,
|
left: 72,
|
||||||
right: 72,
|
right: 72,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingTop: 24,
|
paddingTop: 24,
|
||||||
borderTop: '2px solid #E6E9ED',
|
borderTop: '1px solid #e5e7eb',
|
||||||
fontSize: 9,
|
|
||||||
color: '#6B7280',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
footerLeft: {
|
footerText: {
|
||||||
|
fontSize: 8,
|
||||||
|
color: '#9ca3af',
|
||||||
|
fontWeight: 500,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
footerBrand: {
|
||||||
|
fontSize: 10,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: '#0E2A47',
|
color: '#000d26',
|
||||||
},
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 1,
|
||||||
footerRight: {
|
|
||||||
color: '#6B7280',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -364,6 +257,7 @@ interface ProductData {
|
|||||||
name: string;
|
name: string;
|
||||||
shortDescriptionHtml: string;
|
shortDescriptionHtml: string;
|
||||||
descriptionHtml: string;
|
descriptionHtml: string;
|
||||||
|
applicationHtml?: string;
|
||||||
images: string[];
|
images: string[];
|
||||||
featuredImage: string | null;
|
featuredImage: string | null;
|
||||||
sku: string;
|
sku: string;
|
||||||
@@ -418,99 +312,101 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page size="A4" style={styles.page}>
|
<Page size="A4" style={styles.page}>
|
||||||
{/* Clean, minimal header */}
|
{/* Hero Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.hero}>
|
||||||
<View style={styles.logoArea}>
|
<View style={styles.header}>
|
||||||
<View style={styles.logoContainer}>
|
<View>
|
||||||
{logoUrl ? (
|
<Text style={styles.logoText}>KLZ</Text>
|
||||||
/* 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>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.docInfo}>
|
|
||||||
<Text style={styles.docTitle}>
|
<Text style={styles.docTitle}>
|
||||||
{labels.productDatasheet}
|
{labels.productDatasheet}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.skuContainer}>
|
|
||||||
<Text style={styles.skuLabel}>{labels.sku}</Text>
|
|
||||||
<Text style={styles.skuValue}>{product.sku}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Product section - clean and prominent */}
|
<View style={styles.productRow}>
|
||||||
<View style={styles.productSection}>
|
<View style={styles.productInfoCol}>
|
||||||
<Text style={styles.productName}>{product.name}</Text>
|
<View style={styles.productHero}>
|
||||||
<Text style={styles.productMeta}>
|
<View style={styles.categories}>
|
||||||
{product.categories.map(cat => cat.name).join(' • ')}
|
{product.categories.map((cat, index) => (
|
||||||
</Text>
|
<Text key={index} style={styles.productMeta}>
|
||||||
</View>
|
{cat.name}{index < product.categories.length - 1 ? ' • ' : ''}
|
||||||
|
|
||||||
{/* 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(', ')}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
))}
|
||||||
</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>
|
</View>
|
||||||
)}
|
</View>
|
||||||
|
|
||||||
{/* Categories as clean tags */}
|
<View style={styles.content}>
|
||||||
{product.categories && product.categories.length > 0 && (
|
{/* Description section */}
|
||||||
<View style={styles.section}>
|
{(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml) && (
|
||||||
<Text style={styles.sectionTitle}>{labels.categories}</Text>
|
<View style={styles.section}>
|
||||||
<View style={styles.categories}>
|
<Text style={styles.sectionTitle}>{labels.description}</Text>
|
||||||
{product.categories.map((cat, index) => (
|
<View style={styles.sectionAccent} />
|
||||||
<View key={index} style={styles.categoryTag}>
|
<Text style={styles.description}>
|
||||||
<Text style={styles.categoryText}>{cat.name}</Text>
|
{stripHtml(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml)}
|
||||||
</View>
|
</Text>
|
||||||
))}
|
|
||||||
</View>
|
</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 */}
|
{/* Minimal footer */}
|
||||||
<View style={styles.footer} fixed>
|
<View style={styles.footer} fixed>
|
||||||
<Text style={styles.footerLeft}>
|
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
||||||
{labels.sku}: {product.sku}
|
<Text style={styles.footerText}>
|
||||||
</Text>
|
|
||||||
<Text style={styles.footerRight}>
|
|
||||||
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
|
{new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
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.
BIN
public/datasheets/medium-voltage/n2xsfl2y-mv-de.pdf
Normal file
BIN
public/datasheets/medium-voltage/n2xsfl2y-mv-de.pdf
Normal file
Binary file not shown.
BIN
public/datasheets/medium-voltage/n2xsfl2y-mv-en.pdf
Normal file
BIN
public/datasheets/medium-voltage/n2xsfl2y-mv-en.pdf
Normal file
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.
BIN
public/datasheets/medium-voltage/na2xsfl2y-mv-de.pdf
Normal file
BIN
public/datasheets/medium-voltage/na2xsfl2y-mv-de.pdf
Normal file
Binary file not shown.
BIN
public/datasheets/medium-voltage/na2xsfl2y-mv-en.pdf
Normal file
BIN
public/datasheets/medium-voltage/na2xsfl2y-mv-en.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -39,6 +39,7 @@ type MdxProduct = {
|
|||||||
categories: string[];
|
categories: string[];
|
||||||
images: string[];
|
images: string[];
|
||||||
descriptionHtml: string;
|
descriptionHtml: string;
|
||||||
|
applicationHtml: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MdxIndex = Map<string, MdxProduct>; // key: normalized designation/title
|
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 images = Array.isArray(data.images) ? data.images.map((i: any) => normalizeValue(String(i))).filter(Boolean) : [];
|
||||||
|
|
||||||
const descriptionHtml = extractDescriptionFromMdxFrontmatter(data);
|
const descriptionHtml = extractDescriptionFromMdxFrontmatter(data);
|
||||||
|
const applicationHtml = normalizeValue(String(data?.application || ''));
|
||||||
|
|
||||||
const slug = path.basename(file, '.mdx');
|
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;
|
return idx;
|
||||||
@@ -183,6 +185,7 @@ async function loadProductsFromExcelAndMdx(locale: 'en' | 'de'): Promise<Product
|
|||||||
name: title,
|
name: title,
|
||||||
shortDescriptionHtml: '',
|
shortDescriptionHtml: '',
|
||||||
descriptionHtml,
|
descriptionHtml,
|
||||||
|
applicationHtml: mdx?.applicationHtml || '',
|
||||||
images: mdx?.images || [],
|
images: mdx?.images || [],
|
||||||
featuredImage: (mdx?.images && mdx.images[0]) || null,
|
featuredImage: (mdx?.images && mdx.images[0]) || null,
|
||||||
sku: mdx?.sku || title,
|
sku: mdx?.sku || title,
|
||||||
|
|||||||
@@ -1129,7 +1129,7 @@ function buildMediumVoltageCrossSectionTableFromNewExcel(args: {
|
|||||||
export function buildDatasheetModel(args: { product: ProductData; locale: 'en' | 'de' }): DatasheetModel {
|
export function buildDatasheetModel(args: { product: ProductData; locale: 'en' | 'de' }): DatasheetModel {
|
||||||
const labels = getLabels(args.locale);
|
const labels = getLabels(args.locale);
|
||||||
const categoriesLine = (args.product.categories || []).map(c => stripHtml(c.name)).join(' • ');
|
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 heroSrc = resolveMediaToLocalPath(args.product.featuredImage || args.product.images?.[0] || null);
|
||||||
const productUrl = getProductUrl(args.product);
|
const productUrl = getProductUrl(args.product);
|
||||||
|
|
||||||
@@ -1173,22 +1173,71 @@ export function buildDatasheetModel(args: { product: ProductData; locale: 'en' |
|
|||||||
productUrl,
|
productUrl,
|
||||||
},
|
},
|
||||||
labels,
|
labels,
|
||||||
technicalItems: [
|
technicalItems: (() => {
|
||||||
...(excelModel.ok ? excelModel.technicalItems : []),
|
if (!isMediumVoltageProduct(args.product)) {
|
||||||
...(isMediumVoltageProduct(args.product)
|
return excelModel.ok ? excelModel.technicalItems : [];
|
||||||
? args.locale === 'de'
|
}
|
||||||
? [
|
|
||||||
{ label: 'Prüfspannung 6/10 kV', value: '21 kV' },
|
const pn = normalizeDesignation(args.product.name || '');
|
||||||
{ label: 'Prüfspannung 12/20 kV', value: '42 kV' },
|
const isAl = /^NA/.test(pn);
|
||||||
{ label: 'Prüfspannung 18/30 kV', value: '63 kV' },
|
const isFL = pn.includes('FL');
|
||||||
]
|
const isF = !isFL && pn.includes('F');
|
||||||
: [
|
|
||||||
{ label: 'Test voltage 6/10 kV', value: '21 kV' },
|
const findExcelVal = (labelPart: string) => {
|
||||||
{ label: 'Test voltage 12/20 kV', value: '42 kV' },
|
const found = excelModel.technicalItems.find(it => it.label.toLowerCase().includes(labelPart.toLowerCase()));
|
||||||
{ label: 'Test voltage 18/30 kV', value: '63 kV' },
|
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,
|
voltageTables,
|
||||||
legendItems: crossSectionModel.legendItems || [],
|
legendItems: crossSectionModel.legendItems || [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface ProductData {
|
|||||||
name: string;
|
name: string;
|
||||||
shortDescriptionHtml: string;
|
shortDescriptionHtml: string;
|
||||||
descriptionHtml: string;
|
descriptionHtml: string;
|
||||||
|
applicationHtml: string;
|
||||||
images: string[];
|
images: string[];
|
||||||
featuredImage: string | null;
|
featuredImage: string | null;
|
||||||
sku: string;
|
sku: string;
|
||||||
|
|||||||
@@ -26,56 +26,62 @@ export function DatasheetDocument(props: { model: DatasheetModel; assets: Assets
|
|||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page size="A4" style={styles.page}>
|
<Page size="A4" style={styles.page}>
|
||||||
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} />
|
<View style={styles.hero}>
|
||||||
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
|
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} isHero={true} />
|
||||||
|
|
||||||
<Text style={styles.h1}>{model.product.name}</Text>
|
<View style={styles.productRow}>
|
||||||
{model.product.categoriesLine ? <Text style={styles.subhead}>{model.product.categoriesLine}</Text> : null}
|
<View style={styles.productInfoCol}>
|
||||||
|
<View style={styles.productHero}>
|
||||||
<View style={styles.heroBox}>
|
{model.product.categoriesLine ? <Text style={styles.productMeta}>{model.product.categoriesLine}</Text> : null}
|
||||||
{assets.heroDataUrl ? (
|
<Text style={styles.productName}>{model.product.name}</Text>
|
||||||
<Image src={assets.heroDataUrl} style={styles.heroImage} />
|
</View>
|
||||||
) : (
|
</View>
|
||||||
<Text style={styles.noImage}>{model.labels.noImage}</Text>
|
<View style={styles.productImageCol}>
|
||||||
)}
|
{assets.heroDataUrl ? (
|
||||||
|
<Image src={assets.heroDataUrl} style={styles.heroImage} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.noImage}>{model.labels.noImage}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{model.product.descriptionText ? (
|
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
|
||||||
<Section title={model.labels.description} minPresenceAhead={24}>
|
|
||||||
<Text style={styles.body}>{model.product.descriptionText}</Text>
|
|
||||||
</Section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{model.technicalItems.length ? (
|
<View style={styles.content}>
|
||||||
<Section title={model.labels.technicalData} minPresenceAhead={24}>
|
{model.product.descriptionText ? (
|
||||||
<KeyValueGrid items={model.technicalItems} />
|
<Section title={model.labels.description} minPresenceAhead={24}>
|
||||||
</Section>
|
<Text style={styles.body}>{model.product.descriptionText}</Text>
|
||||||
) : null}
|
</Section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{model.technicalItems.length ? (
|
||||||
|
<Section title={model.labels.technicalData} minPresenceAhead={24}>
|
||||||
|
<KeyValueGrid items={model.technicalItems} />
|
||||||
|
</Section>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
</Page>
|
</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}>
|
<Page size="A4" style={styles.page}>
|
||||||
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} />
|
<Header title={headerTitle} logoDataUrl={assets.logoDataUrl} qrDataUrl={assets.qrDataUrl} />
|
||||||
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
|
<Footer locale={model.locale} siteUrl={CONFIG.siteUrl} />
|
||||||
|
|
||||||
{model.voltageTables.map((t: DatasheetVoltageTable) => (
|
<View style={styles.content}>
|
||||||
<View key={t.voltageLabel} style={{ marginBottom: 14 }} break={false} minPresenceAhead={24}>
|
{model.voltageTables.map((t: DatasheetVoltageTable) => (
|
||||||
<Text style={styles.sectionTitle}>{`${model.labels.crossSection} — ${t.voltageLabel}`}</Text>
|
<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} />
|
{model.legendItems.length ? (
|
||||||
</View>
|
<Section title={model.locale === 'de' ? 'ABKÜRZUNGEN' : 'ABBREVIATIONS'} minPresenceAhead={24}>
|
||||||
))}
|
<KeyValueGrid items={model.legendItems} />
|
||||||
|
</Section>
|
||||||
{model.legendItems.length ? (
|
) : null}
|
||||||
<Section title={model.locale === 'de' ? 'ABKÜRZUNGEN' : 'ABBREVIATIONS'} minPresenceAhead={24}>
|
</View>
|
||||||
<KeyValueGrid items={model.legendItems} />
|
|
||||||
</Section>
|
|
||||||
) : null}
|
|
||||||
</Page>
|
</Page>
|
||||||
</Document>
|
</Document>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export function Footer(props: { locale: 'en' | 'de'; siteUrl?: string }): React.
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.footer} fixed>
|
<View style={styles.footer} fixed>
|
||||||
<Text>{siteUrl}</Text>
|
<Text style={styles.footerBrand}>KLZ CABLES</Text>
|
||||||
<Text>{date}</Text>
|
<Text style={styles.footerText}>{date}</Text>
|
||||||
<Text render={({ pageNumber, totalPages }) => `${pageNumber}/${totalPages}`} />
|
<Text style={styles.footerText} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ import { Image, Text, View } from '@react-pdf/renderer';
|
|||||||
|
|
||||||
import { styles } from '../styles';
|
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 (
|
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}>
|
<View style={styles.headerLeft}>
|
||||||
{props.logoDataUrl ? (
|
{props.logoDataUrl ? (
|
||||||
<Image src={props.logoDataUrl} style={styles.logo} />
|
<Image src={props.logoDataUrl} style={styles.logo} />
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.brandFallback}>
|
<Text style={styles.brandFallback}>KLZ</Text>
|
||||||
<Text style={styles.brandFallbackKlz}>KLZ</Text>
|
|
||||||
<Text style={styles.brandFallbackCables}>Cables</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.headerRight}>
|
<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);
|
const items = (props.items || []).filter(i => i.label && i.value);
|
||||||
if (!items.length) return null;
|
if (!items.length) return null;
|
||||||
|
|
||||||
// 4-column layout: (label, value, label, value)
|
// 2-column layout: (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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.kvGrid}>
|
<View style={styles.kvGrid}>
|
||||||
{rows.map(([left, right], rowIndex) => {
|
{items.map((item, rowIndex) => {
|
||||||
const isLast = rowIndex === rows.length - 1;
|
const isLast = rowIndex === items.length - 1;
|
||||||
const leftValue = left.unit ? `${left.value} ${left.unit}` : left.value;
|
const value = item.unit ? `${item.value} ${item.unit}` : item.value;
|
||||||
const rightValue = right ? (right.unit ? `${right.value} ${right.unit}` : right.value) : '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={`${left.label}-${rowIndex}`}
|
key={`${item.label}-${rowIndex}`}
|
||||||
style={[styles.kvRow, rowIndex % 2 === 0 ? styles.kvRowAlt : null, isLast ? styles.kvRowLast : null]}
|
style={[styles.kvRow, rowIndex % 2 === 0 ? styles.kvRowAlt : null, isLast ? styles.kvRowLast : null]}
|
||||||
wrap={false}
|
wrap={false}
|
||||||
minPresenceAhead={12}
|
minPresenceAhead={12}
|
||||||
>
|
>
|
||||||
<View style={[styles.kvCell, { width: '18%' }]}>
|
<View style={[styles.kvCell, { width: '50%' }]}>
|
||||||
<Text style={styles.kvLabelText}>{left.label}</Text>
|
<Text style={styles.kvLabelText}>{item.label}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.kvCell, styles.kvMidDivider, { width: '32%' }]}>
|
<View style={[styles.kvCell, { width: '50%' }]}>
|
||||||
<Text style={styles.kvValueText}>{leftValue}</Text>
|
<Text style={styles.kvValueText}>{value}</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>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ export function Section(props: {
|
|||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const boxed = props.boxed ?? true;
|
const boxed = props.boxed ?? true;
|
||||||
return (
|
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>
|
<Text style={styles.sectionTitle}>{props.title}</Text>
|
||||||
|
<View style={styles.sectionAccent} />
|
||||||
{props.children}
|
{props.children}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,146 +5,212 @@ import { Font, StyleSheet } from '@react-pdf/renderer';
|
|||||||
Font.registerHyphenationCallback(word => [word]);
|
Font.registerHyphenationCallback(word => [word]);
|
||||||
|
|
||||||
export const COLORS = {
|
export const COLORS = {
|
||||||
navy: '#0E2A47',
|
primary: '#001a4d',
|
||||||
mediumGray: '#6B7280',
|
primaryDark: '#000d26',
|
||||||
darkGray: '#1F2933',
|
accent: '#82ed20',
|
||||||
lightGray: '#E6E9ED',
|
textPrimary: '#111827',
|
||||||
almostWhite: '#F8F9FA',
|
textSecondary: '#4b5563',
|
||||||
headerBg: '#F6F8FB',
|
textLight: '#9ca3af',
|
||||||
|
neutral: '#f8f9fa',
|
||||||
|
border: '#e5e7eb',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const styles = StyleSheet.create({
|
export const styles = StyleSheet.create({
|
||||||
page: {
|
page: {
|
||||||
paddingTop: 54,
|
paddingTop: 0,
|
||||||
paddingLeft: 54,
|
paddingLeft: 30,
|
||||||
paddingRight: 54,
|
paddingRight: 30,
|
||||||
paddingBottom: 72,
|
paddingBottom: 60,
|
||||||
fontFamily: 'Helvetica',
|
fontFamily: 'Helvetica',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: COLORS.darkGray,
|
color: COLORS.textPrimary,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Hero-style header
|
||||||
|
hero: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
paddingTop: 30,
|
||||||
|
paddingBottom: 0,
|
||||||
|
paddingHorizontal: 0,
|
||||||
|
marginBottom: 20,
|
||||||
|
position: 'relative',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
borderBottomColor: COLORS.border,
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 12,
|
marginBottom: 24,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 0,
|
||||||
backgroundColor: COLORS.headerBg,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: COLORS.lightGray,
|
|
||||||
marginBottom: 16,
|
|
||||||
},
|
},
|
||||||
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||||
logo: { width: 110, height: 24, objectFit: 'contain' },
|
logo: { width: 100, height: 22, objectFit: 'contain' },
|
||||||
brandFallback: { flexDirection: 'row', alignItems: 'baseline', gap: 6 },
|
brandFallback: { fontSize: 20, fontWeight: 700, color: COLORS.primaryDark, letterSpacing: 1, textTransform: 'uppercase' },
|
||||||
brandFallbackKlz: { fontSize: 18, fontWeight: 700, color: COLORS.navy },
|
|
||||||
brandFallbackCables: { fontSize: 10, color: COLORS.mediumGray },
|
|
||||||
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||||
headerTitle: { fontSize: 9, fontWeight: 700, color: COLORS.navy, letterSpacing: 0.2 },
|
headerTitle: { fontSize: 9, fontWeight: 700, color: COLORS.primary, letterSpacing: 1.5, textTransform: 'uppercase' },
|
||||||
qr: { width: 34, height: 34, objectFit: 'contain' },
|
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: {
|
footer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 54,
|
left: 30,
|
||||||
right: 54,
|
right: 30,
|
||||||
bottom: 36,
|
bottom: 30,
|
||||||
paddingTop: 10,
|
paddingTop: 16,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: COLORS.lightGray,
|
borderTopColor: COLORS.border,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
fontSize: 8,
|
alignItems: 'center',
|
||||||
color: COLORS.mediumGray,
|
|
||||||
},
|
},
|
||||||
|
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 },
|
h1: { fontSize: 22, fontWeight: 700, color: COLORS.primaryDark, marginBottom: 8, textTransform: 'uppercase' },
|
||||||
subhead: { fontSize: 10.5, color: COLORS.mediumGray, marginBottom: 14 },
|
subhead: { fontSize: 10, fontWeight: 700, color: COLORS.textSecondary, marginBottom: 16, textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||||
|
|
||||||
heroBox: {
|
heroBox: {
|
||||||
height: 110,
|
height: 180,
|
||||||
|
borderRadius: 12,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: COLORS.lightGray,
|
borderColor: COLORS.border,
|
||||||
backgroundColor: COLORS.almostWhite,
|
backgroundColor: '#FFFFFF',
|
||||||
marginBottom: 16,
|
marginBottom: 24,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
padding: 0,
|
||||||
},
|
},
|
||||||
heroImage: { width: '100%', height: '100%', objectFit: 'contain' },
|
heroImage: { width: '100%', height: '100%', objectFit: 'contain' },
|
||||||
noImage: { fontSize: 8, color: COLORS.mediumGray, paddingHorizontal: 12 },
|
noImage: { fontSize: 8, color: COLORS.textLight, textAlign: 'center' },
|
||||||
|
|
||||||
section: {
|
section: {
|
||||||
borderWidth: 1,
|
marginBottom: 10,
|
||||||
borderColor: COLORS.lightGray,
|
|
||||||
padding: 14,
|
|
||||||
marginBottom: 14,
|
|
||||||
},
|
|
||||||
sectionPlain: {
|
|
||||||
paddingVertical: 2,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 10,
|
fontSize: 14,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: COLORS.navy,
|
color: COLORS.primaryDark,
|
||||||
marginBottom: 8,
|
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: {
|
kvGrid: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: COLORS.lightGray,
|
borderColor: COLORS.border,
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
kvRow: {
|
kvRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: COLORS.lightGray,
|
borderBottomColor: COLORS.border,
|
||||||
},
|
},
|
||||||
kvRowAlt: { backgroundColor: COLORS.almostWhite },
|
kvRowAlt: { backgroundColor: COLORS.neutral },
|
||||||
kvRowLast: { borderBottomWidth: 0 },
|
kvRowLast: { borderBottomWidth: 0 },
|
||||||
kvCell: { paddingVertical: 6, paddingHorizontal: 8 },
|
kvCell: { paddingVertical: 3, paddingHorizontal: 12 },
|
||||||
// Visual separator between (label,value) pairs in the 4-col KV grid.
|
|
||||||
// Matches the engineering-table look and improves scanability.
|
|
||||||
kvMidDivider: {
|
kvMidDivider: {
|
||||||
borderRightWidth: 1,
|
borderRightWidth: 1,
|
||||||
borderRightColor: COLORS.lightGray,
|
borderRightColor: COLORS.border,
|
||||||
},
|
},
|
||||||
kvLabelText: { fontSize: 8.5, fontWeight: 700, color: COLORS.mediumGray },
|
kvLabelText: { fontSize: 8, fontWeight: 700, color: COLORS.primaryDark, textTransform: 'uppercase', letterSpacing: 0.3 },
|
||||||
kvValueText: { fontSize: 9.5, color: COLORS.darkGray },
|
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: {
|
tableHeader: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: COLORS.neutral,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: COLORS.lightGray,
|
borderBottomColor: COLORS.border,
|
||||||
},
|
},
|
||||||
tableHeaderCell: {
|
tableHeaderCell: {
|
||||||
paddingVertical: 5,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 6,
|
||||||
fontSize: 6.6,
|
fontSize: 7,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: COLORS.navy,
|
color: COLORS.primaryDark,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 0.2,
|
||||||
},
|
},
|
||||||
tableHeaderCellCfg: {
|
tableHeaderCellCfg: {
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 8,
|
||||||
},
|
},
|
||||||
tableHeaderCellDivider: {
|
tableHeaderCellDivider: {
|
||||||
borderRightWidth: 1,
|
borderRightWidth: 1,
|
||||||
borderRightColor: COLORS.lightGray,
|
borderRightColor: COLORS.border,
|
||||||
},
|
},
|
||||||
tableRow: { width: '100%', flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.lightGray },
|
tableRow: { width: '100%', flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.border },
|
||||||
tableRowAlt: { backgroundColor: COLORS.almostWhite },
|
tableRowAlt: { backgroundColor: '#FFFFFF' },
|
||||||
tableCell: { paddingVertical: 4, paddingHorizontal: 4, fontSize: 6.6, color: COLORS.darkGray },
|
tableCell: { paddingVertical: 6, paddingHorizontal: 6, fontSize: 7, color: COLORS.textSecondary, fontWeight: 500 },
|
||||||
tableCellCfg: {
|
tableCellCfg: {
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 8,
|
||||||
},
|
},
|
||||||
tableCellDivider: {
|
tableCellDivider: {
|
||||||
borderRightWidth: 1,
|
borderRightWidth: 1,
|
||||||
borderRightColor: COLORS.lightGray,
|
borderRightColor: COLORS.border,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user