import * as React from 'react'; import { Document, Page, View, Text, Image, StyleSheet, Font, Link } from '@react-pdf/renderer'; // Register fonts (using system fonts for now, can be customized) Font.register({ family: 'Helvetica', fonts: [ { src: '/fonts/Helvetica.ttf', fontWeight: 400 }, { src: '/fonts/Helvetica-Bold.ttf', fontWeight: 700 }, ], }); // ─── Brand Tokens ──────────────────────────────────────── const C = { navy: '#001a4d', navyDeep: '#000d26', green: '#4da612', white: '#FFFFFF', offWhite: '#f8f9fa', gray200: '#e5e7eb', gray400: '#9ca3af', gray600: '#4b5563', gray900: '#111827', }; const MARGIN = 56; const HEADER_H = 52; const FOOTER_H = 48; const BODY_TOP = HEADER_H + 20; const styles = StyleSheet.create({ page: { color: C.gray900, lineHeight: 1.5, backgroundColor: C.white, paddingTop: BODY_TOP, paddingBottom: FOOTER_H + 40, paddingHorizontal: MARGIN, fontFamily: 'Helvetica', }, header: { position: 'absolute', top: 0, left: 0, right: 0, height: HEADER_H, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-end', paddingHorizontal: MARGIN, paddingBottom: 12, borderBottomWidth: 0, }, logoText: { fontSize: 16, fontWeight: 700, color: C.navy, }, docTitleLabel: { fontSize: 7, fontWeight: 700, color: C.gray400, letterSpacing: 1.2, textTransform: 'uppercase', }, footer: { position: 'absolute', bottom: 20, left: MARGIN, right: MARGIN, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderTopWidth: 0.5, borderTopColor: C.gray200, paddingTop: 8, }, footerText: { fontSize: 7, color: C.gray400, letterSpacing: 0.8, textTransform: 'uppercase', }, pageTitle: { fontSize: 24, fontWeight: 700, color: C.navyDeep, marginBottom: 16, textTransform: 'uppercase', letterSpacing: -0.5, }, accentBar: { width: 40, height: 3, backgroundColor: C.green, marginBottom: 24, }, // Lexical Elements paragraph: { fontSize: 10, color: C.gray600, lineHeight: 1.6, marginBottom: 12, }, heading1: { fontSize: 18, fontWeight: 700, color: C.navyDeep, marginTop: 16, marginBottom: 8, }, heading2: { fontSize: 14, fontWeight: 700, color: C.navyDeep, marginTop: 14, marginBottom: 6, }, heading3: { fontSize: 12, fontWeight: 700, color: C.navyDeep, marginTop: 12, marginBottom: 4, }, list: { marginBottom: 12, marginLeft: 8, }, listItem: { flexDirection: 'row', marginBottom: 4, }, listItemBullet: { width: 12, fontSize: 10, color: C.green, }, listItemContent: { flex: 1, fontSize: 10, color: C.gray600, lineHeight: 1.6, }, link: { color: C.green, textDecoration: 'none', }, textBold: { fontWeight: 700, fontFamily: 'Helvetica-Bold', color: C.navyDeep, }, textItalic: { fontStyle: 'italic', // Not actually working without proper font set, but we will fallback }, }); // ─── Lexical to React-PDF Renderer ──────────────────────────────── const renderLexicalNode = (node: any, idx: number): React.ReactNode => { if (!node) return null; switch (node.type) { case 'text': { const format = node.format || 0; const isBold = (format & 1) !== 0; const isItalic = (format & 2) !== 0; let elementStyle: any = {}; if (isBold) elementStyle = { ...elementStyle, ...styles.textBold }; if (isItalic) elementStyle = { ...elementStyle, ...styles.textItalic }; return ( {node.text} ); } case 'paragraph': { return ( {node.children?.map((child: any, i: number) => renderLexicalNode(child, i))} ); } case 'heading': { let hStyle = styles.heading3; if (node.tag === 'h1') hStyle = styles.heading1; if (node.tag === 'h2') hStyle = styles.heading2; return ( {node.children?.map((child: any, i: number) => renderLexicalNode(child, i))} ); } case 'list': { return ( {node.children?.map((child: any, i: number) => { if (child.type === 'listitem') { return ( {node.listType === 'number' ? `${i + 1}.` : '•'} {child.children?.map((c: any, ci: number) => renderLexicalNode(c, ci))} ); } return renderLexicalNode(child, i); })} ); } case 'link': { const href = node.fields?.url || node.url || '#'; return ( {node.children?.map((child: any, i: number) => renderLexicalNode(child, i))} ); } case 'linebreak': { // React-PDF doesn't handle `
`, but a newline char usually works inside ``. return {'\n'}; } // Ignore payload blocks recursively to avoid crashing, as pages should mainly use rich text case 'block': return null; default: if (node.children) { return ( {node.children.map((child: any, i: number) => renderLexicalNode(child, i))} ); } return null; } }; interface PDFPageProps { page: any; locale?: string; } export const PDFPage: React.FC = ({ page, locale = 'de' }) => { const dateStr = new Date().toLocaleDateString(locale === 'en' ? 'en-US' : 'de-DE', { year: 'numeric', month: 'long', day: 'numeric', }); return ( KLZ {locale === 'en' ? 'Document' : 'Dokument'} {page.title} {page.content?.root?.children?.map((node: any, i: number) => renderLexicalNode(node, i))} KLZ CABLES {dateStr} ); };