fix: render cover letter exports without resume chrome

This commit is contained in:
Amruth Pillai
2026-07-07 17:47:52 +02:00
parent 5270a2a9a0
commit 8570c1c70a
25 changed files with 175 additions and 23 deletions
+5 -3
View File
@@ -15,6 +15,7 @@ import {
WidthType,
} from "docx";
import { parseColorString } from "@reactive-resume/utils/color";
import { shouldShowResumeHeader } from "./cover-letter";
import { toSafeDocxLink } from "./link-utils";
import { renderBuiltInSection, renderCustomSection, renderSummary, setRenderConfig } from "./section-renderers";
@@ -413,9 +414,10 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
textColorHex,
primaryColorHex: colorHex,
};
const showHeader = shouldShowResumeHeader(data);
// Header placement depends on template
if (templateConfig.headerPosition === "full-width") {
if (templateConfig.headerPosition === "full-width" && showHeader) {
setRenderConfig(mainConfig);
documentChildren.push(...buildHeader(data, colorHex, textColorHex));
}
@@ -434,7 +436,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
setRenderConfig(mainConfig);
const mainParagraphs: Paragraph[] = [];
if (templateConfig.headerPosition === "main-only") {
if (templateConfig.headerPosition === "main-only" && showHeader) {
mainParagraphs.push(...buildHeader(data, colorHex, textColorHex));
}
for (const sectionId of layoutPage.main) {
@@ -445,7 +447,7 @@ export function buildDocument(data: ResumeData, resolveTitle?: SectionTitleResol
setRenderConfig({ ...mainConfig, textColorHex: sidebarTextColorHex, primaryColorHex: sidebarHeadingColorHex });
const sidebarParagraphs: Paragraph[] = [];
if (templateConfig.headerPosition === "sidebar-only") {
if (templateConfig.headerPosition === "sidebar-only" && showHeader) {
sidebarParagraphs.push(...buildHeader(data, sidebarHeadingColorHex, sidebarTextColorHex));
}
for (const sectionId of layoutPage.sidebar) {
@@ -0,0 +1,14 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const builderSource = readFileSync(fileURLToPath(new URL("./builder.ts", import.meta.url)), "utf8");
describe("cover letter DOCX layout", () => {
it("gates resume headers for cover-letter-only documents", () => {
expect(builderSource).toContain("shouldShowResumeHeader(data)");
expect(builderSource).toContain('templateConfig.headerPosition === "full-width" && showHeader');
expect(builderSource).toContain('templateConfig.headerPosition === "main-only" && showHeader');
expect(builderSource).toContain('templateConfig.headerPosition === "sidebar-only" && showHeader');
});
});
+15
View File
@@ -0,0 +1,15 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
const isCoverLetterSection = (data: ResumeData, sectionId: string) => {
const section = data.customSections.find((customSection) => customSection.id === sectionId);
return section?.type === "cover-letter" && !section.hidden && section.items.some((item) => !item.hidden);
};
const isCoverLetterOnlyDocument = (data: ResumeData) => {
const sectionIds = data.metadata.layout.pages.flatMap((page) => [...page.main, ...page.sidebar]);
return sectionIds.length > 0 && sectionIds.every((sectionId) => isCoverLetterSection(data, sectionId));
};
export const shouldShowResumeHeader = (data: ResumeData) => !isCoverLetterOnlyDocument(data);
+2 -2
View File
@@ -150,6 +150,7 @@ describe("renderCustomSection", () => {
const section: CustomSection = {
...baseCustom,
type: "cover-letter",
title: "Cover Letter",
items: [
{
id: "x",
@@ -160,8 +161,7 @@ describe("renderCustomSection", () => {
],
};
const paragraphs = renderCustomSection(section, HEX);
// 1 heading + at least two paragraphs (recipient + body)
expect(paragraphs.length).toBeGreaterThanOrEqual(3);
expect(paragraphs).toHaveLength(2);
});
});
+1 -1
View File
@@ -506,7 +506,7 @@ export function renderCustomSection(section: CustomSection, colorHex: string): P
// Cover letter type — render recipient + content
if (sectionType === "cover-letter") {
const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)];
const paragraphs: Paragraph[] = [];
for (const item of visibleItems) {
if ("recipient" in item && item.recipient) {
paragraphs.push(...htmlToParagraphs(item.recipient, getHtmlStyle()));