fix(pdf): honor semantic section heading colors (#3415)

This commit is contained in:
Amruth Pillai
2026-09-05 08:52:49 -07:00
committed by GitHub
parent 779ea5cb4a
commit d53b89ba2d
4 changed files with 84 additions and 2 deletions
@@ -559,6 +559,7 @@ export const SectionHeadingIcon = ({
{...iconPropsWithoutDisplay}
{...props}
{...(resolvedSize === undefined ? {} : { size: resolvedSize })}
{...(resolved.style?.color === undefined ? {} : { color: resolved.style.color })}
style={composeStyles(asStyleInput(iconStyle), asStyleInput(style), resolved.style)}
/>
);
@@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import { renderToBuffer } from "@react-pdf/renderer";
import { getDocument, OPS } from "pdfjs-dist/legacy/build/pdf.mjs";
import { act, createElement } from "react";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { ResumeDocument } from "../../document";
const renderHeading = async (css: string, hideSectionIcons = false) => {
const data = structuredClone(defaultResumeData);
data.basics.name = "Audit";
data.metadata.typography.body.fontFamily = "Helvetica";
data.metadata.typography.heading.fontFamily = "Helvetica";
data.metadata.page.hideSectionIcons = hideSectionIcons;
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills"], sidebar: [] }];
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: `@version 1; ${css}` } };
data.sections.skills.items = [
{ id: "skill", hidden: false, name: "Skill", proficiency: "", level: 0, keywords: [], icon: "", iconColor: "" },
];
const element = createElement(ResumeDocument, {
data,
template: "scizor",
resolveSectionTitle: () => "Heading",
}) as unknown as Parameters<typeof renderToBuffer>[0];
let bytes: Uint8Array = new Uint8Array();
await act(async () => {
bytes = new Uint8Array(await renderToBuffer(element));
});
const loadingTask = getDocument({ data: bytes, useSystemFonts: true });
try {
const document = await loadingTask.promise;
const page = await document.getPage(1);
const operators = await page.getOperatorList();
let fill = "";
const text: { value: string; fill: string }[] = [];
const colors: string[] = [];
for (const [index, fn] of operators.fnArray.entries()) {
const args = operators.argsArray[index];
if (fn === OPS.setFillRGBColor) fill = args[0];
if (fn === OPS.setFillRGBColor || fn === OPS.setStrokeRGBColor) colors.push(args[0]);
if (fn === OPS.showText)
text.push({
value: args[0]
.map((glyph: { unicode?: string } | number) => (typeof glyph === "number" ? "" : (glyph.unicode ?? "")))
.join(""),
fill,
});
}
return { text, colors };
} finally {
await loadingTask.destroy();
}
};
describe("Semantic section heading colors (#3348)", () => {
it.each([false, true])("colors heading text with hideSectionIcons=%s", async (hidden) => {
const { text } = await renderHeading("section-heading { color: #1234ef; }", hidden);
expect(text).toContainEqual({ value: "HEADING", fill: "#1234ef" });
});
it("colors explicitly targeted section icons", async () => {
const { colors } = await renderHeading("section-heading icon { color: #178a6b; }");
expect(colors).toContain("#178a6b");
});
it("allows independent heading and icon colors", async () => {
const { text, colors } = await renderHeading(
"section-heading { color: #1234ef; } section-heading icon { color: #178a6b; }",
);
expect(text).toContainEqual({ value: "HEADING", fill: "#1234ef" });
expect(colors).toContain("#178a6b");
});
it("retains template colors without a custom rule", async () => {
const { text, colors } = await renderHeading("");
expect(text).toContainEqual({ value: "HEADING", fill: "#000000" });
expect(colors).toContain("#dc2626");
});
});
@@ -43,7 +43,7 @@ describe("SectionShell", () => {
);
expect(headingContainerBlock?.groups?.body).toContain('alignItems: "flex-start"');
expect(source).toContain("getSectionHeadingTextStyle(sectionHeadingStyle, sectionHeadingRuleStyle)");
expect(source).toMatch(/getSectionHeadingTextStyle\(\s*sectionHeadingStyle,\s*sectionHeadingRuleStyle(?:,|\))/);
expect(source).toContain("width: _width");
expect(source).not.toContain('width: "auto"');
});
@@ -366,7 +366,13 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio
/>
<Heading
bindSemanticNode={false}
style={getSectionHeadingTextStyle(sectionHeadingStyle, sectionHeadingRuleStyle)}
style={getSectionHeadingTextStyle(
sectionHeadingStyle,
sectionHeadingRuleStyle,
sectionHeadingResolved.style?.color === undefined
? undefined
: { color: sectionHeadingResolved.style.color },
)}
>
{sectionTitle}
</Heading>