feat(pdf): roll out shared RTL layout to all templates

Introduce createRtlStyleHelpers and a single rtl flag on RenderProvider,
migrate every template page to mirrored layout styles, and rename
alignRight to alignEnd. Fix plain rich text rendering via PdfText
paragraph renderers and map legacy Times New Roman to Times-Roman.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Amruth Pillai
2026-05-25 16:29:50 +02:00
co-authored by Cursor
parent 86fff7237f
commit 24c882fa9f
28 changed files with 501 additions and 276 deletions
@@ -4,7 +4,6 @@ import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { isRTL } from "@reactive-resume/utils/locale";
import { useRender } from "../../context";
import { Image, Page, StyleSheet, View } from "../../renderer";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
@@ -14,6 +13,7 @@ import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { createRtlStyleHelpers } from "../shared/rtl";
import { Section } from "../shared/sections";
import { composeStyles, headerNameLineHeight } from "../shared/styles";
@@ -154,15 +154,16 @@ const Header = ({ styles }: { styles: RhyhornStyles }) => {
};
const useRhyhornTemplate = (): RhyhornTemplate => {
const { picture, metadata } = useRender();
const { picture, metadata, rtl } = useRender();
return useMemo(() => {
const r = createRtlStyleHelpers(rtl);
const foreground = rgbaStringToHex(metadata.design.colors.text);
const background = rgbaStringToHex(metadata.design.colors.background);
const primary = rgbaStringToHex(metadata.design.colors.primary);
const colors: TemplateColorRoles = { foreground, background, primary };
const metrics = getTemplateMetrics(metadata.page);
const rtl = isRTL(metadata.page.locale);
const contactGap = metrics.gapX(0.5);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
@@ -170,7 +171,7 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...(rtl ? { direction: "rtl" as const, textAlign: "right" as const } : {}),
...r.text,
} satisfies Style;
const baseStyles = StyleSheet.create({
@@ -183,7 +184,7 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
lineHeight: metadata.typography.body.lineHeight,
direction: rtl ? "rtl" : "ltr",
direction: r.pageDirection,
},
text: bodyText,
heading: {
@@ -192,10 +193,10 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
lineHeight: metadata.typography.heading.lineHeight,
color: foreground,
...(rtl ? { direction: "rtl" as const, textAlign: "right" as const } : {}),
...r.text,
},
div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) },
inline: { flexDirection: rtl ? "row-reverse" : "row", alignItems: "center", columnGap: metrics.gapX(1 / 3) },
inline: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3) },
link: { textDecoration: "none", color: foreground },
small: { fontSize: metadata.typography.body.fontSize * 0.875 },
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "600" },
@@ -210,62 +211,47 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
// bodyText spread first so `textAlign` below isn't clobbered by bodyText.textAlign.
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: rtl ? "left" : "right",
textAlign: r.listMarkerTextAlign,
},
richListItemContent: { flex: 1, ...bodyText },
splitRow: {
flexDirection: rtl ? "row-reverse" : "row",
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignRight: { textAlign: rtl ? "left" : "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 },
alignEnd: { ...r.alignEnd },
section: { flexDirection: "column", rowGap: metrics.gapY(0.25) },
sectionHeading: {
color: primary,
borderBottomWidth: 1,
borderBottomColor: primary,
textAlign: rtl ? "right" : "left",
textAlign: r.sectionHeadingTextAlign,
},
item: { rowGap: metrics.gapY(0.125) },
levelContainer: { width: "100%" },
levelItem: { borderColor: primary },
levelItemActive: { backgroundColor: primary },
header: { flexDirection: rtl ? "row-reverse" : "row", alignItems: "center", columnGap: metrics.gapX(0.5) },
header: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(0.5) },
headerTitle: { flex: 1, rowGap: metrics.gapY(0.5) },
headerIdentity: {
textAlign: rtl ? "right" : "left",
alignItems: rtl ? "flex-end" : "flex-start",
...r.headerIdentity,
rowGap: metrics.gapY(0.35),
},
headerName: { fontSize: metadata.typography.heading.fontSize * 1.5, lineHeight: headerNameLineHeight },
contactList: { flexDirection: rtl ? "row-reverse" : "row", flexWrap: "wrap", rowGap: metrics.gapY(0.125) },
contactList: { flexDirection: r.row, flexWrap: "wrap", rowGap: metrics.gapY(0.125) },
contactItem: {
flexDirection: rtl ? "row-reverse" : "row",
flexDirection: r.row,
alignItems: "center",
...(rtl
? {
borderLeftWidth: 1,
borderLeftColor: primary,
paddingLeft: metrics.gapX(0.5),
marginLeft: metrics.gapX(0.5),
}
: {
borderRightWidth: 1,
borderRightColor: primary,
paddingRight: metrics.gapX(0.5),
marginRight: metrics.gapX(0.5),
}),
...r.contactSeparator(primary, contactGap),
},
contactItemContent: {
flexDirection: rtl ? "row-reverse" : "row",
flexDirection: r.row,
alignItems: "center",
columnGap: metrics.gapX(1 / 6),
},
contactItemLast: rtl
? { borderLeftWidth: 0, paddingLeft: 0, marginLeft: 0 }
: { borderRightWidth: 0, paddingRight: 0, marginRight: 0 },
contactItemLast: r.contactSeparatorClear,
picture: {
width: picture.size,
height: picture.size,
@@ -297,5 +283,5 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
}),
} satisfies RhyhornStyles,
};
}, [picture, metadata]);
}, [picture, metadata, rtl]);
};