From f3a60432dfc00f831c62af1d75aec538dba3ddb5 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Sat, 4 Jul 2026 20:13:42 +0200 Subject: [PATCH] refactor(pdf): introduce createBaseTemplateStyles factory (~1,150 lines removed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/pdf/src/rtl-fixture.test.ts | 3 +- .../pdf/src/templates/azurill/AzurillPage.tsx | 79 +---------- .../pdf/src/templates/bronzor/BronzorPage.tsx | 80 +---------- .../src/templates/chikorita/ChikoritaPage.tsx | 80 +---------- .../pdf/src/templates/ditgar/DitgarPage.tsx | 79 +---------- .../pdf/src/templates/ditto/DittoPage.tsx | 90 ++----------- .../pdf/src/templates/gengar/GengarPage.tsx | 79 +---------- .../pdf/src/templates/glalie/GlaliePage.tsx | 81 +----------- .../pdf/src/templates/kakuna/KakunaPage.tsx | 79 +---------- .../pdf/src/templates/lapras/LaprasPage.tsx | 79 +---------- .../pdf/src/templates/leafish/LeafishPage.tsx | 79 +---------- .../pdf/src/templates/meowth/MeowthPage.tsx | 79 +---------- packages/pdf/src/templates/onyx/OnyxPage.tsx | 79 +---------- .../pdf/src/templates/pikachu/PikachuPage.tsx | 81 +----------- .../pdf/src/templates/rhyhorn/RhyhornPage.tsx | 81 +----------- .../pdf/src/templates/scizor/ScizorPage.tsx | 82 +----------- .../templates/shared/base-template-styles.ts | 124 ++++++++++++++++++ .../shared/rich-text-template-styles.test.ts | 27 +++- 18 files changed, 207 insertions(+), 1154 deletions(-) create mode 100644 packages/pdf/src/templates/shared/base-template-styles.ts diff --git a/packages/pdf/src/rtl-fixture.test.ts b/packages/pdf/src/rtl-fixture.test.ts index 8a86ecd19..5f78f0487 100644 --- a/packages/pdf/src/rtl-fixture.test.ts +++ b/packages/pdf/src/rtl-fixture.test.ts @@ -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"'); }); diff --git a/packages/pdf/src/templates/azurill/AzurillPage.tsx b/packages/pdf/src/templates/azurill/AzurillPage.tsx index de189063d..dbad02893 100644 --- a/packages/pdf/src/templates/azurill/AzurillPage.tsx +++ b/packages/pdf/src/templates/azurill/AzurillPage.tsx @@ -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", diff --git a/packages/pdf/src/templates/bronzor/BronzorPage.tsx b/packages/pdf/src/templates/bronzor/BronzorPage.tsx index b4f679468..0bd4296cc 100644 --- a/packages/pdf/src/templates/bronzor/BronzorPage.tsx +++ b/packages/pdf/src/templates/bronzor/BronzorPage.tsx @@ -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", diff --git a/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx b/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx index 42f290f65..4a3e3a9bb 100644 --- a/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx +++ b/packages/pdf/src/templates/chikorita/ChikoritaPage.tsx @@ -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), diff --git a/packages/pdf/src/templates/ditgar/DitgarPage.tsx b/packages/pdf/src/templates/ditgar/DitgarPage.tsx index c78b3ae50..f345bb0dc 100644 --- a/packages/pdf/src/templates/ditgar/DitgarPage.tsx +++ b/packages/pdf/src/templates/ditgar/DitgarPage.tsx @@ -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, diff --git a/packages/pdf/src/templates/ditto/DittoPage.tsx b/packages/pdf/src/templates/ditto/DittoPage.tsx index 05996e11e..9f9b4711c 100644 --- a/packages/pdf/src/templates/ditto/DittoPage.tsx +++ b/packages/pdf/src/templates/ditto/DittoPage.tsx @@ -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", diff --git a/packages/pdf/src/templates/gengar/GengarPage.tsx b/packages/pdf/src/templates/gengar/GengarPage.tsx index ae8aa0a05..f073d1c9d 100644 --- a/packages/pdf/src/templates/gengar/GengarPage.tsx +++ b/packages/pdf/src/templates/gengar/GengarPage.tsx @@ -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, diff --git a/packages/pdf/src/templates/glalie/GlaliePage.tsx b/packages/pdf/src/templates/glalie/GlaliePage.tsx index f07a0be09..a2909bcae 100644 --- a/packages/pdf/src/templates/glalie/GlaliePage.tsx +++ b/packages/pdf/src/templates/glalie/GlaliePage.tsx @@ -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) }), diff --git a/packages/pdf/src/templates/kakuna/KakunaPage.tsx b/packages/pdf/src/templates/kakuna/KakunaPage.tsx index 585883c78..fb685df33 100644 --- a/packages/pdf/src/templates/kakuna/KakunaPage.tsx +++ b/packages/pdf/src/templates/kakuna/KakunaPage.tsx @@ -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", diff --git a/packages/pdf/src/templates/lapras/LaprasPage.tsx b/packages/pdf/src/templates/lapras/LaprasPage.tsx index a2de62405..093ed805f 100644 --- a/packages/pdf/src/templates/lapras/LaprasPage.tsx +++ b/packages/pdf/src/templates/lapras/LaprasPage.tsx @@ -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), }, diff --git a/packages/pdf/src/templates/leafish/LeafishPage.tsx b/packages/pdf/src/templates/leafish/LeafishPage.tsx index 094b75143..e0cb73edc 100644 --- a/packages/pdf/src/templates/leafish/LeafishPage.tsx +++ b/packages/pdf/src/templates/leafish/LeafishPage.tsx @@ -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, diff --git a/packages/pdf/src/templates/meowth/MeowthPage.tsx b/packages/pdf/src/templates/meowth/MeowthPage.tsx index f48d19916..ab4cd271b 100644 --- a/packages/pdf/src/templates/meowth/MeowthPage.tsx +++ b/packages/pdf/src/templates/meowth/MeowthPage.tsx @@ -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: {}, }); diff --git a/packages/pdf/src/templates/onyx/OnyxPage.tsx b/packages/pdf/src/templates/onyx/OnyxPage.tsx index 9978febb0..39f6900f0 100644 --- a/packages/pdf/src/templates/onyx/OnyxPage.tsx +++ b/packages/pdf/src/templates/onyx/OnyxPage.tsx @@ -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), }, diff --git a/packages/pdf/src/templates/pikachu/PikachuPage.tsx b/packages/pdf/src/templates/pikachu/PikachuPage.tsx index 5806b3400..1365998cc 100644 --- a/packages/pdf/src/templates/pikachu/PikachuPage.tsx +++ b/packages/pdf/src/templates/pikachu/PikachuPage.tsx @@ -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) }), diff --git a/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx b/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx index 541b4e6ad..938fcdda0 100644 --- a/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx +++ b/packages/pdf/src/templates/rhyhorn/RhyhornPage.tsx @@ -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
  • 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: {}, }); diff --git a/packages/pdf/src/templates/scizor/ScizorPage.tsx b/packages/pdf/src/templates/scizor/ScizorPage.tsx index 55d996ffb..b4fd79d74 100644 --- a/packages/pdf/src/templates/scizor/ScizorPage.tsx +++ b/packages/pdf/src/templates/scizor/ScizorPage.tsx @@ -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", }, diff --git a/packages/pdf/src/templates/shared/base-template-styles.ts b/packages/pdf/src/templates/shared/base-template-styles.ts new file mode 100644 index 000000000..db10ba0ce --- /dev/null +++ b/packages/pdf/src/templates/shared/base-template-styles.ts @@ -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; + metrics: ReturnType; + 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
  • 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)`, + }, + }; +} diff --git a/packages/pdf/src/templates/shared/rich-text-template-styles.test.ts b/packages/pdf/src/templates/shared/rich-text-template-styles.test.ts index 3190d919a..e7068ee4f 100644 --- a/packages/pdf/src/templates/shared/rich-text-template-styles.test.ts +++ b/packages/pdf/src/templates/shared/rich-text-template-styles.test.ts @@ -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*{(?[\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*{(?[\s\S]*?)^\s*},/m); + expect(richListItemContentBlock?.groups?.body).toBeDefined(); + expect(richListItemContentBlock?.groups?.body).toContain("...bodyText"); + expect(richListItemContentBlock?.groups?.body).not.toMatch(/\blineHeight:/); + }); });