fix(pdf): preserve first character of section headings by adding left padding (#3386)

* fix(pdf): add left padding to section heading text to prevent first-character clipping

Closes #3380

* fix(pdf): apply heading padding default after style composition

Apply paddingLeft: 1 only when no composed style fragment already defines it, so an explicit paddingLeft from a template or style rule is preserved. Keep the fallback for an empty style list.

* fix(pdf): keep heading safety padding on text only

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
Santhi Prakash
2026-09-05 09:35:06 -07:00
committed by GitHub
co-authored by Amruth Pillai
parent ea3980cba0
commit e0c2f6d88a
3 changed files with 30 additions and 2 deletions
@@ -239,6 +239,21 @@ describe("semantic issue fixtures", () => {
expect(mergedStyle(findText(document, "Analytical Engines")).fontWeight).not.toBe("400");
});
it("preserves the first character of a section heading with a leading text padding (#3380)", async () => {
const data = buildIssueFixture();
data.sections.experience.title = "Experience";
data.metadata.page.hideSectionIcons = false;
const element = createElement(ResumeDocument, { data, template: "onyx" }) as unknown as Parameters<typeof pdf>[0];
const instance = pdf(element);
await vi.waitFor(() => expect(instance.container.document).not.toBeNull());
const document = instance.container.document as HostNode;
const heading = findText(document, "Experience");
expect(heading).toBeDefined();
expect(mergedStyle(heading).paddingLeft).toBe(1);
});
it("renders descriptor filtering and stable item order instead of remapping raw arrays", async () => {
const data = buildIssueFixture();
data.sections.skills.items = ["First", "Hidden", "Last"].map((name, index) => ({
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { getSectionHeadingTextStyle } from "./sections";
const source = readFileSync(fileURLToPath(new URL("./sections.tsx", import.meta.url)), "utf8");
@@ -24,6 +25,11 @@ describe("ItemTitle", () => {
});
describe("SectionShell", () => {
it("keeps heading text safety padding separate from container padding", () => {
expect(getSectionHeadingTextStyle({ paddingLeft: 0 })).toEqual([{ paddingLeft: 1 }]);
expect(getSectionHeadingTextStyle({ paddingLeft: 6 })).toEqual([{ paddingLeft: 1 }]);
});
it("keeps section and heading style rules when section heading icons are hidden", () => {
expect(source).toContain(
"const resolvedSectionStyle = composeStyles(sectionStyle, sectionRuleStyle, resolved.style)",
@@ -188,8 +188,8 @@ const defaultSectionHeadingContainerStyle = {
columnGap: 4,
} satisfies Style;
const getSectionHeadingTextStyle = (...styles: StyleInput[]): Style[] =>
composeStyles(...styles).map(
export const getSectionHeadingTextStyle = (...styles: StyleInput[]): Style[] => {
const textStyles = composeStyles(...styles).map(
({
borderBottomWidth: _borderBottomWidth,
borderLeftWidth: _borderLeftWidth,
@@ -211,6 +211,13 @@ const getSectionHeadingTextStyle = (...styles: StyleInput[]): Style[] =>
}) => textStyle,
);
if (textStyles.length === 0) return [{ paddingLeft: 1 }];
const lastIndex = textStyles.length - 1;
const lastTextStyle: Style = { ...textStyles[lastIndex], paddingLeft: 1 };
return [...textStyles.slice(0, lastIndex), lastTextStyle];
};
const useSectionItemsContext = () => use(SectionItemsContext);
export const SemanticTextRuns = ({