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 = ({