From 24c882fa9f350c48a3d21e069d37b128768d303f Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Mon, 25 May 2026 16:29:50 +0200 Subject: [PATCH] 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 --- packages/fonts/src/index.test.ts | 6 +- packages/fonts/src/index.ts | 2 +- packages/pdf/src/context.tsx | 6 +- packages/pdf/src/rtl-fixture.test.ts | 27 +++++++++ .../pdf/src/templates/azurill/AzurillPage.tsx | 30 +++++----- .../pdf/src/templates/bronzor/BronzorPage.tsx | 31 ++++++----- .../src/templates/chikorita/ChikoritaPage.tsx | 39 ++++++------- .../pdf/src/templates/ditgar/DitgarPage.tsx | 28 ++++++---- .../pdf/src/templates/ditto/DittoPage.tsx | 41 +++++++------- .../pdf/src/templates/gengar/GengarPage.tsx | 35 ++++++------ .../pdf/src/templates/glalie/GlaliePage.tsx | 23 +++++--- .../pdf/src/templates/kakuna/KakunaPage.tsx | 28 +++++----- .../pdf/src/templates/lapras/LaprasPage.tsx | 25 +++++---- .../pdf/src/templates/leafish/LeafishPage.tsx | 27 +++++---- .../pdf/src/templates/meowth/MeowthPage.tsx | 28 ++++++---- packages/pdf/src/templates/onyx/OnyxPage.tsx | 25 +++++---- .../pdf/src/templates/pikachu/PikachuPage.tsx | 28 ++++++---- .../pdf/src/templates/rhyhorn/RhyhornPage.tsx | 54 +++++++----------- .../pdf/src/templates/scizor/ScizorPage.tsx | 25 +++++---- .../templates/shared/rich-text-renderers.ts | 49 +++++++++++++++++ .../src/templates/shared/rich-text.test.ts | 27 +++++++++ .../pdf/src/templates/shared/rich-text.tsx | 36 ++++-------- packages/pdf/src/templates/shared/rtl.test.ts | 34 ++++++++++++ packages/pdf/src/templates/shared/rtl.ts | 55 +++++++++++++++++++ .../pdf/src/templates/shared/sections.tsx | 49 ++++++++--------- packages/pdf/src/templates/shared/types.ts | 2 +- packages/utils/src/locale.test.ts | 15 ++++- packages/utils/src/locale.ts | 2 +- 28 files changed, 501 insertions(+), 276 deletions(-) create mode 100644 packages/pdf/src/rtl-fixture.test.ts create mode 100644 packages/pdf/src/templates/shared/rich-text-renderers.ts create mode 100644 packages/pdf/src/templates/shared/rtl.test.ts create mode 100644 packages/pdf/src/templates/shared/rtl.ts 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); }