diff --git a/packages/fonts/src/index.test.ts b/packages/fonts/src/index.test.ts index d0521fc43..50a9ac234 100644 --- a/packages/fonts/src/index.test.ts +++ b/packages/fonts/src/index.test.ts @@ -261,7 +261,7 @@ describe("buildResumeFontFamily", () => { describe("legacy font compatibility (#2989)", () => { it.each([ - ["Times New Roman", "Tinos"], + ["Times New Roman", "Times-Roman"], ["Arial", "Arimo"], ["Garamond", "EB Garamond"], ["Calibri", "Carlito"], @@ -277,7 +277,7 @@ describe("legacy font compatibility (#2989)", () => { }); it("every alias target is actually registered as a known font", () => { - const aliasTargets = ["Tinos", "Arimo", "EB Garamond", "Carlito"]; + const aliasTargets = ["Times-Roman", "Tinos", "Arimo", "EB Garamond", "Carlito"]; for (const target of aliasTargets) { expect(getFont(target), `alias target ${target} must be a known font`).toBeDefined(); } @@ -286,7 +286,7 @@ describe("legacy font compatibility (#2989)", () => { it("getFont resolves a legacy family to its alias target", () => { const tnr = getFont("Times New Roman"); expect(tnr).toBeDefined(); - expect(tnr?.family).toBe("Tinos"); + expect(tnr?.family).toBe("Times-Roman"); }); it("getFont still returns the direct font when both legacy and direct lookup would succeed", () => { diff --git a/packages/fonts/src/index.ts b/packages/fonts/src/index.ts index 77c061716..b4d468e72 100644 --- a/packages/fonts/src/index.ts +++ b/packages/fonts/src/index.ts @@ -132,7 +132,7 @@ const legacyFontAliases: Record = { Cambria: "Tinos", Calibri: "Carlito", Garamond: "EB Garamond", - "Times New Roman": "Tinos", + "Times New Roman": "Times-Roman", }; export function resolveLegacyFontAlias(family: string): string | null { diff --git a/packages/pdf/src/context.tsx b/packages/pdf/src/context.tsx index f976b3e07..b409f933a 100644 --- a/packages/pdf/src/context.tsx +++ b/packages/pdf/src/context.tsx @@ -2,9 +2,11 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data"; import type { ReactNode } from "react"; import type { SectionTitleResolver } from "./section-title"; import { createContext, use } from "react"; +import { isRTL } from "@reactive-resume/utils/locale"; type RenderContextValue = ResumeData & { resolveSectionTitle?: SectionTitleResolver | undefined; + rtl: boolean; }; const RenderContext = createContext(null); @@ -16,7 +18,9 @@ export type RenderProviderProps = { }; export const RenderProvider = ({ data, resolveSectionTitle, children }: RenderProviderProps) => { - return {children}; + const rtl = isRTL(data.metadata.page.locale); + + return {children}; }; export const useRender = (): RenderContextValue => { diff --git a/packages/pdf/src/rtl-fixture.test.ts b/packages/pdf/src/rtl-fixture.test.ts new file mode 100644 index 000000000..8a86ecd19 --- /dev/null +++ b/packages/pdf/src/rtl-fixture.test.ts @@ -0,0 +1,27 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { templateSchema } from "@reactive-resume/schema/templates"; + +const templatePages = templateSchema.options.map( + (template) => + [ + template, + fileURLToPath(new URL(`./templates/${template}/${capitalize(template)}Page.tsx`, import.meta.url)), + ] as const, +); + +function capitalize(template: string): string { + return template.charAt(0).toUpperCase() + template.slice(1); +} + +describe("RTL PDF fixture", () => { + it.each(templatePages)("%s wires shared RTL helpers and alignEnd slot", (_template, pagePath) => { + const source = readFileSync(pagePath, "utf8"); + + expect(source).toContain("createRtlStyleHelpers"); + expect(source).toContain("alignEnd"); + expect(source).not.toContain("alignRight"); + expect(source).not.toContain('from "@reactive-resume/utils/locale"'); + }); +}); diff --git a/packages/pdf/src/templates/azurill/AzurillPage.tsx b/packages/pdf/src/templates/azurill/AzurillPage.tsx index 2a5877c5b..35881a470 100644 --- a/packages/pdf/src/templates/azurill/AzurillPage.tsx +++ b/packages/pdf/src/templates/azurill/AzurillPage.tsx @@ -18,6 +18,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, resolvePlacementColor } from "../shared/styles"; @@ -131,9 +132,10 @@ const Header = ({ styles }: { styles: AzurillStyles }) => { }; const useAzurillTemplate = (): AzurillTemplate => { - 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); @@ -146,6 +148,7 @@ const useAzurillTemplate = (): AzurillTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -160,6 +163,7 @@ const useAzurillTemplate = (): AzurillTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -168,13 +172,14 @@ const useAzurillTemplate = (): AzurillTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3), }, inline: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3), }, @@ -198,32 +203,29 @@ const useAzurillTemplate = (): AzurillTemplate => { alignItems: "flex-start", }, richListItemMarker: { - width: metadata.typography.body.fontSize, - textAlign: "right", ...bodyText, + width: metadata.typography.body.fontSize, + textAlign: r.listMarkerTextAlign, }, richListItemContent: { flex: 1, ...bodyText, }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { - textAlign: "right", - minWidth: 0, - maxWidth: "100%", - flexShrink: 1, + alignEnd: { + ...r.alignEnd, }, sectionHeading: { color: primary, }, contentRow: { - flexDirection: "row", + flexDirection: r.row, }, sidebarColumn: {}, mainColumn: { @@ -260,13 +262,13 @@ const useAzurillTemplate = (): AzurillTemplate => { }, headerContactRow: { justifyContent: "center", - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(0.5), }, headerContactItem: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6), }, @@ -355,5 +357,5 @@ const useAzurillTemplate = (): AzurillTemplate => { }), } satisfies AzurillStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/bronzor/BronzorPage.tsx b/packages/pdf/src/templates/bronzor/BronzorPage.tsx index bcd5f6658..dfe3cc1cc 100644 --- a/packages/pdf/src/templates/bronzor/BronzorPage.tsx +++ b/packages/pdf/src/templates/bronzor/BronzorPage.tsx @@ -12,6 +12,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"; @@ -128,9 +129,10 @@ const Header = ({ styles }: { styles: BronzorStyles }) => { }; const useBronzorTemplate = (): BronzorTemplate => { - 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); @@ -143,6 +145,7 @@ const useBronzorTemplate = (): BronzorTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -156,6 +159,7 @@ const useBronzorTemplate = (): BronzorTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -164,13 +168,14 @@ const useBronzorTemplate = (): BronzorTemplate => { fontWeight: metadata.typography.heading.fontWeights[0] ?? "500", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3), }, inline: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3), }, @@ -194,29 +199,26 @@ const useBronzorTemplate = (): BronzorTemplate => { alignItems: "flex-start", }, richListItemMarker: { - width: metadata.typography.body.fontSize, - textAlign: "right", ...bodyText, + width: metadata.typography.body.fontSize, + textAlign: r.listMarkerTextAlign, }, richListItemContent: { flex: 1, ...bodyText, }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { - textAlign: "right", - minWidth: 0, - maxWidth: "100%", - flexShrink: 1, + alignEnd: { + ...r.alignEnd, }, section: { - flexDirection: "row", + flexDirection: r.row, columnGap: metrics.columnGap, borderTopWidth: 1, borderTopColor: primary, @@ -227,6 +229,7 @@ const useBronzorTemplate = (): BronzorTemplate => { flexShrink: 0, fontSize: metadata.typography.heading.fontSize * 0.75, color: primary, + textAlign: r.sectionHeadingTextAlign, }, sectionItems: { flex: 1, @@ -265,13 +268,13 @@ const useBronzorTemplate = (): BronzorTemplate => { }, headerContactRow: { justifyContent: "center", - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(5 / 6), }, headerContactItem: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(0.25), }, @@ -283,5 +286,5 @@ const useBronzorTemplate = (): BronzorTemplate => { }); return { colors, styles: baseStyles satisfies BronzorStyles }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx b/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx index bcc0b1fa1..c8b6b455b 100644 --- a/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx +++ b/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx @@ -12,6 +12,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, resolvePlacementColor } from "../shared/styles"; @@ -139,9 +140,10 @@ const Header = ({ styles }: { styles: ChikoritaStyles }) => { }; const useChikoritaTemplate = (): ChikoritaTemplate => { - 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); @@ -160,16 +162,18 @@ const useChikoritaTemplate = (): ChikoritaTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ page: { - flexDirection: "row", + flexDirection: r.row, color: foreground, backgroundColor: background, fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -178,13 +182,14 @@ const useChikoritaTemplate = (): ChikoritaTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3), }, inline: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(0.25), }, @@ -208,26 +213,23 @@ const useChikoritaTemplate = (): ChikoritaTemplate => { alignItems: "flex-start", }, richListItemMarker: { - width: metadata.typography.body.fontSize, - textAlign: "right", ...bodyText, + width: metadata.typography.body.fontSize, + textAlign: r.listMarkerTextAlign, }, richListItemContent: { flex: 1, ...bodyText, }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { - textAlign: "right", - minWidth: 0, - maxWidth: "100%", - flexShrink: 1, + alignEnd: { + ...r.alignEnd, }, section: { flexDirection: "column", @@ -256,7 +258,7 @@ const useChikoritaTemplate = (): ChikoritaTemplate => { backgroundColor: primary, }, header: { - flexDirection: "row", + flexDirection: r.row, alignItems: "flex-start", columnGap: metrics.gapX(0.5), }, @@ -277,8 +279,7 @@ const useChikoritaTemplate = (): ChikoritaTemplate => { rowGap: metrics.gapY(0.5), }, headerIdentity: { - textAlign: "left", - alignItems: "flex-start", + ...r.headerIdentity, rowGap: metrics.gapY(0.35), }, headerName: { @@ -289,13 +290,13 @@ const useChikoritaTemplate = (): ChikoritaTemplate => { rowGap: metrics.gapY(0.125), }, headerContactRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", columnGap: metrics.gapX(2 / 3), rowGap: metrics.gapY(0.125), }, headerContactItem: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6), }, @@ -330,8 +331,8 @@ const useChikoritaTemplate = (): ChikoritaTemplate => { ? { flexDirection: "column", alignItems: "flex-start", justifyContent: "flex-start" } : {}), }), - alignRight: (context) => ({ - ...baseStyles.alignRight, + alignEnd: (context) => ({ + ...baseStyles.alignEnd, ...(context.placement === "sidebar" ? { textAlign: "left" } : {}), }), sectionHeading: (context) => ({ @@ -348,5 +349,5 @@ const useChikoritaTemplate = (): ChikoritaTemplate => { }), } satisfies ChikoritaStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/ditgar/DitgarPage.tsx b/packages/pdf/src/templates/ditgar/DitgarPage.tsx index 5dd25e60b..d54aa8822 100644 --- a/packages/pdf/src/templates/ditgar/DitgarPage.tsx +++ b/packages/pdf/src/templates/ditgar/DitgarPage.tsx @@ -13,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, resolvePlacementColor } from "../shared/styles"; @@ -166,9 +167,10 @@ const getPrimaryTint = (primaryColor: string, opacity: number): string => { }; const useDitgarTemplate = (): DitgarTemplate => { - 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); @@ -188,16 +190,18 @@ const useDitgarTemplate = (): DitgarTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ page: { - flexDirection: "row", + flexDirection: r.row, color: foreground, backgroundColor: background, fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -206,24 +210,25 @@ const useDitgarTemplate = (): DitgarTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) }, - inline: { flexDirection: "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" }, richParagraph: { margin: 0, ...bodyText }, richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" }, - richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText }, + richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign }, richListItemContent: { flex: 1, ...bodyText }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 }, + alignEnd: { ...r.alignEnd }, section: { flexDirection: "column", rowGap: metrics.gapY(0.25) }, sectionHeading: { fontSize: metadata.typography.heading.fontSize * 0.9, @@ -277,8 +282,7 @@ const useDitgarTemplate = (): DitgarTemplate => { }, headerTitle: {}, headerIdentity: { - textAlign: "left", - alignItems: "flex-start", + ...r.headerIdentity, rowGap: metrics.gapY(0.35), }, headerName: { @@ -289,7 +293,7 @@ const useDitgarTemplate = (): DitgarTemplate => { headerText: { color: background }, contactList: { rowGap: metrics.gapY(0.125) }, contactItem: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6), }, @@ -314,8 +318,8 @@ const useDitgarTemplate = (): DitgarTemplate => { richParagraph: (context) => ({ ...baseStyles.richParagraph, color: foregroundFor(context) }), richListItemMarker: (context) => ({ ...baseStyles.richListItemMarker, color: foregroundFor(context) }), richListItemContent: (context) => ({ ...baseStyles.richListItemContent, color: foregroundFor(context) }), - alignRight: (context) => ({ - ...baseStyles.alignRight, + alignEnd: (context) => ({ + ...baseStyles.alignEnd, ...(context.placement === "sidebar" ? { textAlign: "left" } : {}), }), sectionHeading: (context) => ({ @@ -343,5 +347,5 @@ const useDitgarTemplate = (): DitgarTemplate => { }), } satisfies DitgarStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/ditto/DittoPage.tsx b/packages/pdf/src/templates/ditto/DittoPage.tsx index d6fb5b1ee..b234dd862 100644 --- a/packages/pdf/src/templates/ditto/DittoPage.tsx +++ b/packages/pdf/src/templates/ditto/DittoPage.tsx @@ -12,6 +12,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"; @@ -140,9 +141,10 @@ const Header = ({ styles }: { styles: DittoStyles }) => { }; const useDittoTemplate = (): DittoTemplate => { - 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); @@ -156,6 +158,7 @@ const useDittoTemplate = (): DittoTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -166,6 +169,7 @@ const useDittoTemplate = (): DittoTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -174,13 +178,14 @@ const useDittoTemplate = (): DittoTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3), }, inline: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3), }, @@ -204,26 +209,23 @@ const useDittoTemplate = (): DittoTemplate => { alignItems: "flex-start", }, richListItemMarker: { - width: metadata.typography.body.fontSize, - textAlign: "right", ...bodyText, + width: metadata.typography.body.fontSize, + textAlign: r.listMarkerTextAlign, }, richListItemContent: { flex: 1, ...bodyText, }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { - textAlign: "right", - minWidth: 0, - maxWidth: "100%", - flexShrink: 1, + alignEnd: { + ...r.alignEnd, }, section: { flexDirection: "column", @@ -250,7 +252,7 @@ const useDittoTemplate = (): DittoTemplate => { headerBand: { backgroundColor: primary, color: background, - flexDirection: "row", + flexDirection: r.row, ...(hasPicture ? { minHeight: picture.size * 0.6 } : {}), }, pictureAnchor: { @@ -285,8 +287,7 @@ const useDittoTemplate = (): DittoTemplate => { color: background, }, headerIdentity: { - textAlign: "left", - alignItems: "flex-start", + ...r.headerIdentity, rowGap: metrics.gapY(0.35), }, headerName: { @@ -298,7 +299,7 @@ const useDittoTemplate = (): DittoTemplate => { color: background, }, contactRow: { - flexDirection: "row", + flexDirection: r.row, alignItems: "flex-start", }, contactOffset: { @@ -307,7 +308,7 @@ const useDittoTemplate = (): DittoTemplate => { }, contactList: { flex: 1, - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", columnGap: metrics.gapX(2 / 3), rowGap: metrics.gapY(0.125), @@ -317,12 +318,12 @@ const useDittoTemplate = (): DittoTemplate => { paddingBottom: 0, }, contactItem: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6), }, contentRow: { - flexDirection: "row", + flexDirection: r.row, }, sidebarColumn: { flexShrink: 0, @@ -350,8 +351,8 @@ const useDittoTemplate = (): DittoTemplate => { ? { flexDirection: "column", alignItems: "flex-start", justifyContent: "flex-start" } : {}), }), - alignRight: (context) => ({ - ...baseStyles.alignRight, + alignEnd: (context) => ({ + ...baseStyles.alignEnd, ...(context.placement === "sidebar" ? { textAlign: "left" } : {}), }), icon: { @@ -361,5 +362,5 @@ const useDittoTemplate = (): DittoTemplate => { }, } satisfies DittoStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/gengar/GengarPage.tsx b/packages/pdf/src/templates/gengar/GengarPage.tsx index ca693c4aa..e2bd2f3bd 100644 --- a/packages/pdf/src/templates/gengar/GengarPage.tsx +++ b/packages/pdf/src/templates/gengar/GengarPage.tsx @@ -13,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, resolvePlacementColor } from "../shared/styles"; @@ -163,9 +164,10 @@ const getPrimaryTint = (primaryColor: string, opacity: number): string => { }; const useGengarTemplate = (): GengarTemplate => { - 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); @@ -185,16 +187,18 @@ const useGengarTemplate = (): GengarTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ page: { - flexDirection: "row", + flexDirection: r.row, color: foreground, backgroundColor: background, fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -203,13 +207,14 @@ const useGengarTemplate = (): GengarTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3), }, inline: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3), }, @@ -233,26 +238,23 @@ const useGengarTemplate = (): GengarTemplate => { alignItems: "flex-start", }, richListItemMarker: { - width: metadata.typography.body.fontSize, - textAlign: "right", ...bodyText, + width: metadata.typography.body.fontSize, + textAlign: r.listMarkerTextAlign, }, richListItemContent: { flex: 1, ...bodyText, }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { - textAlign: "right", - minWidth: 0, - maxWidth: "100%", - flexShrink: 1, + alignEnd: { + ...r.alignEnd, }, section: { flexDirection: "column", @@ -321,8 +323,7 @@ const useGengarTemplate = (): GengarTemplate => { }, headerTitle: {}, headerIdentity: { - textAlign: "left", - alignItems: "flex-start", + ...r.headerIdentity, rowGap: metrics.gapY(0.35), }, headerName: { @@ -337,7 +338,7 @@ const useGengarTemplate = (): GengarTemplate => { rowGap: metrics.gapY(0.25), }, contactItem: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6), }, @@ -368,8 +369,8 @@ const useGengarTemplate = (): GengarTemplate => { ? { flexDirection: "column", alignItems: "flex-start", justifyContent: "flex-start" } : {}), }), - alignRight: (context) => ({ - ...baseStyles.alignRight, + alignEnd: (context) => ({ + ...baseStyles.alignEnd, ...(context.placement === "sidebar" ? { textAlign: "left" } : {}), }), sectionHeading: (context) => ({ @@ -386,5 +387,5 @@ const useGengarTemplate = (): GengarTemplate => { }), } satisfies GengarStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/glalie/GlaliePage.tsx b/packages/pdf/src/templates/glalie/GlaliePage.tsx index 8235a9762..99cda99b3 100644 --- a/packages/pdf/src/templates/glalie/GlaliePage.tsx +++ b/packages/pdf/src/templates/glalie/GlaliePage.tsx @@ -12,6 +12,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, resolvePlacementColor } from "../shared/styles"; @@ -144,9 +145,10 @@ const getPrimaryTint = (primaryColor: string, opacity: number): string => { }; const useGlalieTemplate = (): GlalieTemplate => { - 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); @@ -166,6 +168,7 @@ const useGlalieTemplate = (): GlalieTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -175,6 +178,7 @@ const useGlalieTemplate = (): GlalieTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -183,24 +187,25 @@ const useGlalieTemplate = (): GlalieTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) }, - inline: { flexDirection: "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" }, richParagraph: { margin: 0, ...bodyText }, richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" }, - richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText }, + richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign }, richListItemContent: { flex: 1, ...bodyText }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 }, + alignEnd: { ...r.alignEnd }, section: { flexDirection: "column", rowGap: metrics.gapY(0.25) }, sectionHeading: { borderBottomWidth: 1, borderBottomColor: primary }, item: { rowGap: metrics.gapY(0.125) }, @@ -211,11 +216,11 @@ const useGlalieTemplate = (): GlalieTemplate => { position: "absolute", top: 0, bottom: 0, - left: 0, + ...r.anchorToStart(0), width: `${metadata.layout.sidebarWidth}%`, backgroundColor: primaryTint, }, - layout: { flexDirection: "row", minHeight: "100%" }, + layout: { flexDirection: r.row, minHeight: "100%" }, sidebarColumn: { zIndex: 1, backgroundColor: primaryTint, @@ -256,7 +261,7 @@ const useGlalieTemplate = (): GlalieTemplate => { padding: metrics.gapX(0.75), rowGap: metrics.gapY(0.125), }, - contactItem: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 6) }, + contactItem: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6) }, }); const accentFor = ({ colors }: TemplateStyleContext) => colors.primary; @@ -284,5 +289,5 @@ const useGlalieTemplate = (): GlalieTemplate => { }), } satisfies GlalieStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/kakuna/KakunaPage.tsx b/packages/pdf/src/templates/kakuna/KakunaPage.tsx index d4bae2dde..57aff7589 100644 --- a/packages/pdf/src/templates/kakuna/KakunaPage.tsx +++ b/packages/pdf/src/templates/kakuna/KakunaPage.tsx @@ -12,6 +12,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"; @@ -111,9 +112,10 @@ const Header = ({ styles }: { styles: KakunaStyles }) => { }; const useKakunaTemplate = (): KakunaTemplate => { - 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); @@ -126,6 +128,7 @@ const useKakunaTemplate = (): KakunaTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -138,6 +141,7 @@ const useKakunaTemplate = (): KakunaTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -146,13 +150,14 @@ const useKakunaTemplate = (): KakunaTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3), }, inline: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 3), }, @@ -176,26 +181,23 @@ const useKakunaTemplate = (): KakunaTemplate => { alignItems: "flex-start", }, richListItemMarker: { - width: metadata.typography.body.fontSize, - textAlign: "right", ...bodyText, + width: metadata.typography.body.fontSize, + textAlign: r.listMarkerTextAlign, }, richListItemContent: { flex: 1, ...bodyText, }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { - textAlign: "right", - minWidth: 0, - maxWidth: "100%", - flexShrink: 1, + alignEnd: { + ...r.alignEnd, }, section: { flexDirection: "column", @@ -261,14 +263,14 @@ const useKakunaTemplate = (): KakunaTemplate => { }, contactList: { width: "100%", - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", justifyContent: "center", rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(0.75), }, contactItem: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6), }, @@ -295,5 +297,5 @@ const useKakunaTemplate = (): KakunaTemplate => { }), } satisfies KakunaStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/lapras/LaprasPage.tsx b/packages/pdf/src/templates/lapras/LaprasPage.tsx index 52c8954aa..51f770ae8 100644 --- a/packages/pdf/src/templates/lapras/LaprasPage.tsx +++ b/packages/pdf/src/templates/lapras/LaprasPage.tsx @@ -12,6 +12,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"; @@ -110,9 +111,10 @@ const Header = ({ styles }: { styles: LaprasStyles }) => { }; const useLaprasTemplate = (): LaprasTemplate => { - 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); @@ -128,6 +130,7 @@ const useLaprasTemplate = (): LaprasTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -140,6 +143,7 @@ const useLaprasTemplate = (): LaprasTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -148,24 +152,25 @@ const useLaprasTemplate = (): LaprasTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) }, - inline: { flexDirection: "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" }, richParagraph: { margin: 0, ...bodyText }, richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" }, - richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText }, + richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign }, richListItemContent: { flex: 1, ...bodyText }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 }, + alignEnd: { ...r.alignEnd }, section: { flexDirection: "column", rowGap: metrics.gapY(0.25), @@ -186,7 +191,7 @@ const useLaprasTemplate = (): LaprasTemplate => { levelItem: { borderColor: primary }, levelItemActive: { backgroundColor: primary }, header: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1), borderWidth: 1, @@ -208,15 +213,15 @@ const useLaprasTemplate = (): LaprasTemplate => { transform: `rotate(${picture.rotation}deg)`, }, headerTitle: { rowGap: metrics.gapY(0.5) }, - headerIdentity: { textAlign: "left", alignItems: "flex-start", rowGap: metrics.gapY(0.35) }, + headerIdentity: { ...r.headerIdentity, rowGap: metrics.gapY(0.35) }, headerName: { fontSize: metadata.typography.heading.fontSize * 1.5, lineHeight: headerNameLineHeight }, contactList: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(0.5), }, - contactItem: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 6) }, + contactItem: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6) }, sectionGroup: {}, }); @@ -235,5 +240,5 @@ const useLaprasTemplate = (): LaprasTemplate => { }), } satisfies LaprasStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/leafish/LeafishPage.tsx b/packages/pdf/src/templates/leafish/LeafishPage.tsx index c33f1ae0d..1786832e1 100644 --- a/packages/pdf/src/templates/leafish/LeafishPage.tsx +++ b/packages/pdf/src/templates/leafish/LeafishPage.tsx @@ -12,6 +12,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"; @@ -140,9 +141,10 @@ const getPrimaryAlpha = (primaryColor: string, opacity: number): string => { }; const useLeafishTemplate = (): LeafishTemplate => { - 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); @@ -157,6 +159,7 @@ const useLeafishTemplate = (): LeafishTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -166,6 +169,7 @@ const useLeafishTemplate = (): LeafishTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -174,24 +178,25 @@ const useLeafishTemplate = (): LeafishTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) }, - inline: { flexDirection: "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" }, richParagraph: { margin: 0, ...bodyText }, richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" }, - richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText }, + richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign }, richListItemContent: { flex: 1, ...bodyText }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 }, + alignEnd: { ...r.alignEnd }, section: { flexDirection: "column", rowGap: metrics.gapY(0.25) }, sectionHeading: { borderBottomWidth: 1, borderBottomColor: primary }, item: { rowGap: metrics.gapY(0.125) }, @@ -205,7 +210,7 @@ const useLeafishTemplate = (): LeafishTemplate => { paddingVertical: metrics.page.paddingVertical, }, headerBody: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1), }, @@ -213,7 +218,7 @@ const useLeafishTemplate = (): LeafishTemplate => { flex: 1, rowGap: metrics.gapY(0.5), }, - headerIdentity: { textAlign: "left", alignItems: "flex-start", rowGap: metrics.gapY(0.35) }, + headerIdentity: { ...r.headerIdentity, rowGap: metrics.gapY(0.35) }, headerName: { fontSize: metadata.typography.heading.fontSize * 1.5, lineHeight: headerNameLineHeight }, headerContactBand: { backgroundColor: primaryTintDark, @@ -221,13 +226,13 @@ const useLeafishTemplate = (): LeafishTemplate => { paddingVertical: metrics.page.paddingVertical, }, contactList: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1), }, contactItem: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6), }, @@ -244,7 +249,7 @@ const useLeafishTemplate = (): LeafishTemplate => { transform: `rotate(${picture.rotation}deg)`, }, body: { - flexDirection: "row", + flexDirection: r.row, columnGap: metrics.columnGap, paddingHorizontal: metrics.page.paddingHorizontal, paddingTop: metrics.page.paddingVertical, @@ -269,5 +274,5 @@ const useLeafishTemplate = (): LeafishTemplate => { }), } satisfies LeafishStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/meowth/MeowthPage.tsx b/packages/pdf/src/templates/meowth/MeowthPage.tsx index 74b59e2c1..189c5b252 100644 --- a/packages/pdf/src/templates/meowth/MeowthPage.tsx +++ b/packages/pdf/src/templates/meowth/MeowthPage.tsx @@ -12,6 +12,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"; @@ -115,9 +116,10 @@ const Header = ({ styles }: { styles: MeowthStyles }) => { }; const useMeowthTemplate = (): MeowthTemplate => { - 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); @@ -129,6 +131,7 @@ const useMeowthTemplate = (): MeowthTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -141,6 +144,7 @@ const useMeowthTemplate = (): MeowthTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -149,25 +153,26 @@ const useMeowthTemplate = (): MeowthTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) }, - inline: { flexDirection: "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" }, richParagraph: { margin: 0, ...bodyText }, richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" }, - richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText }, + richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign }, richListItemContent: { flex: 1, ...bodyText }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 }, - inlineItemHeader: { flexDirection: "row", alignItems: "flex-start", columnGap: metrics.gapX(0.75) }, + alignEnd: { ...r.alignEnd }, + inlineItemHeader: { flexDirection: r.row, alignItems: "flex-start", columnGap: metrics.gapX(0.75) }, inlineItemHeaderLeading: { flex: 1, minWidth: 0 }, inlineItemHeaderMiddle: { flex: 1, minWidth: 0 }, inlineItemHeaderTrailing: { flexShrink: 0, textAlign: "right" }, @@ -179,23 +184,24 @@ const useMeowthTemplate = (): MeowthTemplate => { borderBottomWidth: 1, borderBottomColor: primary, paddingBottom: metrics.gapY(0.125), + textAlign: r.sectionHeadingTextAlign, }, item: { rowGap: metrics.gapY(0.125) }, levelContainer: { width: "100%" }, levelItem: { borderColor: primary }, levelItemActive: { backgroundColor: primary }, - header: { flexDirection: "row", alignItems: "flex-start", columnGap: metrics.gapX(1) }, + header: { flexDirection: r.row, alignItems: "flex-start", columnGap: metrics.gapX(1) }, headerTitle: { flex: 1, rowGap: metrics.gapY(0.5) }, - headerIdentity: { textAlign: "left", alignItems: "flex-start", rowGap: metrics.gapY(0.35) }, + headerIdentity: { ...r.headerIdentity, rowGap: metrics.gapY(0.35) }, headerName: { fontSize: metadata.typography.heading.fontSize * 1.5, lineHeight: headerNameLineHeight }, headerHeadline: { opacity: 0.8 }, contactList: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(0.75), }, - contactItem: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 6) }, + contactItem: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6) }, picture: { width: picture.size, height: picture.size, @@ -231,5 +237,5 @@ const useMeowthTemplate = (): MeowthTemplate => { }), } satisfies MeowthStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/onyx/OnyxPage.tsx b/packages/pdf/src/templates/onyx/OnyxPage.tsx index f3675e00a..ca2ba69d8 100644 --- a/packages/pdf/src/templates/onyx/OnyxPage.tsx +++ b/packages/pdf/src/templates/onyx/OnyxPage.tsx @@ -12,6 +12,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"; @@ -110,9 +111,10 @@ const Header = ({ styles }: { styles: OnyxStyles }) => { }; const useOnyxTemplate = (): OnyxTemplate => { - 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); @@ -124,6 +126,7 @@ const useOnyxTemplate = (): OnyxTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -136,6 +139,7 @@ const useOnyxTemplate = (): OnyxTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -144,31 +148,32 @@ const useOnyxTemplate = (): OnyxTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) }, - inline: { flexDirection: "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" }, richParagraph: { margin: 0, ...bodyText }, richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" }, - richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText }, + richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign }, richListItemContent: { flex: 1, ...bodyText }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 }, + alignEnd: { ...r.alignEnd }, section: { flexDirection: "column", rowGap: metrics.gapY(0.25) }, item: { rowGap: metrics.gapY(0.125) }, levelContainer: { width: "100%" }, levelItem: { borderColor: primary }, levelItemActive: { backgroundColor: primary }, header: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1), borderBottomWidth: 1, @@ -188,15 +193,15 @@ const useOnyxTemplate = (): OnyxTemplate => { transform: `rotate(${picture.rotation}deg)`, }, headerTitle: { rowGap: metrics.gapY(0.5) }, - headerIdentity: { textAlign: "left", alignItems: "flex-start", rowGap: metrics.gapY(0.35) }, + headerIdentity: { ...r.headerIdentity, rowGap: metrics.gapY(0.35) }, headerName: { fontSize: metadata.typography.heading.fontSize * 1.5, lineHeight: headerNameLineHeight }, contactList: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(0.75), }, - contactItem: { flexDirection: "row", alignItems: "center", columnGap: metrics.gapX(1 / 6) }, + contactItem: { flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6) }, sectionGroup: {}, }); @@ -215,5 +220,5 @@ const useOnyxTemplate = (): OnyxTemplate => { }), } satisfies OnyxStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/pikachu/PikachuPage.tsx b/packages/pdf/src/templates/pikachu/PikachuPage.tsx index ba8ec8714..ac1811aed 100644 --- a/packages/pdf/src/templates/pikachu/PikachuPage.tsx +++ b/packages/pdf/src/templates/pikachu/PikachuPage.tsx @@ -12,6 +12,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, resolvePlacementColor } from "../shared/styles"; @@ -147,9 +148,10 @@ const Header = ({ styles, colors }: { styles: PikachuStyles; colors: TemplateCol }; const usePikachuTemplate = (): PikachuTemplate => { - 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); @@ -162,6 +164,7 @@ const usePikachuTemplate = (): PikachuTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -173,6 +176,7 @@ const usePikachuTemplate = (): PikachuTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -181,24 +185,25 @@ const usePikachuTemplate = (): PikachuTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) }, - inline: { flexDirection: "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" }, richParagraph: { margin: 0, ...bodyText }, richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" }, - richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText }, + richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign }, richListItemContent: { flex: 1, ...bodyText }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 }, + alignEnd: { ...r.alignEnd }, section: { flexDirection: "column", rowGap: metrics.gapY(0.25) }, sectionHeading: { borderBottomWidth: 1, borderBottomColor: primary }, item: { rowGap: metrics.gapY(0.125) }, @@ -206,7 +211,7 @@ const usePikachuTemplate = (): PikachuTemplate => { levelItem: { borderColor: primary }, levelItemActive: { backgroundColor: primary }, layout: { - flexDirection: "row", + flexDirection: r.row, columnGap: metrics.columnGap, }, sidebarColumn: { @@ -217,7 +222,7 @@ const usePikachuTemplate = (): PikachuTemplate => { flex: 1, }, headerRow: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1), }, @@ -237,8 +242,7 @@ const usePikachuTemplate = (): PikachuTemplate => { paddingBottom: metrics.gapY(0.5), }, headerIdentity: { - textAlign: "left", - alignItems: "flex-start", + ...r.headerIdentity, rowGap: metrics.gapY(0.35), }, headerName: { @@ -248,13 +252,13 @@ const usePikachuTemplate = (): PikachuTemplate => { }, headerText: { color: background }, contactList: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(0.75), }, contactItem: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6), }, @@ -297,5 +301,5 @@ const usePikachuTemplate = (): PikachuTemplate => { }), } satisfies PikachuStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx b/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx index 2d99a29ac..128df5d6b 100644 --- a/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx +++ b/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx @@ -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]); }; diff --git a/packages/pdf/src/templates/scizor/ScizorPage.tsx b/packages/pdf/src/templates/scizor/ScizorPage.tsx index af4d99dfc..8e940f489 100644 --- a/packages/pdf/src/templates/scizor/ScizorPage.tsx +++ b/packages/pdf/src/templates/scizor/ScizorPage.tsx @@ -12,6 +12,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"; @@ -103,9 +104,10 @@ const Header = ({ styles }: { styles: ScizorStyles }) => { }; const useScizorTemplate = (): ScizorTemplate => { - 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); @@ -118,6 +120,7 @@ const useScizorTemplate = (): ScizorTemplate => { fontWeight: metadata.typography.body.fontWeights[0] ?? "400", lineHeight: metadata.typography.body.lineHeight, color: foreground, + ...r.text, } satisfies Style; const baseStyles = StyleSheet.create({ @@ -132,6 +135,7 @@ const useScizorTemplate = (): ScizorTemplate => { fontFamily: metadata.typography.body.fontFamily, fontSize: metadata.typography.body.fontSize, lineHeight: metadata.typography.body.lineHeight, + direction: r.pageDirection, }, text: bodyText, heading: { @@ -140,24 +144,25 @@ const useScizorTemplate = (): ScizorTemplate => { fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "700", lineHeight: metadata.typography.heading.lineHeight, color: foreground, + ...r.text, }, div: { rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(1 / 3) }, - inline: { flexDirection: "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) ?? "700", color: foreground }, richParagraph: { margin: 0, ...bodyText }, richListItemRow: { flexDirection: "row", columnGap: metrics.gapX(1 / 3), alignItems: "flex-start" }, - richListItemMarker: { width: metadata.typography.body.fontSize, textAlign: "right", ...bodyText }, + richListItemMarker: { ...bodyText, width: metadata.typography.body.fontSize, textAlign: r.listMarkerTextAlign }, richListItemContent: { flex: 1, ...bodyText }, splitRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between", columnGap: metrics.gapX(2 / 3), }, - alignRight: { textAlign: "right", minWidth: 0, maxWidth: "100%", flexShrink: 1 }, + alignEnd: { ...r.alignEnd }, section: { flexDirection: "column", rowGap: metrics.gapY(0.25), @@ -177,12 +182,12 @@ const useScizorTemplate = (): ScizorTemplate => { levelItem: { borderColor: primary }, levelItemActive: { backgroundColor: primary }, header: { - flexDirection: "row", + flexDirection: r.row, alignItems: "flex-start", columnGap: metrics.gapX(1), paddingBottom: metrics.gapY(0.35), }, - headerIdentity: { flex: 1, alignItems: "flex-start", rowGap: metrics.gapY(0.45) }, + headerIdentity: { flex: 1, ...r.headerIdentity, rowGap: metrics.gapY(0.45) }, headerName: { color: foreground, fontSize: metadata.typography.heading.fontSize * 1.85, @@ -195,13 +200,13 @@ const useScizorTemplate = (): ScizorTemplate => { }, headerHeadline: { color: foreground }, headerContactRow: { - flexDirection: "row", + flexDirection: r.row, flexWrap: "wrap", rowGap: metrics.gapY(0.125), columnGap: metrics.gapX(0.55), }, headerContactItem: { - flexDirection: "row", + flexDirection: r.row, alignItems: "center", columnGap: metrics.gapX(1 / 6), color: foreground, @@ -240,5 +245,5 @@ const useScizorTemplate = (): ScizorTemplate => { }), } satisfies ScizorStyles, }; - }, [picture, metadata]); + }, [picture, metadata, rtl]); }; diff --git a/packages/pdf/src/templates/shared/rich-text-renderers.ts b/packages/pdf/src/templates/shared/rich-text-renderers.ts new file mode 100644 index 000000000..e8a438115 --- /dev/null +++ b/packages/pdf/src/templates/shared/rich-text-renderers.ts @@ -0,0 +1,49 @@ +import type { Style } from "@react-pdf/types"; +import type { ReactNode } from "react"; +import { createElement } from "react"; +import { Text as PdfText } from "../../renderer"; +import { + getRichTextEdgeTrimStyle, + isRichTextElementInsideListItem, + stripRichTextVerticalMargins, +} from "./rich-text-spacing"; +import { composeStyles } from "./styles"; + +export const toRichTextStyleArray = (style: Style | Style[] | undefined): Style[] => { + if (!style) return []; + if (Array.isArray(style)) return style.filter(Boolean); + + return [style]; +}; + +type RichTextParagraphRendererProps = { + children: ReactNode; + element: Parameters[0]; + style: Style | Style[] | undefined; + rtl?: boolean; + rtlTextWrapStyle?: Style | undefined; + applyRtlDirection?: (node: ReactNode) => ReactNode; +}; + +export const renderRichTextParagraph = ({ + element, + style, + children, + rtl, + rtlTextWrapStyle, + applyRtlDirection, +}: RichTextParagraphRendererProps) => { + const paragraphStyles = isRichTextElementInsideListItem(element) + ? toRichTextStyleArray(style).map(stripRichTextVerticalMargins) + : style; + + const composedStyle = composeStyles( + paragraphStyles, + getRichTextEdgeTrimStyle(element), + rtl ? rtlTextWrapStyle : undefined, + ); + + const content = rtl && applyRtlDirection ? applyRtlDirection(children) : children; + + return createElement(PdfText, { style: composedStyle }, content); +}; diff --git a/packages/pdf/src/templates/shared/rich-text.test.ts b/packages/pdf/src/templates/shared/rich-text.test.ts index f6d7995d5..849861487 100644 --- a/packages/pdf/src/templates/shared/rich-text.test.ts +++ b/packages/pdf/src/templates/shared/rich-text.test.ts @@ -1,5 +1,14 @@ +import type { ReactElement } from "react"; import { describe, expect, it } from "vitest"; +import { Text as PdfText } from "@react-pdf/renderer"; +import { parse } from "node-html-parser"; +import { createElement } from "react"; import { normalizeRichTextHtml } from "./rich-text-html"; +import { renderRichTextParagraph } from "./rich-text-renderers"; + +type PdfElement = ReactElement<{ children?: unknown; style?: unknown }>; + +const getPdfElementProps = (element: unknown) => (element as PdfElement).props; describe("normalizeRichTextHtml", () => { it("wraps top-level inline rich text in a paragraph", () => { @@ -23,3 +32,21 @@ describe("normalizeRichTextHtml", () => { ); }); }); + +describe("renderRichTextParagraph", () => { + it("keeps unmarked paragraph text inside a PDF text node", () => { + const paragraph = parse("

Plain bold text

").querySelector("p"); + + if (!paragraph) throw new Error("Expected paragraph to exist."); + + const rendered = renderRichTextParagraph({ + element: paragraph, + style: { fontSize: 10 }, + children: ["Plain ", createElement(PdfText, { key: "bold" }, "bold"), " text"], + }); + const props = getPdfElementProps(rendered); + + expect(rendered.type).toBe(PdfText); + expect(props.children).toEqual(["Plain ", expect.any(Object), " text"]); + }); +}); diff --git a/packages/pdf/src/templates/shared/rich-text.tsx b/packages/pdf/src/templates/shared/rich-text.tsx index 43ffc88fc..b8bff8240 100644 --- a/packages/pdf/src/templates/shared/rich-text.tsx +++ b/packages/pdf/src/templates/shared/rich-text.tsx @@ -2,16 +2,15 @@ import type { Style } from "@react-pdf/types"; import type { ReactElement, ReactNode } from "react"; import { cloneElement, isValidElement } from "react"; import { Html } from "react-pdf-html"; -import { isRTL } from "@reactive-resume/utils/locale"; import { useRender } from "../../context"; import { Text as PdfText, View } from "../../renderer"; import { useTemplateStyle } from "./context"; import { safeTextStyle } from "./primitives"; import { convertPseudoBulletParagraphs, normalizeRichTextHtml, richTextMarkClassName } from "./rich-text-html"; +import { renderRichTextParagraph, toRichTextStyleArray } from "./rich-text-renderers"; import { createRichTextProseSpacing, getRichTextEdgeTrimStyle, - isRichTextElementInsideListItem, isRichTextElementInsideOrderedList, resolveRichTextBodyLineHeight, stripRichTextVerticalMargins, @@ -26,13 +25,6 @@ const richMarkStyle = { backgroundColor: "#ffff00", } satisfies Style; -const toStyleArray = (style: Style | Style[] | undefined): Style[] => { - if (!style) return []; - if (Array.isArray(style)) return style.filter(Boolean); - - return [style]; -}; - // react-pdf textkit reads BiDi base direction from each run's own `direction` attribute // (default "ltr"), and react-pdf-html buckets inline content into styleless inner // frames — so the rtl style has to be injected onto every descendant, not just a wrapper. @@ -65,8 +57,7 @@ const applyRtlDirectionRecursively = (node: ReactNode): ReactNode => { }; export const RichText = ({ children }: { children: string }) => { - const data = useRender(); - const rtl = isRTL(data.metadata.page.locale); + const { rtl } = useRender(); const rtlTextWrapStyle: Style | undefined = rtl ? { direction: "rtl", textAlign: "right" } : undefined; const boldStyle = useTemplateStyle("bold"); const linkStyle = useTemplateStyle("link"); @@ -95,25 +86,20 @@ export const RichText = ({ children }: { children: string }) => { resetStyles renderers={{ b: ({ children }) => {children}, - p: ({ element, style, children }) => { - const paragraphStyles = isRichTextElementInsideListItem(element) - ? toStyleArray(style).map(stripRichTextVerticalMargins) - : style; + p: (props) => { + const paragraphProps = { + ...props, + rtl, + ...(rtlTextWrapStyle ? { rtlTextWrapStyle } : {}), + ...(rtl ? { applyRtlDirection: applyRtlDirectionRecursively } : {}), + }; - if (rtl) { - return ( - - {applyRtlDirectionRecursively(children)} - - ); - } - - return {children}; + return renderRichTextParagraph(paragraphProps); }, li: ({ element, style, children }) => { const isOrderedList = isRichTextElementInsideOrderedList(element); const marker = isOrderedList ? `${element.indexOfType + 1}.` : "•"; - const itemStyles = toStyleArray(style); + const itemStyles = toRichTextStyleArray(style); const contentItemStyles = itemStyles.map(stripRichTextVerticalMargins); const markerNode = ( diff --git a/packages/pdf/src/templates/shared/rtl.test.ts b/packages/pdf/src/templates/shared/rtl.test.ts new file mode 100644 index 000000000..aa0410160 --- /dev/null +++ b/packages/pdf/src/templates/shared/rtl.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { createRtlStyleHelpers } from "./rtl"; + +describe("createRtlStyleHelpers", () => { + it("returns LTR defaults when rtl is false", () => { + const r = createRtlStyleHelpers(false); + + expect(r.pageDirection).toBe("ltr"); + expect(r.row).toBe("row"); + expect(r.text).toEqual({}); + expect(r.alignEnd.textAlign).toBe("right"); + expect(r.gridRowStyle).toBeUndefined(); + expect(r.anchorToStart()).toEqual({ left: 0, right: undefined }); + }); + + it("returns RTL mirrors when rtl is true", () => { + const r = createRtlStyleHelpers(true); + + expect(r.pageDirection).toBe("rtl"); + expect(r.row).toBe("row-reverse"); + expect(r.text).toEqual({ direction: "rtl", textAlign: "right" }); + expect(r.alignEnd.textAlign).toBe("left"); + expect(r.gridRowStyle).toEqual({ flexDirection: "row-reverse" }); + expect(r.anchorToStart()).toEqual({ right: 0, left: undefined }); + }); + + it("mirrors contact separator borders", () => { + const ltr = createRtlStyleHelpers(false).contactSeparator("#000", 4); + const rtl = createRtlStyleHelpers(true).contactSeparator("#000", 4); + + expect(ltr).toMatchObject({ borderRightWidth: 1, paddingRight: 4, marginRight: 4 }); + expect(rtl).toMatchObject({ borderLeftWidth: 1, paddingLeft: 4, marginLeft: 4 }); + }); +}); diff --git a/packages/pdf/src/templates/shared/rtl.ts b/packages/pdf/src/templates/shared/rtl.ts new file mode 100644 index 000000000..0a065d1e5 --- /dev/null +++ b/packages/pdf/src/templates/shared/rtl.ts @@ -0,0 +1,55 @@ +import type { Style } from "@react-pdf/types"; + +export type RtlStyleHelpers = { + rtl: boolean; + pageDirection: "ltr" | "rtl"; + row: "row" | "row-reverse"; + text: Pick; + alignEnd: Pick; + sectionHeadingTextAlign: NonNullable; + headerIdentity: Pick; + listMarkerTextAlign: NonNullable; + gridRowStyle: Style | undefined; + contactSeparator: (color: string, gap: number) => Style; + contactSeparatorClear: Style; + anchorToStart: (offset?: number | string) => Style; +}; + +export function createRtlStyleHelpers(rtl: boolean): RtlStyleHelpers { + return { + rtl, + pageDirection: rtl ? "rtl" : "ltr", + row: rtl ? "row-reverse" : "row", + text: rtl ? { direction: "rtl", textAlign: "right" } : {}, + alignEnd: { + textAlign: rtl ? "left" : "right", + minWidth: 0, + maxWidth: "100%", + flexShrink: 1, + }, + sectionHeadingTextAlign: rtl ? "right" : "left", + headerIdentity: rtl + ? { textAlign: "right", alignItems: "flex-end" } + : { textAlign: "left", alignItems: "flex-start" }, + listMarkerTextAlign: rtl ? "left" : "right", + gridRowStyle: rtl ? { flexDirection: "row-reverse" } : undefined, + contactSeparator: (color, gap) => + rtl + ? { + borderLeftWidth: 1, + borderLeftColor: color, + paddingLeft: gap, + marginLeft: gap, + } + : { + borderRightWidth: 1, + borderRightColor: color, + paddingRight: gap, + marginRight: gap, + }, + contactSeparatorClear: rtl + ? { borderLeftWidth: 0, paddingLeft: 0, marginLeft: 0 } + : { borderRightWidth: 0, paddingRight: 0, marginRight: 0 }, + anchorToStart: (offset = 0) => (rtl ? { right: offset } : { left: offset }), + }; +} diff --git a/packages/pdf/src/templates/shared/sections.tsx b/packages/pdf/src/templates/shared/sections.tsx index 71e4836dd..245f0ade3 100644 --- a/packages/pdf/src/templates/shared/sections.tsx +++ b/packages/pdf/src/templates/shared/sections.tsx @@ -21,7 +21,6 @@ import type { StyleInput, TemplatePlacement } from "./styles"; import type { CustomItemSection, ItemSection } from "./types"; import { Children, createContext, isValidElement, use } from "react"; import { match } from "ts-pattern"; -import { isRTL } from "@reactive-resume/utils/locale"; import { useRender } from "../../context"; import { View } from "../../renderer"; import { getResumeSectionTitle } from "../../section-title"; @@ -39,6 +38,7 @@ import { LevelDisplay } from "./level-display"; import { getTemplateMetrics } from "./metrics"; import { Bold, Div, Heading, Icon, Link, Small, Text } from "./primitives"; import { RichText } from "./rich-text"; +import { createRtlStyleHelpers } from "./rtl"; import { getInlineItemWebsiteUrl, shouldRenderSeparateItemWebsite } from "./section-links"; import { hasSplitRowText, promoteSplitRowRight } from "./split-row"; import { composeStyles } from "./styles"; @@ -118,8 +118,7 @@ const SectionItems = ({ children, columns = 1 }: { children: ReactNode; columns? columns: layout.columns, }); const context = { itemStyle: layout.itemStyle, useTimeline }; - const rtl = isRTL(data.metadata.page.locale); - const rtlRowStyle = rtl ? { flexDirection: "row-reverse" as const } : undefined; + const rtlRowStyle = createRtlStyleHelpers(data.rtl).gridRowStyle; if (!useTimeline) { if (layout.isGrid) { @@ -340,7 +339,7 @@ const ExperienceSection = ({ const experience = sectionData ?? data.sections.experience; const items = getVisibleItems(experience, "experience"); const splitRowStyle = useSectionSplitRowStyle(); - const alignRightStyle = useTemplateStyle("alignRight"); + const alignEndStyle = useTemplateStyle("alignEnd"); const inlineItemHeader = useTemplateFeature("inlineItemHeader"); if (items.length === 0) return null; @@ -368,7 +367,7 @@ const ExperienceSection = ({ ) : null } middle={{item.company}} - trailing={{item.period}} + trailing={{item.period}} /> ); @@ -376,15 +375,13 @@ const ExperienceSection = ({ <> {item.company} - {hasSplitRowText(headerLocation) && ( - {headerLocation} - )} + {hasSplitRowText(headerLocation) && {headerLocation}} {item.roles.length === 0 && (hasPosition || hasSplitRowText(headerPeriod)) && ( {hasPosition && {item.position}} - {hasSplitRowText(headerPeriod) && {headerPeriod}} + {hasSplitRowText(headerPeriod) && {headerPeriod}} )} @@ -400,7 +397,7 @@ const ExperienceSection = ({ {role.position} - {role.period} + {role.period} {role.description} @@ -428,7 +425,7 @@ const EducationSection = ({ const education = sectionData ?? data.sections.education; const items = getVisibleItems(education, "education"); const splitRowStyle = useSectionSplitRowStyle(); - const alignRightStyle = useTemplateStyle("alignRight"); + const alignEndStyle = useTemplateStyle("alignEnd"); const inlineItemHeader = useTemplateFeature("inlineItemHeader"); if (items.length === 0) return null; @@ -460,7 +457,7 @@ const EducationSection = ({ ) : null } middle={{item.school}} - trailing={{item.period}} + trailing={{item.period}} /> {gradeAndLocation && {gradeAndLocation}} @@ -471,7 +468,7 @@ const EducationSection = ({ {item.school} {hasSplitRowText(headerDegreeAndGrade) && ( - {headerDegreeAndGrade} + {headerDegreeAndGrade} )} @@ -479,7 +476,7 @@ const EducationSection = ({ {hasArea && {item.area}} {hasSplitRowText(headerLocationAndPeriod) && ( - {headerLocationAndPeriod} + {headerLocationAndPeriod} )} )} @@ -512,7 +509,7 @@ const ProjectsSection = ({ const projects = sectionData ?? data.sections.projects; const items = getVisibleItems(projects, "projects"); const splitRowStyle = useSectionSplitRowStyle(); - const alignRightStyle = useTemplateStyle("alignRight"); + const alignEndStyle = useTemplateStyle("alignEnd"); if (items.length === 0) return null; @@ -524,7 +521,7 @@ const ProjectsSection = ({ {item.name} - {item.period} + {item.period} @@ -653,7 +650,7 @@ const AwardsSection = ({ const awards = sectionData ?? data.sections.awards; const items = getVisibleItems(awards, "awards"); const splitRowStyle = useTemplateStyle("splitRow"); - const alignRightStyle = useTemplateStyle("alignRight"); + const alignEndStyle = useTemplateStyle("alignEnd"); if (items.length === 0) return null; @@ -665,7 +662,7 @@ const AwardsSection = ({ {item.title} - {item.date} + {item.date} {item.awarder} @@ -690,7 +687,7 @@ const CertificationsSection = ({ const certifications = sectionData ?? data.sections.certifications; const items = getVisibleItems(certifications, "certifications"); const splitRowStyle = useSectionSplitRowStyle(); - const alignRightStyle = useTemplateStyle("alignRight"); + const alignEndStyle = useTemplateStyle("alignEnd"); if (items.length === 0) return null; @@ -702,7 +699,7 @@ const CertificationsSection = ({ {item.title} - {item.date} + {item.date} {item.issuer} @@ -728,7 +725,7 @@ const PublicationsSection = ({ const publications = sectionData ?? data.sections.publications; const items = getVisibleItems(publications, "publications"); const splitRowStyle = useSectionSplitRowStyle(); - const alignRightStyle = useTemplateStyle("alignRight"); + const alignEndStyle = useTemplateStyle("alignEnd"); if (items.length === 0) return null; @@ -740,7 +737,7 @@ const PublicationsSection = ({ {item.title} - {item.date} + {item.date} {item.publisher} @@ -767,7 +764,7 @@ const VolunteerSection = ({ const volunteer = sectionData ?? data.sections.volunteer; const items = getVisibleItems(volunteer, "volunteer"); const splitRowStyle = useSectionSplitRowStyle(); - const alignRightStyle = useTemplateStyle("alignRight"); + const alignEndStyle = useTemplateStyle("alignEnd"); const inlineItemHeader = useTemplateFeature("inlineItemHeader"); if (items.length === 0) return null; @@ -783,15 +780,13 @@ const VolunteerSection = ({ {item.location} : null} middle={{item.organization}} - trailing={{item.period}} + trailing={{item.period}} /> ) : ( <> {item.organization} - {hasSplitRowText(item.period) && ( - {item.period} - )} + {hasSplitRowText(item.period) && {item.period}} {item.location} diff --git a/packages/pdf/src/templates/shared/types.ts b/packages/pdf/src/templates/shared/types.ts index 715559673..c3d2185dd 100644 --- a/packages/pdf/src/templates/shared/types.ts +++ b/packages/pdf/src/templates/shared/types.ts @@ -58,7 +58,7 @@ export type TemplateStyleSlots = { richListItemMarker?: TemplateStyleSlot; richListItemContent?: TemplateStyleSlot; splitRow?: TemplateStyleSlot; - alignRight?: TemplateStyleSlot; + alignEnd?: TemplateStyleSlot; inlineItemHeader?: TemplateStyleSlot; inlineItemHeaderLeading?: TemplateStyleSlot; inlineItemHeaderMiddle?: TemplateStyleSlot; diff --git a/packages/utils/src/locale.test.ts b/packages/utils/src/locale.test.ts index e1f7d5cea..33c2c5c93 100644 --- a/packages/utils/src/locale.test.ts +++ b/packages/utils/src/locale.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { defaultLocale, isLocale } from "./locale"; +import { defaultLocale, isLocale, isRTL } from "./locale"; describe("defaultLocale", () => { it("is en-US", () => { @@ -40,3 +40,16 @@ describe("isLocale", () => { expect(isLocale([])).toBe(false); }); }); + +describe("isRTL", () => { + it.each([ + ["ar-SA", true], + ["he-IL", true], + ["fa-IR", true], + ["en-US", false], + ["en-GB", false], + ["fr-FR", false], + ])("returns %s → %s", (locale, expected) => { + expect(isRTL(locale)).toBe(expected); + }); +}); diff --git a/packages/utils/src/locale.ts b/packages/utils/src/locale.ts index 860a17ca0..e187fc846 100644 --- a/packages/utils/src/locale.ts +++ b/packages/utils/src/locale.ts @@ -84,6 +84,6 @@ const RTL_LANGUAGES = new Set([ ]); export function isRTL(locale: string): boolean { - const language = locale.split("-")[0].toLowerCase(); + const language = locale.split("-")[0]?.toLowerCase() ?? ""; return RTL_LANGUAGES.has(language); }