From e0c2f6d88aa1c6af434334ce1b507b79a25b60b1 Mon Sep 17 00:00:00 2001 From: Santhi Prakash <38608178+santhiprakash@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:05:06 +0530 Subject: [PATCH] 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 --- packages/pdf/src/semantic/issue-fixtures.test.tsx | 15 +++++++++++++++ .../pdf/src/templates/shared/sections.test.ts | 6 ++++++ packages/pdf/src/templates/shared/sections.tsx | 11 +++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/pdf/src/semantic/issue-fixtures.test.tsx b/packages/pdf/src/semantic/issue-fixtures.test.tsx index 254b9f772..c513847f4 100644 --- a/packages/pdf/src/semantic/issue-fixtures.test.tsx +++ b/packages/pdf/src/semantic/issue-fixtures.test.tsx @@ -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[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) => ({ diff --git a/packages/pdf/src/templates/shared/sections.test.ts b/packages/pdf/src/templates/shared/sections.test.ts index 4066b381d..8df871ebf 100644 --- a/packages/pdf/src/templates/shared/sections.test.ts +++ b/packages/pdf/src/templates/shared/sections.test.ts @@ -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)", diff --git a/packages/pdf/src/templates/shared/sections.tsx b/packages/pdf/src/templates/shared/sections.tsx index 2cf112319..7b191b443 100644 --- a/packages/pdf/src/templates/shared/sections.tsx +++ b/packages/pdf/src/templates/shared/sections.tsx @@ -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 = ({