refactor(pdf): introduce createBaseTemplateStyles factory (~1,150 lines removed)

Move 14 identical style slots (text/heading/div/inline/link/small/bold/
richParagraph/richListItemRow/richListItemMarker/richListItemContent/
splitRow/alignEnd/picture) from all 15 template Page.tsx files into a
single createBaseTemplateStyles factory in templates/shared. Each template
now spreads …base and keeps only its real overrides. Resolved StyleSheet
values are identical to before. Update rtl-fixture and rich-text-template-
styles tests to guard the factory file rather than each template directly.

Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
This commit is contained in:
Amruth Pillai
2026-07-04 20:13:42 +02:00
parent 0701f3b62a
commit f3a60432df
18 changed files with 207 additions and 1154 deletions
+2 -1
View File
@@ -20,7 +20,8 @@ describe("RTL PDF fixture", () => {
const source = readFileSync(pagePath, "utf8");
expect(source).toContain("createRtlStyleHelpers");
expect(source).toContain("alignEnd");
// ponytail: alignEnd moved to createBaseTemplateStyles factory; either direct or factory counts.
expect(source.includes("alignEnd") || source.includes("createBaseTemplateStyles")).toBe(true);
expect(source).not.toContain("alignRight");
expect(source).not.toContain('from "@reactive-resume/utils/locale"');
});
@@ -11,6 +11,7 @@ import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -137,16 +138,10 @@ const useAzurillTemplate = (): AzurillTemplate => {
const colors: TemplateColorRoles = { foreground, background, primary };
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
flexDirection: "column",
rowGap: metrics.headerGap,
@@ -160,62 +155,6 @@ const useAzurillTemplate = (): AzurillTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
sectionHeading: {
color: primary,
},
@@ -230,18 +169,6 @@ const useAzurillTemplate = (): AzurillTemplate => {
alignItems: "center",
rowGap: metrics.gapY(0.5),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
headerTitle: {
alignItems: "center",
textAlign: "center",
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -134,16 +135,11 @@ const useBronzorTemplate = (): BronzorTemplate => {
const colors: TemplateColorRoles = { foreground, background, primary };
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
heading: { ...base.heading, fontWeight: metadata.typography.heading.fontWeights[0] ?? "500" },
page: {
flexDirection: "column",
rowGap: metrics.headerGap,
@@ -156,62 +152,6 @@ const useBronzorTemplate = (): BronzorTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: r.row,
columnGap: metrics.columnGap,
@@ -236,18 +176,6 @@ const useBronzorTemplate = (): BronzorTemplate => {
alignItems: "center",
rowGap: metrics.gapY(0.5),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
headerTitle: {
alignItems: "center",
textAlign: "center",
@@ -5,6 +5,7 @@ import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -151,16 +152,11 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
};
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
inline: { ...base.inline, columnGap: metrics.gapX(0.25) },
page: {
flexDirection: r.row,
color: foreground,
@@ -170,62 +166,6 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: r.row,
alignItems: "center",
columnGap: metrics.gapX(0.25),
},
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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -257,18 +197,6 @@ const useChikoritaTemplate = (): ChikoritaTemplate => {
alignItems: "flex-start",
columnGap: metrics.gapX(0.5),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
headerTitle: {
flex: 1,
rowGap: metrics.gapY(0.5),
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import { getPrimaryTint } from "../shared/color-helpers";
import {
CustomFieldContactItem,
@@ -187,16 +188,10 @@ const useDitgarTemplate = (): DitgarTemplate => {
};
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
flexDirection: r.row,
color: foreground,
@@ -206,62 +201,6 @@ const useDitgarTemplate = (): DitgarTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -314,18 +253,6 @@ const useDitgarTemplate = (): DitgarTemplate => {
paddingVertical: metrics.page.paddingVertical,
rowGap: metrics.gapY(0.5),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
headerTitle: {},
headerIdentity: {
...r.headerIdentity,
+10 -80
View File
@@ -5,6 +5,7 @@ import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -147,16 +148,17 @@ const useDittoTemplate = (): DittoTemplate => {
const metrics = getTemplateMetrics(metadata.page);
const hasPicture = hasTemplatePicture(picture);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
picture: {
...base.picture,
position: "absolute",
top: metrics.page.paddingVertical * 0.75,
left: "50%",
marginLeft: -picture.size / 2,
},
page: {
flexDirection: "column",
color: foreground,
@@ -166,62 +168,6 @@ const useDittoTemplate = (): DittoTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -255,22 +201,6 @@ const useDittoTemplate = (): DittoTemplate => {
flexShrink: 0,
position: "relative",
},
picture: {
position: "absolute",
top: metrics.page.paddingVertical * 0.75,
left: "50%",
marginLeft: -picture.size / 2,
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
headerTitle: {
flex: 1,
justifyContent: "center",
@@ -5,6 +5,7 @@ import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import { getPrimaryTint } from "../shared/color-helpers";
import {
CustomFieldContactItem,
@@ -183,16 +184,10 @@ const useGengarTemplate = (): GengarTemplate => {
};
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
flexDirection: r.row,
color: foreground,
@@ -202,62 +197,6 @@ const useGengarTemplate = (): GengarTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -311,18 +250,6 @@ const useGengarTemplate = (): GengarTemplate => {
paddingVertical: metrics.page.paddingVertical,
rowGap: metrics.gapY(0.5),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
headerTitle: {},
headerIdentity: {
...r.headerIdentity,
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import { getPrimaryTint } from "../shared/color-helpers";
import {
CustomFieldContactItem,
@@ -148,16 +149,10 @@ const useGlalieTemplate = (): GlalieTemplate => {
};
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
color: foreground,
backgroundColor: background,
@@ -166,62 +161,6 @@ const useGlalieTemplate = (): GlalieTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -276,18 +215,6 @@ const useGlalieTemplate = (): GlalieTemplate => {
alignItems: "center",
rowGap: metrics.gapY(0.5),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
headerTitle: {
alignItems: "center",
textAlign: "center",
@@ -328,7 +255,7 @@ const useGlalieTemplate = (): GlalieTemplate => {
colors,
styles: {
...baseStyles,
text: (context) => ({ ...bodyText, color: foregroundFor(context) }),
text: (context) => ({ ...base.text, color: foregroundFor(context) }),
heading: (context) => ({ ...baseStyles.heading, color: foregroundFor(context) }),
link: (context) => ({ ...baseStyles.link, color: foregroundFor(context) }),
sectionHeading: (context) => ({ ...baseStyles.sectionHeading, color: accentFor(context) }),
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -117,16 +118,10 @@ const useKakunaTemplate = (): KakunaTemplate => {
const colors: TemplateColorRoles = { foreground, background, primary };
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
color: foreground,
backgroundColor: background,
@@ -138,62 +133,6 @@ const useKakunaTemplate = (): KakunaTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -222,18 +161,6 @@ const useKakunaTemplate = (): KakunaTemplate => {
alignItems: "center",
rowGap: metrics.gapY(0.5),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
headerTitle: {
width: "100%",
textAlign: "center",
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -119,16 +120,10 @@ const useLaprasTemplate = (): LaprasTemplate => {
const colors: TemplateColorRoles = { foreground, background, primary };
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
color: foreground,
backgroundColor: background,
@@ -140,62 +135,6 @@ const useLaprasTemplate = (): LaprasTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -233,18 +172,6 @@ const useLaprasTemplate = (): LaprasTemplate => {
backgroundColor: background,
padding: metrics.gapX(1),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
headerTitle: {
rowGap: metrics.gapY(0.5),
},
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import { getPrimaryTint as getPrimaryAlpha } from "../shared/color-helpers";
import {
CustomFieldContactItem,
@@ -139,16 +140,10 @@ const useLeafishTemplate = (): LeafishTemplate => {
const colors: TemplateColorRoles = { foreground, background, primary };
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
color: foreground,
backgroundColor: background,
@@ -157,62 +152,6 @@ const useLeafishTemplate = (): LeafishTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -272,18 +211,6 @@ const useLeafishTemplate = (): LeafishTemplate => {
alignItems: "center",
columnGap: metrics.gapX(1 / 6),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
body: {
flexDirection: r.row,
columnGap: metrics.columnGap,
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -120,16 +121,10 @@ const useMeowthTemplate = (): MeowthTemplate => {
const primary = rgbaStringToHex(metadata.design.colors.primary);
const colors: TemplateColorRoles = { foreground, background, primary };
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
color: foreground,
backgroundColor: background,
@@ -141,62 +136,6 @@ const useMeowthTemplate = (): MeowthTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
inlineItemHeader: {
flexDirection: r.row,
alignItems: "flex-start",
@@ -270,18 +209,6 @@ const useMeowthTemplate = (): MeowthTemplate => {
alignItems: "center",
columnGap: metrics.gapX(1 / 6),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
sectionGroup: {},
});
+3 -76
View File
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -115,16 +116,10 @@ const useOnyxTemplate = (): OnyxTemplate => {
const primary = rgbaStringToHex(metadata.design.colors.primary);
const colors: TemplateColorRoles = { foreground, background, primary };
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
color: foreground,
backgroundColor: background,
@@ -136,62 +131,6 @@ const useOnyxTemplate = (): OnyxTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -216,18 +155,6 @@ const useOnyxTemplate = (): OnyxTemplate => {
borderBottomColor: primary,
paddingBottom: metrics.page.paddingVertical,
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
headerTitle: {
rowGap: metrics.gapY(0.5),
},
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -169,16 +170,10 @@ const usePikachuTemplate = (): PikachuTemplate => {
const colors: TemplateColorRoles = { foreground, background, primary };
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
color: foreground,
backgroundColor: background,
@@ -189,62 +184,6 @@ const usePikachuTemplate = (): PikachuTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -321,18 +260,6 @@ const usePikachuTemplate = (): PikachuTemplate => {
alignItems: "center",
columnGap: metrics.gapX(1 / 6),
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
});
const accentFor = ({ colors }: TemplateStyleContext) => colors.primary;
@@ -347,7 +274,7 @@ const usePikachuTemplate = (): PikachuTemplate => {
colors,
styles: {
...baseStyles,
text: (context) => ({ ...bodyText, color: foregroundFor(context) }),
text: (context) => ({ ...base.text, color: foregroundFor(context) }),
heading: (context) => ({ ...baseStyles.heading, color: foregroundFor(context) }),
link: (context) => ({ ...baseStyles.link, color: foregroundFor(context) }),
sectionHeading: (context) => ({ ...baseStyles.sectionHeading, color: accentFor(context) }),
@@ -6,6 +6,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -161,16 +162,10 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
const metrics = getTemplateMetrics(metadata.page);
const contactGap = metrics.gapX(0.5);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
color: foreground,
backgroundColor: background,
@@ -182,64 +177,6 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
// Stays `row` for both LTR and RTL; the <li> renderer swaps DOM order for RTL.
flexDirection: "row",
columnGap: metrics.gapX(1 / 3),
alignItems: "flex-start",
},
richListItemMarker: {
// bodyText spread first so `textAlign` below isn't clobbered by bodyText.textAlign.
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -295,18 +232,6 @@ const useRhyhornTemplate = (): RhyhornTemplate => {
columnGap: metrics.gapX(1 / 6),
},
contactItemLast: r.contactSeparatorClear,
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
sectionGroup: {},
});
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { Image, Page, StyleSheet, View } from "#react-pdf-renderer";
import { useRender } from "../../context";
import { createBaseTemplateStyles } from "../shared/base-template-styles";
import {
CustomFieldContactItem,
EmailContactItem,
@@ -109,16 +110,10 @@ const useScizorTemplate = (): ScizorTemplate => {
const divider = "#D8DCE2";
const colors: TemplateColorRoles = { foreground, background, primary };
const metrics = getTemplateMetrics(metadata.page);
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
const base = createBaseTemplateStyles({ metadata, foreground, r, metrics, picture });
const baseStyles = StyleSheet.create({
...base,
page: {
color: foreground,
backgroundColor: background,
@@ -132,63 +127,8 @@ const useScizorTemplate = (): ScizorTemplate => {
lineHeight: metadata.typography.body.lineHeight,
direction: r.pageDirection,
},
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
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: 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: {
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
},
richListItemContent: {
...bodyText,
flex: 1,
},
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
},
alignEnd: {
...r.alignEnd,
},
heading: { ...base.heading, fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "700" },
bold: { fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "700", color: foreground },
section: {
flexDirection: "column",
rowGap: metrics.gapY(0.25),
@@ -253,18 +193,6 @@ const useScizorTemplate = (): ScizorTemplate => {
columnGap: metrics.gapX(1 / 6),
color: foreground,
},
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
sections: {
flexDirection: "column",
},
@@ -0,0 +1,124 @@
import type { Style } from "@react-pdf/types";
import type { Picture, ResumeData } from "@reactive-resume/schema/resume/data";
import type { getTemplateMetrics } from "./metrics";
import type { createRtlStyleHelpers } from "./rtl";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
type BaseTemplateStylesInput = {
metadata: ResumeData["metadata"];
foreground: string;
r: ReturnType<typeof createRtlStyleHelpers>;
metrics: ReturnType<typeof getTemplateMetrics>;
picture: Picture;
};
/**
* Returns the ~20 byte-identical style slots shared by all 15 templates as plain objects.
* Each template spreads the result into its own StyleSheet.create() call and overrides only
* the slots that differ (heading fontWeight, bold fallback, inline gap, picture extras, etc.).
*
* ponytail: factory returns plain objects, not StyleSheet.create'd; each template does one
* StyleSheet.create pass so the final resolved styles are identical to before.
*/
export function createBaseTemplateStyles({ metadata, foreground, r, metrics, picture }: BaseTemplateStylesInput) {
const bodyText = {
fontFamily: metadata.typography.body.fontFamily,
fontSize: metadata.typography.body.fontSize,
fontWeight: metadata.typography.body.fontWeights[0] ?? "400",
lineHeight: metadata.typography.body.lineHeight,
color: foreground,
...r.text,
} satisfies Style;
return {
/** The canonical body text style; alias for `text` in StyleSheet slots. */
text: bodyText,
heading: {
fontFamily: metadata.typography.heading.fontFamily,
fontSize: metadata.typography.heading.fontSize,
/** Default fallback "600". bronzor uses fontWeights[0]??"500"; scizor uses .at(-1)??"700". */
fontWeight: metadata.typography.heading.fontWeights.at(-1) ?? "600",
lineHeight: metadata.typography.heading.lineHeight,
color: foreground,
...r.text,
} satisfies Style,
div: {
rowGap: metrics.gapY(0.125),
columnGap: metrics.gapX(1 / 3),
} satisfies Style,
/** Default gap = gapX(1/3). chikorita overrides to gapX(0.25). */
inline: {
flexDirection: r.row,
alignItems: "center",
columnGap: metrics.gapX(1 / 3),
} satisfies Style,
link: {
textDecoration: "none",
color: foreground,
} satisfies Style,
small: {
fontSize: metadata.typography.body.fontSize * 0.875,
} satisfies Style,
/** Default fallback "600". scizor overrides to "700". */
bold: {
fontWeight: metadata.typography.body.fontWeights.at(-1) ?? "600",
} satisfies Style,
richParagraph: {
margin: 0,
...bodyText,
} satisfies Style,
richListItemRow: {
// Stays `row` for both LTR and RTL; the <li> renderer swaps DOM order for RTL.
flexDirection: "row",
columnGap: metrics.gapX(1 / 3),
alignItems: "flex-start",
} satisfies Style,
richListItemMarker: {
// bodyText spread first so `textAlign` below isn't clobbered by bodyText.textAlign.
...bodyText,
width: metadata.typography.body.fontSize,
textAlign: r.listMarkerTextAlign,
} satisfies Style,
richListItemContent: {
...bodyText,
flex: 1,
} satisfies Style,
splitRow: {
flexDirection: r.row,
flexWrap: "wrap",
alignItems: "flex-start",
justifyContent: "space-between",
columnGap: metrics.gapX(2 / 3),
} satisfies Style,
alignEnd: { ...r.alignEnd } satisfies Style,
/**
* Standard picture style shared by 14/15 templates.
* ditto overrides: spread this and add `position: "absolute"`, `top`, `left`, `marginLeft`.
*/
picture: {
width: picture.size,
height: picture.size,
objectFit: "cover",
aspectRatio: picture.aspectRatio,
borderRadius: picture.borderRadius,
borderColor: rgbaStringToHex(picture.borderColor),
borderWidth: picture.borderWidth,
shadowColor: rgbaStringToHex(picture.shadowColor),
shadowWidth: picture.shadowWidth,
transform: `rotate(${picture.rotation}deg)`,
},
};
}
@@ -4,6 +4,8 @@ import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const templatesDir = fileURLToPath(new URL("../", import.meta.url));
// ponytail: richListItemContent moved to shared factory; guard the factory directly.
const factoryFile = fileURLToPath(new URL("./base-template-styles.ts", import.meta.url));
const templatePageFiles = readdirSync(templatesDir, { withFileTypes: true }).flatMap((entry) => {
if (!entry.isDirectory() || entry.name === "shared") return [];
@@ -15,14 +17,31 @@ const templatePageFiles = readdirSync(templatesDir, { withFileTypes: true }).fla
});
describe("rich text template styles", () => {
it.each(
templatePageFiles.map((file) => [basename(file), file]),
)("%s keeps list item rich text on the global body line height", (_name, file) => {
const source = readFileSync(file, "utf8");
// The shared factory owns richListItemContent for all 15 templates.
it("base factory keeps list item rich text on the global body line height", () => {
const source = readFileSync(factoryFile, "utf8");
const richListItemContentBlock = source.match(/richListItemContent:\s*{(?<body>[\s\S]*?)^\s*},/m);
expect(richListItemContentBlock?.groups?.body).toBeDefined();
expect(richListItemContentBlock?.groups?.body).toContain("...bodyText");
expect(richListItemContentBlock?.groups?.body).not.toMatch(/\blineHeight:/);
});
it.each(
templatePageFiles.map((file) => [basename(file), file]),
)("%s keeps list item rich text on the global body line height", (_name, file) => {
const source = readFileSync(file, "utf8");
if (source.includes("createBaseTemplateStyles")) {
// Factory handles richListItemContent; guard is on the factory test above.
expect(source).toContain("createBaseTemplateStyles");
return;
}
// Legacy: template defines richListItemContent inline — no lineHeight override allowed.
const richListItemContentBlock = source.match(/richListItemContent:\s*{(?<body>[\s\S]*?)^\s*},/m);
expect(richListItemContentBlock?.groups?.body).toBeDefined();
expect(richListItemContentBlock?.groups?.body).toContain("...bodyText");
expect(richListItemContentBlock?.groups?.body).not.toMatch(/\blineHeight:/);
});
});