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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Amruth Pillai
2026-05-25 16:29:50 +02:00
parent 86fff7237f
commit 24c882fa9f
28 changed files with 501 additions and 276 deletions
+3 -3
View File
@@ -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", () => {
+1 -1
View File
@@ -132,7 +132,7 @@ const legacyFontAliases: Record<string, string> = {
Cambria: "Tinos",
Calibri: "Carlito",
Garamond: "EB Garamond",
"Times New Roman": "Tinos",
"Times New Roman": "Times-Roman",
};
export function resolveLegacyFontAlias(family: string): string | null {
+5 -1
View File
@@ -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<RenderContextValue | null>(null);
@@ -16,7 +18,9 @@ export type RenderProviderProps = {
};
export const RenderProvider = ({ data, resolveSectionTitle, children }: RenderProviderProps) => {
return <RenderContext.Provider value={{ ...data, resolveSectionTitle }}>{children}</RenderContext.Provider>;
const rtl = isRTL(data.metadata.page.locale);
return <RenderContext.Provider value={{ ...data, resolveSectionTitle, rtl }}>{children}</RenderContext.Provider>;
};
export const useRender = (): RenderContextValue => {
+27
View File
@@ -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"');
});
});
@@ -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]);
};
@@ -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]);
};
@@ -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]);
};
@@ -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]);
};
+21 -20
View File
@@ -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]);
};
@@ -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]);
};
@@ -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]);
};
@@ -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]);
};
@@ -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]);
};
@@ -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]);
};
@@ -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]);
};
+15 -10
View File
@@ -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]);
};
@@ -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]);
};
@@ -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]);
};
@@ -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]);
};
@@ -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<typeof isRichTextElementInsideListItem>[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);
};
@@ -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("<p>Plain <strong>bold</strong> text</p>").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"]);
});
});
+11 -25
View File
@@ -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 <Text>
// 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 }) => <PdfText style={composeStyles(boldStyle, safeTextStyle)}>{children}</PdfText>,
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 (
<PdfText style={composeStyles(paragraphStyles, getRichTextEdgeTrimStyle(element), rtlTextWrapStyle)}>
{applyRtlDirectionRecursively(children)}
</PdfText>
);
}
return <View style={composeStyles(paragraphStyles, getRichTextEdgeTrimStyle(element))}>{children}</View>;
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 = (
@@ -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 });
});
});
+55
View File
@@ -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<Style, "direction" | "textAlign">;
alignEnd: Pick<Style, "textAlign" | "minWidth" | "maxWidth" | "flexShrink">;
sectionHeadingTextAlign: NonNullable<Style["textAlign"]>;
headerIdentity: Pick<Style, "textAlign" | "alignItems">;
listMarkerTextAlign: NonNullable<Style["textAlign"]>;
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 }),
};
}
+22 -27
View File
@@ -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={<ItemTitle website={item.website}>{item.company}</ItemTitle>}
trailing={<Text style={composeStyles(alignRightStyle)}>{item.period}</Text>}
trailing={<Text style={composeStyles(alignEndStyle)}>{item.period}</Text>}
/>
);
@@ -376,15 +375,13 @@ const ExperienceSection = ({
<>
<View style={composeStyles(splitRowStyle)}>
<ItemTitle website={item.website}>{item.company}</ItemTitle>
{hasSplitRowText(headerLocation) && (
<Text style={composeStyles(alignRightStyle)}>{headerLocation}</Text>
)}
{hasSplitRowText(headerLocation) && <Text style={composeStyles(alignEndStyle)}>{headerLocation}</Text>}
</View>
{item.roles.length === 0 && (hasPosition || hasSplitRowText(headerPeriod)) && (
<View style={composeStyles(splitRowStyle)}>
{hasPosition && <Text>{item.position}</Text>}
{hasSplitRowText(headerPeriod) && <Text style={composeStyles(alignRightStyle)}>{headerPeriod}</Text>}
{hasSplitRowText(headerPeriod) && <Text style={composeStyles(alignEndStyle)}>{headerPeriod}</Text>}
</View>
)}
</>
@@ -400,7 +397,7 @@ const ExperienceSection = ({
<View key={role.id}>
<View style={composeStyles(splitRowStyle)}>
<Text>{role.position}</Text>
<Text style={composeStyles(alignRightStyle)}>{role.period}</Text>
<Text style={composeStyles(alignEndStyle)}>{role.period}</Text>
</View>
<RichText>{role.description}</RichText>
</View>
@@ -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={<ItemTitle website={item.website}>{item.school}</ItemTitle>}
trailing={<Text style={composeStyles(alignRightStyle)}>{item.period}</Text>}
trailing={<Text style={composeStyles(alignEndStyle)}>{item.period}</Text>}
/>
{gradeAndLocation && <Text>{gradeAndLocation}</Text>}
</>
@@ -471,7 +468,7 @@ const EducationSection = ({
<View style={composeStyles(splitRowStyle)}>
<ItemTitle website={item.website}>{item.school}</ItemTitle>
{hasSplitRowText(headerDegreeAndGrade) && (
<Text style={composeStyles(alignRightStyle)}>{headerDegreeAndGrade}</Text>
<Text style={composeStyles(alignEndStyle)}>{headerDegreeAndGrade}</Text>
)}
</View>
@@ -479,7 +476,7 @@ const EducationSection = ({
<View style={composeStyles(splitRowStyle)}>
{hasArea && <Text>{item.area}</Text>}
{hasSplitRowText(headerLocationAndPeriod) && (
<Text style={composeStyles(alignRightStyle)}>{headerLocationAndPeriod}</Text>
<Text style={composeStyles(alignEndStyle)}>{headerLocationAndPeriod}</Text>
)}
</View>
)}
@@ -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 = ({
<SectionItemHeader>
<View style={composeStyles(splitRowStyle)}>
<ItemTitle website={item.website}>{item.name}</ItemTitle>
<Text style={composeStyles(alignRightStyle)}>{item.period}</Text>
<Text style={composeStyles(alignEndStyle)}>{item.period}</Text>
</View>
</SectionItemHeader>
@@ -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 = ({
<SectionItemHeader>
<View style={composeStyles(splitRowStyle, awardTitleDateRowStyle)}>
<ItemTitle website={item.website}>{item.title}</ItemTitle>
<Text style={composeStyles(alignRightStyle)}>{item.date}</Text>
<Text style={composeStyles(alignEndStyle)}>{item.date}</Text>
</View>
<Text>{item.awarder}</Text>
</SectionItemHeader>
@@ -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 = ({
<SectionItemHeader>
<View style={composeStyles(splitRowStyle)}>
<ItemTitle website={item.website}>{item.title}</ItemTitle>
<Text style={composeStyles(alignRightStyle)}>{item.date}</Text>
<Text style={composeStyles(alignEndStyle)}>{item.date}</Text>
</View>
<Text>{item.issuer}</Text>
</SectionItemHeader>
@@ -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 = ({
<SectionItemHeader>
<View style={composeStyles(splitRowStyle)}>
<ItemTitle website={item.website}>{item.title}</ItemTitle>
<Text style={composeStyles(alignRightStyle)}>{item.date}</Text>
<Text style={composeStyles(alignEndStyle)}>{item.date}</Text>
</View>
<Text>{item.publisher}</Text>
@@ -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 = ({
<InlineItemHeader
leading={hasSplitRowText(item.location) ? <Text>{item.location}</Text> : null}
middle={<ItemTitle website={item.website}>{item.organization}</ItemTitle>}
trailing={<Text style={composeStyles(alignRightStyle)}>{item.period}</Text>}
trailing={<Text style={composeStyles(alignEndStyle)}>{item.period}</Text>}
/>
) : (
<>
<View style={composeStyles(splitRowStyle)}>
<ItemTitle website={item.website}>{item.organization}</ItemTitle>
{hasSplitRowText(item.period) && (
<Text style={composeStyles(alignRightStyle)}>{item.period}</Text>
)}
{hasSplitRowText(item.period) && <Text style={composeStyles(alignEndStyle)}>{item.period}</Text>}
</View>
<Text>{item.location}</Text>
+1 -1
View File
@@ -58,7 +58,7 @@ export type TemplateStyleSlots = {
richListItemMarker?: TemplateStyleSlot;
richListItemContent?: TemplateStyleSlot;
splitRow?: TemplateStyleSlot;
alignRight?: TemplateStyleSlot;
alignEnd?: TemplateStyleSlot;
inlineItemHeader?: TemplateStyleSlot;
inlineItemHeaderLeading?: TemplateStyleSlot;
inlineItemHeaderMiddle?: TemplateStyleSlot;
+14 -1
View File
@@ -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);
});
});
+1 -1
View File
@@ -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);
}