feat: implement free-form resume page formats, resolves #2991

This commit is contained in:
Amruth Pillai
2026-05-08 11:28:18 +02:00
parent 9cbb30d3ba
commit 2cd774dab7
94 changed files with 860 additions and 443 deletions
+1 -1
View File
@@ -30,7 +30,7 @@
"@react-pdf/types": "^2.11.1",
"@reactive-resume/config": "workspace:*",
"@types/react": "^19.2.14",
"@typescript/native-preview": "7.0.0-dev.20260507.1",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"typescript": "^6.0.3"
}
}
@@ -15,7 +15,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -51,12 +51,13 @@ export const AzurillPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles, featureStyles } = useAzurillTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const sidebarSections = filterSections(page.sidebar, data);
const mainSections = filterSections(page.main, data);
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} featureStyles={featureStyles} colors={colors} features={azurillFeatures}>
{showHeader && <Header styles={styles} />}
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -54,13 +54,14 @@ export const BronzorPage = ({ page, pageIndex }: TemplatePageProps) => {
const { styles, colors } = useBronzorTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const sidebarSections = filterSections(page.sidebar, data);
const mainSections = filterSections(page.main, data);
const sections = getBronzorSections({ mainSections, sidebarSections, fullWidth: page.fullWidth });
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors}>
{showHeader && <Header styles={styles} />}
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -40,13 +40,14 @@ export const ChikoritaPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useChikoritaTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const hasPicture = hasTemplatePicture(picture);
const showHeader = pageIndex === 0;
const sidebarSections = filterSections(page.sidebar, data);
const mainSections = filterSections(page.main, data);
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors}>
<View
style={composeStyles(styles.mainColumn, {
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -48,6 +48,7 @@ export const DitgarPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useDitgarTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const showSidebar = !page.fullWidth || showHeader;
const sidebarSections = filterSections(page.sidebar, data);
@@ -56,7 +57,7 @@ export const DitgarPage = ({ page, pageIndex }: TemplatePageProps) => {
const regularMainSections = showHeader ? mainSections.slice(1) : mainSections;
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors} features={ditgarFeatures}>
{showSidebar && (
<View
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -45,13 +45,14 @@ export const DittoPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useDittoTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const hasPicture = hasTemplatePicture(picture);
const showHeader = pageIndex === 0;
const sidebarSections = filterSections(page.sidebar, data);
const mainSections = filterSections(page.main, data);
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors}>
{showHeader && <Header styles={styles} />}
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -43,6 +43,7 @@ export const GengarPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useGengarTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const showSidebar = !page.fullWidth || showHeader;
const sidebarSections = filterSections(page.sidebar, data);
@@ -51,7 +52,7 @@ export const GengarPage = ({ page, pageIndex }: TemplatePageProps) => {
const regularMainSections = showHeader ? mainSections.slice(1) : mainSections;
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors}>
{showSidebar && (
<View
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -47,13 +47,14 @@ export const GlaliePage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useGlalieTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const showSidebar = !page.fullWidth || showHeader;
const mainSections = filterSections(page.main, data);
const sidebarSections = filterSections(page.sidebar, data);
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors} features={glalieFeatures}>
{showSidebar && <View style={styles.sidebarBackground} />}
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -39,12 +39,13 @@ export const KakunaPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useKakunaTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const mainSections = filterSections(page.main, data);
const sidebarSections = filterSections(page.sidebar, data);
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors}>
{showHeader && <Header styles={styles} />}
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -38,12 +38,13 @@ export const LaprasPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useLaprasTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const mainSections = filterSections(page.main, data);
const sidebarSections = filterSections(page.sidebar, data);
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors}>
{showHeader && <Header styles={styles} />}
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -43,12 +43,13 @@ export const LeafishPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useLeafishTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const mainSections = filterSections(page.main, data).filter((section) => section !== "summary");
const sidebarSections = filterSections(page.sidebar, data).filter((section) => section !== "summary");
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors}>
{showHeader && <Header styles={styles} />}
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -43,12 +43,13 @@ export const MeowthPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useMeowthTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const mainSections = filterSections(page.main, data);
const sidebarSections = filterSections(page.sidebar, data);
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors} features={meowthFeatures}>
{showHeader && <Header styles={styles} />}
+3 -2
View File
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -38,12 +38,13 @@ export const OnyxPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useOnyxTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const mainSections = filterSections(page.main, data);
const sidebarSections = filterSections(page.sidebar, data);
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors}>
{showHeader && <Header styles={styles} />}
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -47,6 +47,7 @@ export const PikachuPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = usePikachuTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const showSidebar = !page.fullWidth;
const hasPicture = hasTemplatePicture(picture);
@@ -54,7 +55,7 @@ export const PikachuPage = ({ page, pageIndex }: TemplatePageProps) => {
const sidebarSections = filterSections(page.sidebar, data);
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors} features={pikachuFeatures}>
<View style={styles.layout}>
{showSidebar && (
@@ -9,7 +9,7 @@ import { useRender } from "../../context";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
import { getTemplatePageSize } from "../shared/page-size";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "../shared/page-size";
import { hasTemplatePicture } from "../shared/picture";
import { Heading, Icon, Link, Text } from "../shared/primitives";
import { Section } from "../shared/sections";
@@ -40,12 +40,13 @@ export const RhyhornPage = ({ page, pageIndex }: TemplatePageProps) => {
const { colors, styles } = useRhyhornTemplate();
const metrics = getTemplateMetrics(metadata.page);
const pageSize = getTemplatePageSize(metadata.page.format);
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
const showHeader = pageIndex === 0;
const mainSections = filterSections(page.main, data);
const sidebarSections = filterSections(page.sidebar, data);
return (
<Page size={pageSize} style={styles.page}>
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
<TemplateProvider styles={styles} colors={colors}>
{showHeader && <Header styles={styles} />}
+18 -2
View File
@@ -1,4 +1,20 @@
import type { Style } from "@react-pdf/types";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
export const getTemplatePageSize = (format: ResumeData["metadata"]["page"]["format"]) =>
format === "letter" ? "LETTER" : "A4";
const A4_PAGE_SIZE = {
width: 595.28,
height: 841.89,
} as const;
export const getTemplatePageSize = (format: ResumeData["metadata"]["page"]["format"]) => {
if (format === "free-form") return { width: A4_PAGE_SIZE.width };
if (format === "letter") return "LETTER";
return "A4";
};
export const getTemplatePageMinHeightStyle = (format: ResumeData["metadata"]["page"]["format"]): Style | undefined => {
if (format !== "free-form") return undefined;
return { minHeight: A4_PAGE_SIZE.height };
};