feat: product catalog
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 2m20s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-03-01 13:07:13 +01:00
parent 0cb96dfbac
commit 437dd35c9c
3 changed files with 286 additions and 243 deletions

View File

@@ -11,10 +11,10 @@ import {
// ─── Brand Colors ─────────────────────────────────────────────────────────── // ─── Brand Colors ───────────────────────────────────────────────────────────
const C = { const C = {
primary: '#001a4d', primary: '#001a4d', // Navy
primaryDark: '#000d26', primaryDark: '#000d26', // Deepest Navy
saturated: '#0117bf', saturated: '#0117bf',
accent: '#4da612', accent: '#4da612', // Green
accentLight: '#e8f5d8', accentLight: '#e8f5d8',
black: '#000000', black: '#000000',
white: '#FFFFFF', white: '#FFFFFF',
@@ -94,10 +94,21 @@ const L = (locale: 'en' | 'de') => locale === 'de' ? {
qrWeb: 'Web', qrPdf: 'PDF', values: 'Our Values', edition: 'Edition', page: 'p.', qrWeb: 'Web', qrPdf: 'PDF', values: 'Our Values', edition: 'Edition', page: 'p.',
}; };
// ─── Text tokens ────────────────────────────────────────────────────────────
const T = {
label: (d: boolean) => ({ fontSize: 9, fontWeight: 700, color: C.accent, textTransform: 'uppercase' as 'uppercase', letterSpacing: 1.2 }),
sectionTitle: (d: boolean) => ({ fontSize: 24, fontWeight: 700, color: d ? C.white : C.primaryDark, letterSpacing: -0.5 }),
body: (d: boolean) => ({ fontSize: 10, color: d ? C.gray300 : C.gray600, lineHeight: 1.7 }),
bodyLead: (d: boolean) => ({ fontSize: 13, color: d ? C.white : C.gray900, lineHeight: 1.8 }),
bodyBold: (d: boolean) => ({ fontSize: 10, fontWeight: 700, color: d ? C.white : C.primaryDark }),
caption: (d: boolean) => ({ fontSize: 8, color: d ? C.gray400 : C.gray400, textTransform: 'uppercase' as 'uppercase', letterSpacing: 1 }),
};
// ─── Rich Text (supports **bold** and *italic*) ──────────────────────────── // ─── Rich Text (supports **bold** and *italic*) ────────────────────────────
const RichText: React.FC<{ children: string; style?: any; paragraphGap?: number }> = ({ children, style = {}, paragraphGap = 8 }) => { const RichText: React.FC<{ children: string; style?: any; paragraphGap?: number; isDark?: boolean; asParagraphs?: boolean }> = ({ children, style = {}, paragraphGap = 8, isDark = false, asParagraphs = true }) => {
const paragraphs = children.split('\n\n').filter(p => p.trim()); const paragraphs = asParagraphs ? children.split('\n\n').filter(p => p.trim()) : [children];
return ( return (
<View style={{ gap: paragraphGap }}> <View style={{ gap: paragraphGap }}>
{paragraphs.map((para, pIdx) => { {paragraphs.map((para, pIdx) => {
@@ -116,7 +127,7 @@ const RichText: React.FC<{ children: string; style?: any; paragraphGap?: number
<Text key={pIdx} style={style}> <Text key={pIdx} style={style}>
{parts.map((part, i) => ( {parts.map((part, i) => (
<Text key={i} style={{ <Text key={i} style={{
...(part.bold ? { fontWeight: 700, fontFamily: 'Helvetica-Bold', color: C.primaryDark } : {}), ...(part.bold ? { fontWeight: 700, fontFamily: 'Helvetica-Bold', color: isDark ? C.white : C.primaryDark } : {}),
...(part.italic ? { fontWeight: 700, fontFamily: 'Helvetica-Bold', color: C.accent } : {}), ...(part.italic ? { fontWeight: 700, fontFamily: 'Helvetica-Bold', color: C.accent } : {}),
}}>{part.text}</Text> }}>{part.text}</Text>
))} ))}
@@ -127,105 +138,159 @@ const RichText: React.FC<{ children: string; style?: any; paragraphGap?: number
); );
}; };
// ─── Text tokens ────────────────────────────────────────────────────────────
const T = {
label: { fontSize: 9 as number, fontWeight: 700 as const, color: C.accent, textTransform: 'uppercase' as const, letterSpacing: 1.2 },
sectionTitle: { fontSize: 16 as number, fontWeight: 700 as const, color: C.primaryDark, letterSpacing: -0.3 },
body: { fontSize: 10 as number, color: C.gray600, lineHeight: 1.7 },
bodyLead: { fontSize: 11 as number, color: C.gray900, lineHeight: 1.8 },
bodyBold: { fontSize: 10 as number, fontWeight: 700 as const, color: C.primaryDark },
caption: { fontSize: 8 as number, color: C.gray400, textTransform: 'uppercase' as const, letterSpacing: 1 },
};
// ─── Reusable Components ──────────────────────────────────────────────────── // ─── Reusable Components ────────────────────────────────────────────────────
const FixedHeader: React.FC<{ logoBlack?: string | Buffer; rightText?: string }> = ({ logoBlack, rightText }) => ( const FixedHeader: React.FC<{ logoWhite?: string | Buffer; logoBlack?: string | Buffer; rightText?: string; isDark?: boolean }> = ({ logoWhite, logoBlack, rightText, isDark }) => {
<View style={{ const logo = isDark ? (logoWhite || logoBlack) : logoBlack;
position: 'absolute', top: 0, left: 0, right: 0, height: HEADER_H, return (
paddingHorizontal: M.h, paddingTop: 24, paddingBottom: 16, <View style={{
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', position: 'absolute', top: 0, left: 0, right: 0, height: HEADER_H,
borderBottomWidth: 0.5, borderBottomColor: C.gray300, borderBottomStyle: 'solid', paddingHorizontal: M.h, paddingTop: 24, paddingBottom: 16,
backgroundColor: C.white, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
}} fixed> borderBottomWidth: 0.5, borderBottomColor: isDark ? 'rgba(255,255,255,0.1)' : C.gray300, borderBottomStyle: 'solid',
{logoBlack ? <Image src={logoBlack} style={{ width: 64 }} /> : <Text style={{ fontSize: 16, fontWeight: 700, color: C.primaryDark }}>KLZ</Text>} backgroundColor: isDark ? C.primaryDark : C.white,
{rightText && <Text style={{ fontSize: 8, fontWeight: 700, color: C.primary, letterSpacing: 0.8, textTransform: 'uppercase' }}>{rightText}</Text>} }} fixed>
</View> {logo ? <Image src={logo} style={{ width: 64 }} /> : <Text style={{ fontSize: 16, fontWeight: 700, color: isDark ? C.white : C.primaryDark }}>KLZ</Text>}
); {rightText && <Text style={{ fontSize: 8, fontWeight: 700, color: isDark ? C.white : C.primary, letterSpacing: 0.8, textTransform: 'uppercase' }}>{rightText}</Text>}
const Footer: React.FC<{ left: string; right: string; logoBlack?: string | Buffer }> = ({ left, right, logoBlack }) => (
<View style={{
position: 'absolute', bottom: 40, left: M.h, right: M.h,
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
paddingTop: 12,
borderTopWidth: 0.5, borderTopColor: C.gray300, borderTopStyle: 'solid',
}} fixed>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
{logoBlack && <Image src={logoBlack} style={{ width: 40, height: 'auto' }} />}
<Text style={T.caption}>{left}</Text>
</View> </View>
<Text style={T.caption}>{right}</Text> );
</View> };
);
const SectionHeading: React.FC<{ label?: string; title: string }> = ({ label, title }) => ( const Footer: React.FC<{ left: string; right: string; logoWhite?: string | Buffer; logoBlack?: string | Buffer; isDark?: boolean }> = ({ left, right, logoWhite, logoBlack, isDark }) => {
<View style={{ marginBottom: S.md }} minPresenceAhead={120}> const logo = isDark ? (logoWhite || logoBlack) : logoBlack;
{label && <Text style={{ ...T.label, marginBottom: S.sm }}>{label}</Text>} return (
<Text style={{ ...T.sectionTitle, marginBottom: S.sm }}>{title}</Text> <View style={{
<View style={{ width: 32, height: 3, backgroundColor: C.accent, borderRadius: 1.5 }} /> position: 'absolute', bottom: 40, left: M.h, right: M.h,
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
paddingTop: 12,
borderTopWidth: 0.5, borderTopColor: isDark ? 'rgba(255,255,255,0.1)' : C.gray300, borderTopStyle: 'solid',
}} fixed>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
{logo && <Image src={logo} style={{ width: 40, height: 'auto' }} />}
<Text style={T.caption(!!isDark)}>{left}</Text>
</View>
<Text style={T.caption(!!isDark)}>{right}</Text>
</View>
);
};
const SectionHeading: React.FC<{ label?: string; title: string, isDark: boolean }> = ({ label, title, isDark }) => (
<View style={{ marginBottom: S.lg }} minPresenceAhead={120}>
{label && <Text style={{ ...T.label(isDark), marginBottom: S.sm }}>{label}</Text>}
<Text style={{ ...T.sectionTitle(isDark), marginBottom: S.md }}>{title}</Text>
<View style={{ width: 48, height: 4, backgroundColor: C.accent }} />
</View> </View>
); );
// Pull-quote callout block // Pull-quote callout block
const PullQuote: React.FC<{ quote: string }> = ({ quote }) => ( const PullQuote: React.FC<{ quote: string, isDark: boolean }> = ({ quote, isDark }) => (
<View style={{ <View style={{
borderLeftWidth: 3, borderLeftColor: C.accent, borderLeftStyle: 'solid', marginVertical: S.xl,
paddingLeft: S.md, paddingVertical: S.sm, paddingLeft: S.lg,
marginVertical: S.md, borderLeftWidth: 4, borderLeftColor: C.accent, borderLeftStyle: 'solid',
}}> }}>
<Text style={{ fontSize: 12, fontWeight: 700, color: C.primaryDark, lineHeight: 1.6 }}> <Text style={{ fontSize: 16, fontWeight: 700, color: isDark ? C.white : C.primaryDark, lineHeight: 1.5, letterSpacing: -0.2 }}>
{quote}" {quote}"
</Text> </Text>
</View> </View>
); );
// Stat highlight boxes // Stat highlight boxes
const HighlightRow: React.FC<{ highlights: Array<{ value: string; label: string }> }> = ({ highlights }) => ( const HighlightRow: React.FC<{ highlights: Array<{ value: string; label: string }>, isDark: boolean }> = ({ highlights, isDark }) => (
<View style={{ flexDirection: 'row', gap: S.md, marginVertical: S.md }}> <View style={{ flexDirection: 'row', gap: S.md, marginVertical: S.lg }}>
{highlights.map((h, i) => ( {highlights.map((h, i) => (
<View key={i} style={{ <View key={i} style={{
flex: 1, flex: 1,
backgroundColor: C.gray050, backgroundColor: isDark ? 'rgba(255,255,255,0.03)' : C.gray050,
borderWidth: 0.5, borderColor: C.gray200, borderStyle: 'solid', borderLeftWidth: 3, borderLeftColor: C.accent, borderLeftStyle: 'solid',
borderRadius: 4, paddingVertical: S.md, paddingHorizontal: S.md,
paddingVertical: S.sm, paddingHorizontal: S.md, alignItems: 'flex-start',
alignItems: 'center',
}}> }}>
<Text style={{ fontSize: 12, fontWeight: 700, color: C.accent, marginBottom: 2 }}>{h.value}</Text> <Text style={{ fontSize: 18, fontWeight: 700, color: isDark ? C.white : C.primaryDark, marginBottom: 4 }}>{h.value}</Text>
<Text style={{ fontSize: 8, color: C.gray600, textAlign: 'center' }}>{h.label}</Text> <Text style={{ fontSize: 9, color: isDark ? C.gray400 : C.gray600, textTransform: 'uppercase', letterSpacing: 0.5 }}>{h.label}</Text>
</View> </View>
))} ))}
</View> </View>
); );
// Inline image strip between sections // Magazine Edge-to-Edge Image
const SectionImage: React.FC<{ src: string | Buffer; height?: number }> = ({ src, height = 100 }) => { // By using negative horizontal margin (-M.h) matching the parent padding, it touches the very edges.
// Skip empty buffers // By using negative vertical margin matching the vertical padding, it touches the top or bottom of the colored block!
const MagazineImage: React.FC<{ src: string | Buffer; height?: number; position: 'top' | 'bottom' | 'middle'; isDark?: boolean }> = ({ src, height = 260, position, isDark }) => {
if (Buffer.isBuffer(src) && src.length === 0) return null; if (Buffer.isBuffer(src) && src.length === 0) return null;
const marginTop = position === 'top' ? -S.xxl : (position === 'middle' ? S.xl : S.xxl);
const marginBottom = position === 'bottom' ? -S.xxl : (position === 'middle' ? S.xl : S.xxl);
return ( return (
<View style={{ <View style={{
width: '100%', height, borderRadius: 4, overflow: 'hidden', marginHorizontal: -M.h,
marginVertical: S.md, height,
marginTop,
marginBottom,
position: 'relative'
}}> }}>
<Image src={src} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> <Image src={src} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
{isDark && (
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.primaryDark, opacity: 0.2 }} />
)}
</View> </View>
); );
}; };
// Horizontal divider // Magazine Block wrapper
const Divider: React.FC = () => ( const MagazineSection: React.FC<{
<View style={{ borderTopWidth: 0.5, borderTopColor: C.gray200, borderTopStyle: 'solid', marginVertical: S.lg }} /> section: NonNullable<BrochureProps['marketingSections']>[0];
); image?: string | Buffer;
theme: 'white' | 'gray' | 'dark';
imagePosition: 'top' | 'bottom' | 'middle';
}> = ({ section, image, theme, imagePosition }) => {
const isDark = theme === 'dark';
const bgColor = theme === 'white' ? C.white : (theme === 'gray' ? C.gray050 : C.primaryDark);
return (
<View style={{
marginHorizontal: -M.h,
paddingHorizontal: M.h,
paddingVertical: S.xxl,
backgroundColor: bgColor,
}} wrap={true}>
{image && imagePosition === 'top' && <MagazineImage src={image} height={280} position="top" isDark={isDark} />}
<SectionHeading label={section.subtitle} title={section.title} isDark={isDark} />
{section.description && (
<RichText style={T.bodyLead(isDark)} paragraphGap={S.md} isDark={isDark}>
{section.description}
</RichText>
)}
{image && imagePosition === 'middle' && <MagazineImage src={image} height={220} position="middle" isDark={isDark} />}
{section.highlights && section.highlights.length > 0 && (
<HighlightRow highlights={section.highlights} isDark={isDark} />
)}
{section.pullQuote && <PullQuote quote={section.pullQuote} isDark={isDark} />}
{section.items && section.items.length > 0 && (
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: S.xl, marginTop: S.xl }}>
{section.items.map((item, i) => (
<View key={i} style={{ width: '45%', marginBottom: S.sm }} minPresenceAhead={60}>
<View style={{ width: 24, height: 2, backgroundColor: C.accent, marginBottom: S.sm }} />
<Text style={{ ...T.bodyBold(isDark), fontSize: 11, marginBottom: 4 }}>{item.title}</Text>
<RichText style={{ ...T.body(isDark) }} asParagraphs={false} isDark={isDark}>
{item.description}
</RichText>
</View>
))}
</View>
)}
{image && imagePosition === 'bottom' && <MagazineImage src={image} height={320} position="bottom" isDark={isDark} />}
</View>
);
};
// ─── Cover Page ───────────────────────────────────────────────────────────── // ─── Cover Page ─────────────────────────────────────────────────────────────
@@ -233,145 +298,121 @@ const CoverPage: React.FC<{
locale: 'en' | 'de'; locale: 'en' | 'de';
introContent?: BrochureProps['introContent']; introContent?: BrochureProps['introContent'];
logoBlack?: string | Buffer; logoBlack?: string | Buffer;
logoWhite?: string | Buffer;
galleryImages?: Array<string | Buffer>; galleryImages?: Array<string | Buffer>;
}> = ({ locale, introContent, logoBlack, galleryImages }) => { }> = ({ locale, introContent, logoWhite, logoBlack, galleryImages }) => {
const l = L(locale); const l = L(locale);
const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', { year: 'numeric', month: 'long' }); const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', { year: 'numeric', month: 'long' });
const bgImage = galleryImages?.[0] || introContent?.heroImage; const bgImage = galleryImages?.[0] || introContent?.heroImage;
const logo = logoWhite || logoBlack;
return ( return (
<Page size="A4" style={{ backgroundColor: C.white, fontFamily: 'Helvetica' }}> <Page size="A4" style={{ backgroundColor: C.primaryDark, fontFamily: 'Helvetica' }}>
{bgImage && ( {bgImage && (
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}> <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
<Image src={bgImage} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> <Image src={bgImage} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.white, opacity: 0.82 }} /> <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.primaryDark, opacity: 0.85 }} />
</View> </View>
)} )}
<View style={{ position: 'absolute', top: 0, left: 0, width: 4, height: 280, backgroundColor: C.accent }} /> <View style={{ position: 'absolute', top: 0, left: 0, width: 6, height: 320, backgroundColor: C.accent }} />
<View style={{ flex: 1, paddingHorizontal: M.h }}> <View style={{ flex: 1, paddingHorizontal: M.h }}>
<View style={{ marginTop: 80 }}> <View style={{ marginTop: 80 }}>
{logoBlack ? <Image src={logoBlack} style={{ width: 140 }} /> : <Text style={{ fontSize: 28, fontWeight: 700, color: C.black }}>KLZ</Text>} {logo ? <Image src={logo} style={{ width: 140 }} /> : <Text style={{ fontSize: 28, fontWeight: 700, color: C.white }}>KLZ</Text>}
</View> </View>
<View style={{ marginTop: 160 }}> <View style={{ marginTop: 180 }}>
<View style={{ width: 48, height: 4, backgroundColor: C.accent, borderRadius: 2, marginBottom: S.lg }} /> <View style={{ width: 48, height: 4, backgroundColor: C.accent, borderRadius: 2, marginBottom: S.xl }} />
<Text style={{ fontSize: 42, fontWeight: 700, color: C.black, textTransform: 'uppercase', letterSpacing: 0.5, lineHeight: 1.1, marginBottom: S.md }}> <Text style={{ fontSize: 48, fontWeight: 700, color: C.white, textTransform: 'uppercase', letterSpacing: 0.5, lineHeight: 1.1, marginBottom: S.md }}>
{l.catalog} {l.catalog}
</Text> </Text>
<Text style={{ fontSize: 14, color: C.gray600, lineHeight: 1.6, maxWidth: 360 }}> <Text style={{ fontSize: 16, color: C.gray300, lineHeight: 1.6, maxWidth: 360 }}>
{introContent?.excerpt || l.subtitle} {introContent?.excerpt || l.subtitle}
</Text> </Text>
</View> </View>
<View style={{ position: 'absolute', bottom: S.xxl, left: M.h, right: M.h, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}> <View style={{ position: 'absolute', bottom: S.xxl, left: M.h, right: M.h, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={T.caption}>{l.edition} {dateStr}</Text> <Text style={{ fontSize: 10, color: C.gray400, textTransform: 'uppercase', letterSpacing: 1 }}>{l.edition} {dateStr}</Text>
<Text style={T.caption}>www.klz-cables.com</Text> <Text style={{ fontSize: 10, color: C.white, fontWeight: 700 }}>www.klz-cables.com</Text>
</View> </View>
</View> </View>
</Page> </Page>
); );
}; };
// ─── Info Flow (NO repeated background image — uses inline images) ────────── // ─── Info Flow ──────────────────────────────────────────────────────────────
const InfoFlow: React.FC<{ const InfoFlow: React.FC<{
locale: 'en' | 'de'; locale: 'en' | 'de';
companyInfo: BrochureProps['companyInfo']; companyInfo: BrochureProps['companyInfo'];
introContent?: BrochureProps['introContent'];
marketingSections?: BrochureProps['marketingSections']; marketingSections?: BrochureProps['marketingSections'];
logoBlack?: string | Buffer; logoBlack?: string | Buffer;
galleryImages?: Array<string | Buffer>; galleryImages?: Array<string | Buffer>;
}> = ({ locale, companyInfo, introContent, marketingSections, logoBlack, galleryImages }) => { }> = ({ locale, companyInfo, marketingSections, logoBlack, galleryImages }) => {
const l = L(locale); const l = L(locale);
return ( return (
<Page size="A4" wrap style={{ backgroundColor: C.white, fontFamily: 'Helvetica', paddingTop: PAGE_TOP_PADDING, paddingBottom: M.bottom }}> <Page size="A4" wrap style={{ backgroundColor: C.white, fontFamily: 'Helvetica', paddingTop: PAGE_TOP_PADDING, paddingBottom: M.bottom }}>
<FixedHeader logoBlack={logoBlack} rightText="KLZ Cables" /> <FixedHeader logoBlack={logoBlack} rightText="KLZ Cables" isDark={false} />
<Footer left="KLZ Cables" right={companyInfo.website} logoBlack={logoBlack} /> <Footer left="KLZ Cables" right={companyInfo.website} logoBlack={logoBlack} isDark={false} />
<View style={{ paddingHorizontal: M.h }}> <View style={{ paddingHorizontal: M.h }}>
{/* ── About KLZ ── */} {/* ── About KLZ ── */}
<View style={{ marginBottom: S.xl }}> <View style={{
<SectionHeading label={l.about} title="KLZ Cables" /> marginHorizontal: -M.h, paddingHorizontal: M.h,
paddingTop: S.xxl, paddingBottom: S.xl,
<RichText style={{ ...T.bodyLead, marginBottom: S.md }}> backgroundColor: C.white,
{companyInfo.tagline} }}>
</RichText> <View style={{ flexDirection: 'row', gap: S.xl }}>
<View style={{ flex: 1.5 }}>
{/* Inline image strip */} <SectionHeading label={l.about} title="KLZ Cables" isDark={false} />
{galleryImages?.[1] && <SectionImage src={galleryImages[1]} height={90} />} <RichText style={{ ...T.bodyLead(false), fontSize: 14, lineHeight: 1.6 }} paragraphGap={S.md} isDark={false}>
{companyInfo.tagline}
{/* Values in 2-col grid */} </RichText>
<Text style={{ ...T.label, marginBottom: S.sm }}>{l.values}</Text> </View>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: S.sm }}> <View style={{ flex: 1 }}>
{companyInfo.values.map((v, i) => ( {galleryImages?.[1] && (
<View key={i} style={{ width: '48%', marginBottom: S.xs, flexDirection: 'row', alignItems: 'flex-start', gap: S.sm }} minPresenceAhead={40}> <View style={{ width: '100%', height: 240, borderLeftWidth: 4, borderLeftColor: C.accent, borderLeftStyle: 'solid' }}>
<View style={{ <Image src={galleryImages[1]} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
width: 20, height: 20, borderRadius: 10,
backgroundColor: C.accent,
justifyContent: 'center', alignItems: 'center', flexShrink: 0, marginTop: 2,
}}>
<Text style={{ fontSize: 9, fontWeight: 700, color: C.white, textAlign: 'center' }}>0{i + 1}</Text>
</View> </View>
<View style={{ flex: 1 }}> )}
<Text style={{ ...T.bodyBold, marginBottom: 1 }}>{v.title}</Text> </View>
<Text style={{ ...T.body, fontSize: 9 }}>{v.description}</Text> </View>
<View style={{ marginTop: S.xxl }}>
<Text style={{ ...T.label(false), marginBottom: S.md }}>{l.values}</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: S.lg }}>
{companyInfo.values.map((v, i) => (
<View key={i} style={{ width: '47%', marginBottom: S.md }} minPresenceAhead={40}>
<Text style={{ ...T.bodyBold(false), fontSize: 11, marginBottom: 4 }}>
<Text style={{ color: C.accent }}>0{i + 1}</Text> {'\u00A0'} {v.title}
</Text>
<Text style={{ ...T.body(false), fontSize: 9 }}>{v.description}</Text>
</View> </View>
</View> ))}
))} </View>
</View> </View>
</View> </View>
<Divider />
{/* ── Marketing Sections ── */} {/* ── Marketing Sections ── */}
{marketingSections?.map((section, sIdx) => ( {marketingSections?.map((section, sIdx) => {
<View key={`m-${sIdx}`} style={{ marginBottom: S.xl }} minPresenceAhead={200}> const themes: Array<'white' | 'gray' | 'dark'> = ['gray', 'dark', 'white', 'gray', 'white', 'dark'];
<SectionHeading label={section.subtitle} title={section.title} /> const imagePositions: Array<'top' | 'bottom' | 'middle'> = ['bottom', 'top', 'bottom', 'middle', 'middle', 'top'];
const theme = themes[sIdx % themes.length];
const pos = imagePositions[sIdx % imagePositions.length];
const img = galleryImages?.[sIdx + 2];
{/* Description */} return (
{section.description && ( <MagazineSection
<RichText style={{ ...T.bodyLead }} paragraphGap={S.sm}> key={`m-${sIdx}`}
{section.description} section={section}
</RichText> image={img}
)} theme={theme}
imagePosition={pos}
{/* Pull quote */} />
{section.pullQuote && <PullQuote quote={section.pullQuote} />} );
})}
{/* Stat highlights */}
{section.highlights && section.highlights.length > 0 && (
<HighlightRow highlights={section.highlights} />
)}
{/* Inline image for visual break (different image per section) */}
{galleryImages && galleryImages[sIdx + 2] && sIdx < 3 && (
<SectionImage src={galleryImages[sIdx + 2]} height={80} />
)}
{/* Item list */}
{section.items && (
<View style={{ flexDirection: 'column', gap: S.md, marginTop: S.sm }}>
{section.items.map((item, i) => (
<View key={i} style={{ flexDirection: 'row', alignItems: 'flex-start', gap: S.sm }} minPresenceAhead={60}>
<View style={{ flexShrink: 0, marginTop: 5 }}>
<View style={{ width: 5, height: 5, borderRadius: 2.5, backgroundColor: C.accent }} />
</View>
<View style={{ flex: 1 }}>
<Text style={{ ...T.bodyBold, marginBottom: 2 }}>{item.title}</Text>
<Text style={{ ...T.body }}>{item.description}</Text>
</View>
</View>
))}
</View>
)}
{/* Divider between sections */}
{sIdx < (marketingSections?.length || 0) - 1 && <Divider />}
</View>
))}
</View> </View>
</Page> </Page>
); );
@@ -403,26 +444,30 @@ const TocPage: React.FC<{
<Footer left="KLZ Cables" right="www.klz-cables.com" logoBlack={logoBlack} /> <Footer left="KLZ Cables" right="www.klz-cables.com" logoBlack={logoBlack} />
<View style={{ paddingHorizontal: M.h }}> <View style={{ paddingHorizontal: M.h }}>
<SectionHeading label={l.catalog} title={l.toc} /> <SectionHeading label={l.catalog} title={l.toc} isDark={false} />
{/* Optional decorative image */} {/* Decorative image edge-to-edge */}
{galleryImages?.[5] && <SectionImage src={galleryImages[5]} height={70} />} {galleryImages?.[5] && (
<View style={{ marginHorizontal: -M.h, height: 160, marginBottom: S.xxl }}>
<Image src={galleryImages[5]} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</View>
)}
<View style={{ flexDirection: 'column' }}> <View style={{ flexDirection: 'column' }}>
{Array.from(grouped.entries()).map(([cat, items]) => ( {Array.from(grouped.entries()).map(([cat, items]) => (
<View key={cat} style={{ width: '100%', marginBottom: S.md }}> <View key={cat} style={{ width: '100%', marginBottom: S.md }}>
<View style={{ borderBottomWidth: 1, borderBottomColor: C.accent, borderBottomStyle: 'solid', paddingBottom: S.xs, marginBottom: S.sm }}> <View style={{ borderBottomWidth: 1.5, borderBottomColor: C.primaryDark, borderBottomStyle: 'solid', paddingBottom: S.xs, marginBottom: S.sm }}>
<Text style={{ ...T.label }}>{cat}</Text> <Text style={{ ...T.label(false) }}>{cat}</Text>
</View> </View>
{items.map((item, i) => ( {items.map((item, i) => (
<View key={i} style={{ <View key={i} style={{
flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between',
paddingVertical: 3, paddingVertical: 5,
borderBottomWidth: i < items.length - 1 ? 0.5 : 0, borderBottomWidth: i < items.length - 1 ? 0.5 : 0,
borderBottomColor: C.gray200, borderBottomStyle: 'solid', borderBottomColor: C.gray200, borderBottomStyle: 'solid',
}}> }}>
<Text style={{ fontSize: 10, fontWeight: 700, color: C.primaryDark }}>{item.product.name}</Text> <Text style={{ fontSize: 11, fontWeight: 700, color: C.primaryDark }}>{item.product.name}</Text>
<Text style={{ fontSize: 9, color: C.gray400 }}>{l.page} {item.pageNum}</Text> <Text style={{ fontSize: 10, color: C.gray400 }}>{l.page} {item.pageNum}</Text>
</View> </View>
))} ))}
</View> </View>
@@ -441,56 +486,64 @@ const ProductBlock: React.FC<{
}> = ({ product, locale }) => { }> = ({ product, locale }) => {
const l = L(locale); const l = L(locale);
const desc = stripHtml(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml); const desc = stripHtml(product.applicationHtml || product.shortDescriptionHtml || product.descriptionHtml);
const LABEL_WIDTH = 260; // Wider label container for technical data
return ( return (
<View> <View>
<SectionHeading <SectionHeading
label={product.categories.map(c => c.name).join(' · ')} label={product.categories.map(c => c.name).join(' · ')}
title={product.name} title={product.name}
isDark={false}
/> />
{/* Full-width product image */} {/* Edge-to-edge product image */}
<View style={{ <View style={{
width: '100%', height: 120, justifyContent: 'center', alignItems: 'center', marginHorizontal: -M.h,
borderRadius: 4, backgroundColor: C.gray050, height: 200, justifyContent: 'center', alignItems: 'center',
borderWidth: 0.5, borderColor: C.gray200, borderStyle: 'solid', backgroundColor: C.gray050,
overflow: 'hidden', padding: S.sm, marginBottom: S.lg, borderTopWidth: 0.5, borderTopColor: C.gray200, borderTopStyle: 'solid',
borderBottomWidth: 0.5, borderBottomColor: C.gray200, borderBottomStyle: 'solid',
marginBottom: S.xl, padding: S.lg
}}> }}>
{product.featuredImage ? ( {product.featuredImage ? (
<Image src={product.featuredImage} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> <Image src={product.featuredImage} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
) : ( ) : (
<Text style={{ fontSize: 8, color: C.gray400 }}>—</Text> <Text style={{ fontSize: 10, color: C.gray400 }}>—</Text>
)} )}
</View> </View>
{/* Description + QR */} {/* Description + QR */}
<View style={{ flexDirection: 'row', gap: S.xl, marginBottom: S.lg }}> <View style={{ flexDirection: 'row', gap: S.xl, marginBottom: S.xl }}>
<View style={{ flex: 1.6 }}> <View style={{ flex: 1.8 }}>
{desc && ( {desc && (
<View> <View>
<Text style={{ ...T.label, marginBottom: S.sm }}>{l.application}</Text> <Text style={{ ...T.label(false), marginBottom: S.md }}>{l.application}</Text>
<RichText style={{ ...T.body, lineHeight: 1.8 }}>{desc}</RichText> <RichText style={{ ...T.body(false), lineHeight: 1.8 }}>{desc}</RichText>
</View> </View>
)} )}
</View> </View>
<View style={{ flex: 1, flexDirection: 'column', justifyContent: 'flex-start' }}> <View style={{ flex: 1, flexDirection: 'column', justifyContent: 'flex-start' }}>
{(product.qrWebsite || product.qrDatasheet) && ( {(product.qrWebsite || product.qrDatasheet) && (
<View style={{ flexDirection: 'column', gap: S.sm }}> <View style={{ flexDirection: 'column', gap: S.md }}>
{product.qrWebsite && ( {product.qrWebsite && (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: S.sm }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: S.md }}>
<Image src={product.qrWebsite} style={{ width: 32, height: 32 }} /> <View style={{ padding: 4, borderWidth: 1, borderColor: C.gray200, borderStyle: 'solid' }}>
<Image src={product.qrWebsite} style={{ width: 44, height: 44 }} />
</View>
<View> <View>
<Text style={{ ...T.caption, fontWeight: 700, color: C.primaryDark, marginBottom: 1 }}>{l.qrWeb}</Text> <Text style={{ ...T.caption(false), fontWeight: 700, color: C.primaryDark, marginBottom: 2 }}>{l.qrWeb}</Text>
<Text style={{ fontSize: 7, color: C.gray400 }}>{locale === 'de' ? 'Produktseite' : 'Product Page'}</Text> <Text style={{ fontSize: 8, color: C.gray400 }}>{locale === 'de' ? 'Produktseite' : 'Product Page'}</Text>
</View> </View>
</View> </View>
)} )}
{product.qrDatasheet && ( {product.qrDatasheet && (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: S.sm }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: S.md }}>
<Image src={product.qrDatasheet} style={{ width: 32, height: 32 }} /> <View style={{ padding: 4, borderWidth: 1, borderColor: C.gray200, borderStyle: 'solid' }}>
<Image src={product.qrDatasheet} style={{ width: 44, height: 44 }} />
</View>
<View> <View>
<Text style={{ ...T.caption, fontWeight: 700, color: C.primaryDark, marginBottom: 1 }}>{l.qrPdf}</Text> <Text style={{ ...T.caption(false), fontWeight: 700, color: C.primaryDark, marginBottom: 2 }}>{l.qrPdf}</Text>
<Text style={{ fontSize: 7, color: C.gray400 }}>{locale === 'de' ? 'Datenblatt' : 'Datasheet'}</Text> <Text style={{ fontSize: 8, color: C.gray400 }}>{locale === 'de' ? 'Datenblatt' : 'Datasheet'}</Text>
</View> </View>
</View> </View>
)} )}
@@ -499,55 +552,48 @@ const ProductBlock: React.FC<{
</View> </View>
</View> </View>
{/* Technical Data Table — clean, minimal header */} {/* Technical Data Table — clean, minimal header, wider labels */}
{product.attributes && product.attributes.length > 0 && ( {product.attributes && product.attributes.length > 0 && (
<View> <View>
<Text style={{ ...T.label, marginBottom: S.sm }}>{l.specs}</Text> <Text style={{ ...T.label(false), marginBottom: S.sm }}>{l.specs}</Text>
<View style={{ <View style={{
borderWidth: 0.5, borderColor: C.gray200, borderStyle: 'solid', flexDirection: 'row',
borderRadius: 4, overflow: 'hidden', borderBottomWidth: 1.5, borderBottomColor: C.primaryDark, borderBottomStyle: 'solid',
paddingBottom: S.xs, marginBottom: 4,
}}> }}>
{/* Subtle header */} <View style={{ width: LABEL_WIDTH, paddingHorizontal: 10 }}>
<View style={{ <Text style={{ ...T.label(false), fontSize: 7, color: C.gray600 }}>
{locale === 'de' ? 'Eigenschaft' : 'Property'}
</Text>
</View>
<View style={{ flex: 1, paddingHorizontal: 10 }}>
<Text style={{ ...T.label(false), fontSize: 7, color: C.gray600 }}>
{locale === 'de' ? 'Wert' : 'Value'}
</Text>
</View>
</View>
{/* Rows */}
{product.attributes.map((attr, i) => (
<View key={i} style={{
flexDirection: 'row', flexDirection: 'row',
backgroundColor: C.gray050, borderBottomWidth: i < product.attributes.length - 1 ? 0.5 : 0,
borderBottomWidth: 1, borderBottomColor: C.accent, borderBottomStyle: 'solid', borderBottomColor: C.gray200, borderBottomStyle: 'solid',
backgroundColor: i % 2 === 0 ? C.white : C.gray050,
paddingVertical: 6,
}}> }}>
<View style={{ <View style={{
width: 200, paddingVertical: S.xs, paddingHorizontal: 10, width: LABEL_WIDTH, paddingHorizontal: 10,
borderRightWidth: 0.5, borderRightColor: C.gray200, borderRightStyle: 'solid' borderRightWidth: 1, borderRightColor: C.gray200, borderRightStyle: 'solid',
}}> }}>
<Text style={{ ...T.label, fontSize: 7, color: C.gray600 }}> <Text style={{ fontSize: 9, fontWeight: 700, color: C.primaryDark, letterSpacing: 0.2 }}>{attr.name}</Text>
{locale === 'de' ? 'Eigenschaft' : 'Property'}
</Text>
</View> </View>
<View style={{ flex: 1, paddingVertical: S.xs, paddingHorizontal: 10 }}> <View style={{ flex: 1, paddingHorizontal: 10, justifyContent: 'center' }}>
<Text style={{ ...T.label, fontSize: 7, color: C.gray600 }}> <Text style={{ fontSize: 10, color: C.gray900 }}>{attr.options.join(', ')}</Text>
{locale === 'de' ? 'Wert' : 'Value'}
</Text>
</View> </View>
</View> </View>
))}
{/* Rows */}
{product.attributes.map((attr, i) => (
<View key={i} style={{
flexDirection: 'row',
borderBottomWidth: i < product.attributes.length - 1 ? 0.5 : 0,
borderBottomColor: C.gray200, borderBottomStyle: 'solid',
backgroundColor: i % 2 === 0 ? C.white : C.gray050,
}}>
<View style={{
width: 200, paddingVertical: S.xs, paddingHorizontal: 10,
borderRightWidth: 0.5, borderRightColor: C.gray200, borderRightStyle: 'solid',
}}>
<Text style={{ fontSize: 8, fontWeight: 700, color: C.primaryDark, textTransform: 'uppercase', letterSpacing: 0.3 }}>{attr.name}</Text>
</View>
<View style={{ flex: 1, paddingVertical: S.xs, paddingHorizontal: 10, justifyContent: 'center' }}>
<Text style={{ fontSize: 9, color: C.gray900 }}>{attr.options.join(', ')}</Text>
</View>
</View>
))}
</View>
</View> </View>
)} )}
</View> </View>
@@ -582,38 +628,38 @@ const ProductsFlow: React.FC<{
const BackCoverPage: React.FC<{ const BackCoverPage: React.FC<{
companyInfo: BrochureProps['companyInfo']; companyInfo: BrochureProps['companyInfo'];
locale: 'en' | 'de'; locale: 'en' | 'de';
logoBlack?: string | Buffer; logoWhite?: string | Buffer;
galleryImages?: Array<string | Buffer>; galleryImages?: Array<string | Buffer>;
}> = ({ companyInfo, locale, logoBlack, galleryImages }) => { }> = ({ companyInfo, locale, logoWhite, galleryImages }) => {
const l = L(locale); const l = L(locale);
const bgImage = galleryImages?.[6]; const bgImage = galleryImages?.[6];
return ( return (
<Page size="A4" style={{ backgroundColor: C.white, fontFamily: 'Helvetica' }}> <Page size="A4" style={{ backgroundColor: C.primaryDark, fontFamily: 'Helvetica' }}>
{bgImage && ( {bgImage && (
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}> <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
<Image src={bgImage} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> <Image src={bgImage} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.white, opacity: 0.9 }} /> <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: C.primaryDark, opacity: 0.9 }} />
</View> </View>
)} )}
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: M.h }}> <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: M.h }}>
{logoBlack ? ( {logoWhite ? (
<Image src={logoBlack} style={{ width: 180, marginBottom: S.xl }} /> <Image src={logoWhite} style={{ width: 180, marginBottom: S.xl }} />
) : ( ) : (
<Text style={{ fontSize: 32, fontWeight: 700, color: C.black, letterSpacing: 2, textTransform: 'uppercase', marginBottom: S.xl }}>KLZ CABLES</Text> <Text style={{ fontSize: 32, fontWeight: 700, color: C.white, letterSpacing: 2, textTransform: 'uppercase', marginBottom: S.xl }}>KLZ CABLES</Text>
)} )}
<View style={{ width: 48, height: 4, backgroundColor: C.accent, borderRadius: 2, marginBottom: S.xl }} /> <View style={{ width: 48, height: 4, backgroundColor: C.accent, borderRadius: 2, marginBottom: S.xl }} />
<View style={{ alignItems: 'center', marginBottom: S.lg }}> <View style={{ alignItems: 'center', marginBottom: S.lg }}>
<Text style={{ ...T.label, marginBottom: S.sm }}>{l.contact}</Text> <Text style={{ ...T.label(true), marginBottom: S.sm }}>{l.contact}</Text>
<Text style={{ fontSize: 13, color: C.gray900, lineHeight: 1.7, textAlign: 'center' }}>{companyInfo.address}</Text> <Text style={{ fontSize: 13, color: C.white, lineHeight: 1.7, textAlign: 'center' }}>{companyInfo.address}</Text>
</View> </View>
<View style={{ alignItems: 'center', marginBottom: S.lg }}> <View style={{ alignItems: 'center', marginBottom: S.lg }}>
<Text style={{ fontSize: 13, color: C.gray900 }}>{companyInfo.phone}</Text> <Text style={{ fontSize: 13, color: C.white }}>{companyInfo.phone}</Text>
<Text style={{ fontSize: 13, color: C.gray900 }}>{companyInfo.email}</Text> <Text style={{ fontSize: 13, color: C.gray300 }}>{companyInfo.email}</Text>
</View> </View>
<Text style={{ fontSize: 14, fontWeight: 700, color: C.accent, marginTop: S.lg }}>{companyInfo.website}</Text> <Text style={{ fontSize: 14, fontWeight: 700, color: C.accent, marginTop: S.lg }}>{companyInfo.website}</Text>
@@ -630,20 +676,17 @@ const BackCoverPage: React.FC<{
export const PDFBrochure: React.FC<BrochureProps> = ({ export const PDFBrochure: React.FC<BrochureProps> = ({
products, locale, companyInfo, introContent, products, locale, companyInfo, introContent,
marketingSections, logoBlack, galleryImages, marketingSections, logoBlack, logoWhite, galleryImages,
}) => { }) => {
const productStartPage = 5; const productStartPage = 5;
return ( return (
<Document> <Document>
<CoverPage locale={locale} introContent={introContent} logoBlack={logoBlack} galleryImages={galleryImages} /> <CoverPage locale={locale} introContent={introContent} logoBlack={logoBlack} logoWhite={logoWhite} galleryImages={galleryImages} />
<InfoFlow locale={locale} companyInfo={companyInfo} introContent={introContent} <InfoFlow locale={locale} companyInfo={companyInfo} marketingSections={marketingSections} logoBlack={logoBlack} galleryImages={galleryImages} />
marketingSections={marketingSections} logoBlack={logoBlack} <TocPage products={products} locale={locale} logoBlack={logoBlack} productStartPage={productStartPage} galleryImages={galleryImages} />
galleryImages={galleryImages} />
<TocPage products={products} locale={locale} logoBlack={logoBlack}
productStartPage={productStartPage} galleryImages={galleryImages} />
<ProductsFlow products={products} locale={locale} logoBlack={logoBlack} /> <ProductsFlow products={products} locale={locale} logoBlack={logoBlack} />
<BackCoverPage companyInfo={companyInfo} locale={locale} logoBlack={logoBlack} galleryImages={galleryImages} /> <BackCoverPage companyInfo={companyInfo} locale={locale} logoWhite={logoWhite} galleryImages={galleryImages} />
</Document> </Document>
); );
}; };