Files
at-mintel/packages/pdf-library/src/components/pdf/SharedUI.tsx
Marc Mintel a4d021c658
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 13s
Monorepo Pipeline / 🏗️ Build (push) Failing after 11s
Monorepo Pipeline / 🧪 Test (push) Failing after 25s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
feat(pdf): rename acquisition-library to pdf-library and update package name to @mintel/pdf
2026-02-12 21:46:45 +01:00

729 lines
21 KiB
TypeScript

"use client";
import * as React from "react";
import {
View as PDFView,
Text as PDFText,
StyleSheet,
Image as PDFImage,
} from "@react-pdf/renderer";
// INDUSTRIAL DESIGN SYSTEM TOKENS
export const COLORS = {
CHARCOAL: "#0f172a", // Slate 900
TEXT_MAIN: "#334155", // Slate 700
TEXT_DIM: "#64748b", // Slate 500
TEXT_LIGHT: "#94a3b8", // Slate 400
DIVIDER: "#cbd5e1", // Slate 300
GRID: "#f1f5f9", // Slate 100
BLUEPRINT: "#e2e8f0", // Slate 200
WHITE: "#ffffff",
};
export const FONT_SIZES = {
HERO: 24, // Main Page Titles
HEADING: 14, // Section Headers
BODY: 11, // Standard Content
LABEL: 10, // Bold Labels / Keys
SMALL: 9, // Descriptions / Footnotes
TINY: 8, // Metadata / Unit prices
};
// Mintel Industrial Glyphs (strictly 1px stroke, 12x12px grid)
export const IndustrialGlyph = ({
type,
color = COLORS.TEXT_LIGHT,
size = 12,
}: {
type: string;
color?: string;
size?: number;
}) => {
const stroke = 1;
const scale = size / 12;
switch (type) {
case "base": // Skeletal cube base
return (
<PDFView style={{ width: size, height: size, position: "relative" }}>
<PDFView
style={{
position: "absolute",
top: 2 * scale,
left: 2 * scale,
width: 8 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
position: "absolute",
top: 0,
left: 0,
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
backgroundColor: "white",
}}
/>
</PDFView>
);
case "pages": // Layered rectangles
return (
<PDFView style={{ width: size, height: size, position: "relative" }}>
<PDFView
style={{
position: "absolute",
top: 3 * scale,
left: 3 * scale,
width: 6 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
position: "absolute",
top: 0,
left: 0,
width: 6 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
backgroundColor: "white",
}}
/>
</PDFView>
);
case "modules": // Four small squares grid
return (
<PDFView
style={{
width: size,
height: size,
flexDirection: "row",
flexWrap: "wrap",
gap: 2 * scale,
}}
>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
<PDFView
style={{
width: 4 * scale,
height: 4 * scale,
borderWidth: stroke,
borderColor: color,
}}
/>
</PDFView>
);
case "logic": // Diamond with center point
return (
<PDFView
style={{
width: size,
height: size,
alignItems: "center",
justifyContent: "center",
}}
>
<PDFView
style={{
width: 8 * scale,
height: 8 * scale,
borderWidth: stroke,
borderColor: color,
transform: "rotate(45deg)",
}}
/>
<PDFView
style={{
width: 2 * scale,
height: 2 * scale,
backgroundColor: color,
position: "absolute",
}}
/>
</PDFView>
);
case "interface": // Three horizontal lines of varying length
return (
<PDFView
style={{
width: size,
height: size,
justifyContent: "center",
gap: 2 * scale,
}}
>
<PDFView
style={{
width: 10 * scale,
height: stroke,
backgroundColor: color,
}}
/>
<PDFView
style={{ width: 6 * scale, height: stroke, backgroundColor: color }}
/>
<PDFView
style={{
width: 10 * scale,
height: stroke,
backgroundColor: color,
}}
/>
</PDFView>
);
case "management": // Framed grid
return (
<PDFView
style={{
width: size,
height: size,
borderWidth: stroke,
borderColor: color,
padding: 1 * scale,
}}
>
<PDFView
style={{
width: "100%",
height: 2 * scale,
backgroundColor: color,
marginBottom: 1 * scale,
}}
/>
<PDFView
style={{
width: "100%",
height: 2 * scale,
backgroundColor: color,
marginBottom: 1 * scale,
}}
/>
<PDFView
style={{ width: "100%", height: 2 * scale, backgroundColor: color }}
/>
</PDFView>
);
case "reveal": // Ascending bars
return (
<PDFView
style={{
width: size,
height: size,
flexDirection: "row",
alignItems: "flex-end",
gap: 1 * scale,
}}
>
<PDFView
style={{
width: 2 * scale,
height: 4 * scale,
backgroundColor: color,
opacity: 0.4,
}}
/>
<PDFView
style={{
width: 2 * scale,
height: 7 * scale,
backgroundColor: color,
opacity: 0.7,
}}
/>
<PDFView
style={{
width: 2 * scale,
height: 10 * scale,
backgroundColor: color,
}}
/>
</PDFView>
);
case "maintenance": // Circle with vertical notch
return (
<PDFView
style={{
width: size,
height: size,
borderRadius: 6 * scale,
borderWidth: stroke,
borderColor: color,
alignItems: "center",
}}
>
<PDFView
style={{
width: stroke,
height: 4 * scale,
backgroundColor: color,
marginTop: 1 * scale,
}}
/>
</PDFView>
);
default:
return (
<PDFView
style={{
width: size,
height: size,
borderWidth: stroke,
borderColor: COLORS.BLUEPRINT,
borderStyle: "dashed",
}}
/>
);
}
};
export const pdfStyles = StyleSheet.create({
page: {
paddingTop: 45, // DIN 5008
paddingLeft: 70, // ~25mm
paddingRight: 57, // ~20mm
paddingBottom: 80, // Safe buffer for absolute footer
backgroundColor: COLORS.WHITE,
fontFamily: "Helvetica",
fontSize: FONT_SIZES.BODY,
color: COLORS.CHARCOAL,
},
titlePage: {
width: "100%",
height: "100%",
backgroundColor: COLORS.WHITE,
fontFamily: "Helvetica",
color: COLORS.CHARCOAL,
padding: 0,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
marginBottom: 20,
minHeight: 120,
},
addressBlock: {
width: "55%",
marginTop: 45,
},
senderLine: {
fontSize: FONT_SIZES.TINY,
textDecoration: "underline",
color: COLORS.TEXT_DIM,
marginBottom: 8,
},
recipientAddress: {
fontSize: FONT_SIZES.BODY,
lineHeight: 1.4,
},
brandLogoContainer: {
width: "40%",
alignItems: "flex-end",
},
brandIconContainer: {
width: 40,
height: 40,
backgroundColor: "#0f172a",
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
marginBottom: 12,
},
brandIconText: {
color: COLORS.WHITE,
fontSize: 20,
fontWeight: "bold",
},
titleInfo: {
marginBottom: 24,
},
mainTitle: {
fontSize: FONT_SIZES.HEADING,
fontWeight: "bold",
marginBottom: 4,
color: COLORS.CHARCOAL,
letterSpacing: 0.5,
},
subTitle: {
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
marginTop: 2,
lineHeight: 1.4,
},
section: {
marginBottom: 32,
},
sectionTitle: {
fontSize: FONT_SIZES.LABEL,
fontWeight: "bold",
textTransform: "uppercase",
letterSpacing: 1,
color: COLORS.TEXT_LIGHT,
marginBottom: 8,
},
footer: {
position: "absolute",
bottom: 32,
left: 70,
right: 57,
borderTopWidth: 1,
borderTopColor: COLORS.GRID,
paddingTop: 16,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
},
footerColumn: {
flex: 1,
alignItems: "flex-start",
},
footerLogo: {
height: 20,
width: "auto",
objectFit: "contain",
marginBottom: 8,
},
footerText: {
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
lineHeight: 1.4,
},
asymmetryContainer: {
flexDirection: "row",
gap: 32,
},
asymmetryLeft: {
width: "32%",
},
asymmetryRight: {
width: "63%",
},
specRow: {
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 6,
borderBottomWidth: 1,
borderBottomColor: COLORS.GRID,
},
specLabel: {
fontSize: FONT_SIZES.TINY,
fontWeight: "bold",
color: COLORS.TEXT_LIGHT,
textTransform: "uppercase",
letterSpacing: 0.5,
},
specValue: {
fontSize: FONT_SIZES.SMALL,
color: COLORS.CHARCOAL,
fontWeight: "bold",
},
blueprintBox: {
borderWidth: 1,
borderColor: COLORS.GRID,
padding: 16,
backgroundColor: "#fafafa",
},
footerLabel: {
fontWeight: "bold",
color: COLORS.TEXT_DIM,
},
pageNumber: {
fontSize: FONT_SIZES.TINY,
color: COLORS.DIVIDER,
fontWeight: "bold",
marginTop: 8,
textAlign: "right",
},
foldingMark: {
position: "absolute",
left: 20,
width: 10,
borderTopWidth: 0.5,
borderTopColor: COLORS.DIVIDER,
},
divider: {
width: "100%",
height: 1,
backgroundColor: COLORS.DIVIDER,
marginVertical: 12,
},
industrialListItem: {
flexDirection: "row",
alignItems: "flex-start",
marginBottom: 6,
},
industrialBulletBox: {
width: 6,
height: 6,
backgroundColor: COLORS.DIVIDER,
marginRight: 8,
marginTop: 5,
},
industrialTitle: {
fontSize: FONT_SIZES.HERO,
fontWeight: "bold",
color: COLORS.CHARCOAL,
marginBottom: 6,
letterSpacing: 0,
},
});
export const IndustrialListItem = ({
children,
}: {
children: React.ReactNode;
}) => (
<PDFView style={pdfStyles.industrialListItem}>
<PDFView style={pdfStyles.industrialBulletBox} />
{children}
</PDFView>
);
export const Divider = ({ style = {} }: { style?: any }) => (
<PDFView style={[pdfStyles.divider, style]} />
);
export const FoldingMarks = () => (
<>
<PDFView style={[pdfStyles.foldingMark, { top: 297.6 }]} fixed />
<PDFView style={[pdfStyles.foldingMark, { top: 420.9, width: 15 }]} fixed />
<PDFView style={[pdfStyles.foldingMark, { top: 595.3 }]} fixed />
</>
);
export const Footer = ({
logo,
companyData,
bankData,
showDetails = true,
showPageNumber = true,
}: {
logo?: string;
companyData: any;
bankData?: any;
showDetails?: boolean;
showPageNumber?: boolean;
}) => (
<PDFView style={pdfStyles.footer}>
<PDFView style={pdfStyles.footerColumn}>
{logo ? (
<PDFImage src={logo} style={pdfStyles.footerLogo} />
) : (
<PDFText style={{ fontSize: 12, fontWeight: "bold", marginBottom: 8 }}>
marc mintel
</PDFText>
)}
</PDFView>
{showDetails && (
<>
<PDFView style={pdfStyles.footerColumn}>
<PDFText style={pdfStyles.footerText}>
<PDFText style={pdfStyles.footerLabel}>{companyData.name}</PDFText>
{"\n"}
{companyData.address1}
{"\n"}
{companyData.address2}
{"\n"}UST: {companyData.ustId}
</PDFText>
</PDFView>
<PDFView style={[pdfStyles.footerColumn, { alignItems: "flex-end" }]}>
{showPageNumber && (
<PDFText
style={pdfStyles.pageNumber}
render={({ pageNumber, totalPages }) =>
`${pageNumber} / ${totalPages}`
}
fixed
/>
)}
</PDFView>
</>
)}
{!showDetails && (
<PDFView style={[pdfStyles.footerColumn, { alignItems: "flex-end" }]}>
{showPageNumber && (
<PDFText
style={pdfStyles.pageNumber}
render={({ pageNumber, totalPages }) =>
`${pageNumber} / ${totalPages}`
}
fixed
/>
)}
</PDFView>
)}
</PDFView>
);
export const Header = ({
sender,
recipient,
icon,
showAddress = true,
}: {
sender?: string;
recipient?: {
title: string;
subtitle?: string;
email?: string;
address?: string;
phone?: string;
taxId?: string;
};
icon?: string;
showAddress?: boolean;
}) => (
<PDFView
style={[
pdfStyles.header,
showAddress ? {} : { minHeight: 40, marginBottom: 0 },
]}
>
<PDFView style={pdfStyles.addressBlock}>
{showAddress && sender && (
<>
<PDFText style={pdfStyles.senderLine}>{sender}</PDFText>
{recipient && (
<PDFView style={pdfStyles.recipientAddress}>
<PDFText style={{ fontWeight: "bold" }}>
{recipient.title}
</PDFText>
{recipient.subtitle && <PDFText>{recipient.subtitle}</PDFText>}
{recipient.address && <PDFText>{recipient.address}</PDFText>}
{recipient.phone && <PDFText>{recipient.phone}</PDFText>}
{recipient.email && <PDFText>{recipient.email}</PDFText>}
{recipient.taxId && <PDFText>USt-ID: {recipient.taxId}</PDFText>}
</PDFView>
)}
</>
)}
</PDFView>
<PDFView style={pdfStyles.brandLogoContainer}>
<PDFView style={pdfStyles.brandIconContainer}>
{icon ? (
<PDFImage src={icon} style={{ width: 24, height: 24 }} />
) : (
<PDFText style={pdfStyles.brandIconText}>M</PDFText>
)}
</PDFView>
</PDFView>
</PDFView>
);
export const DocumentTitle = ({
title,
subLines,
isHero = false,
}: {
title: string;
subLines?: string[];
isHero?: boolean;
}) => (
<PDFView style={pdfStyles.titleInfo}>
<PDFText
style={[
pdfStyles.mainTitle,
{ fontSize: isHero ? FONT_SIZES.HERO : FONT_SIZES.HEADING },
]}
>
{title}
</PDFText>
{subLines?.map((line, i) => (
<PDFText
key={i}
style={[
pdfStyles.subTitle,
i === 1 ? { fontWeight: "bold", color: COLORS.CHARCOAL } : {},
]}
>
{line}
</PDFText>
))}
</PDFView>
);
export const TechnicalSpec = ({
label,
value,
}: {
label: string;
value: string;
}) => (
<PDFView style={pdfStyles.specRow}>
<PDFText style={pdfStyles.specLabel}>{label}</PDFText>
<PDFText style={pdfStyles.specValue}>{value}</PDFText>
</PDFView>
);
export const AsymmetryView = ({
left,
right,
style = {},
}: {
left: React.ReactNode;
right: React.ReactNode;
style?: any;
}) => (
<PDFView style={[pdfStyles.asymmetryContainer, style]}>
<PDFView style={pdfStyles.asymmetryLeft}>{left}</PDFView>
<PDFView style={pdfStyles.asymmetryRight}>{right}</PDFView>
</PDFView>
);
export const IndustrialCard = ({
title,
children,
style = {},
}: {
title: string;
children: React.ReactNode;
style?: any;
}) => (
<PDFView style={[pdfStyles.blueprintBox, { marginBottom: 12 }, style]}>
<PDFText
style={{
fontSize: FONT_SIZES.TINY,
fontWeight: "bold",
color: COLORS.TEXT_LIGHT,
letterSpacing: 1,
marginBottom: 6,
textTransform: "uppercase",
}}
>
{title}
</PDFText>
{children}
</PDFView>
);