From 368858a56fc9c3152b540c39829908e2c3ea04c5 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Sat, 5 Sep 2026 20:34:21 -0700 Subject: [PATCH] feat(resume): add per-section heading visibility (#3477) --- .orchestration/plan-21-implementation.md | 33 +++++ .../src/dialogs/resume/sections/custom.tsx | 1 + .../preview/resume-accessible-text.test.tsx | 25 ++++ apps/web/src/libs/resume/move-item.test.ts | 1 + apps/web/src/libs/resume/move-item.ts | 1 + .../-sidebar/left/sections/custom.tsx | 14 ++ .../left/shared/section-menu.test.tsx | 21 +++ .../-sidebar/left/shared/section-menu.tsx | 16 +++ docs/guides/json-resume-schema.mdx | 135 ++++++++++++++++++ packages/docx/src/section-renderers.test.ts | 33 +++++ packages/docx/src/section-renderers.ts | 39 ++--- packages/pdf/src/section-icon.test.ts | 4 + packages/pdf/src/section-title.test.ts | 2 + .../src/semantic/binding-inventory.test.ts | 1 + .../pdf/src/semantic/issue-fixtures.test.tsx | 28 ++++ .../pdf/src/semantic/legacy-parity.test.ts | 1 + .../semantic/legacy-render-parity.test.tsx | 1 + packages/pdf/src/semantic/tree.test.ts | 1 + .../pdf/src/templates/shared/sections.tsx | 9 +- packages/schema/src/resume/data.test.ts | 36 +++++ packages/schema/src/resume/data.ts | 18 ++- packages/schema/src/resume/default.ts | 2 + packages/schema/src/resume/sample.ts | 15 ++ skills/resume-builder/references/schema.md | 27 ++++ tooling/recovery/compare-resume.test.ts | 8 +- 25 files changed, 449 insertions(+), 23 deletions(-) create mode 100644 .orchestration/plan-21-implementation.md diff --git a/.orchestration/plan-21-implementation.md b/.orchestration/plan-21-implementation.md new file mode 100644 index 000000000..07e19e751 --- /dev/null +++ b/.orchestration/plan-21-implementation.md @@ -0,0 +1,33 @@ +# Plan 21 implementation evidence + +## Revision and scope + +- Worktree: `issue-3060-section-heading-visibility` +- Base: current `origin/main` at dispatch, `2a4a1583b` (`fix(pdf): restore Gengar skill rating order (#3473)`) +- Product decisions applied: Q1 explicit Show heading toggle; Q2 Move-to continuations default visible; Q3 visual omission in preview/PDF/DOCX with accessible outline labels retained. +- No `.codegraph/` directory exists in this worktree, so CodeGraph was skipped after the required presence check. +- Intent discovery ran before edits; no matching local skill was available for this schema/PDF/DOCX/web change. + +## Implementation + +- Added backward-compatible `showHeading` section data for summary, built-ins, and custom sections. `parseResumeData` normalizes absent legacy values to `true`; explicit `false` survives round trips. +- Added heading toggles to built-in/summary and custom section menus. Toggle mutations use `useUpdateResumeData`, preserving undo/autosave/save/reload behavior; legacy absent values are treated as visible. Existing lock fieldset remains authoritative. +- Move-to-created custom sections explicitly set `showHeading: true`, independent of source heading state or copied title. +- `SectionShell` omits complete heading/icon/decoration output when disabled in both icon and no-icon branches. Empty titles still resolve localized defaults. +- DOCX section renderers omit visible heading paragraphs for summary, built-in, and custom sections while retaining content. Screen-reader mirror continues to expose section labels regardless of visual setting. +- Added characterization for Semantic CSS `section[id="..."] section-heading { display: none; }`; body remains while heading is omitted. +- Updated default/sample fixtures, generated schema references, recovery hashes, and compatibility tests; existing Gengar renderer/order changes remain untouched. + +## Verification + +- `pnpm --filter @reactive-resume/schema test`: 9 files, 132 tests passed. +- `pnpm --filter @reactive-resume/pdf test`: 81 files, 1059 tests passed. +- `pnpm --filter @reactive-resume/docx test`: 9 files, 76 tests passed. +- `pnpm --filter web test`: 135 files, 942 tests passed. +- `pnpm test`: full Turborepo suite passed (19 successful tasks; 10 cache hits). +- Affected typechecks passed: schema, PDF, DOCX, web. +- Focused menu, Move-to, accessible-outline, schema, PDF semantic, and DOCX renderer tests passed. +- `pnpm exec turbo boundaries`: passed (1108 files, 20 packages). +- Read-only `pnpm exec biome check` on 22 changed source/test files: passed; no write-capable `pnpm check` run. +- `git diff --check`: passed. +- Final diff review completed; local commit follows. diff --git a/apps/web/src/dialogs/resume/sections/custom.tsx b/apps/web/src/dialogs/resume/sections/custom.tsx index 112982e9c..a317dcfb4 100644 --- a/apps/web/src/dialogs/resume/sections/custom.tsx +++ b/apps/web/src/dialogs/resume/sections/custom.tsx @@ -39,6 +39,7 @@ const defaultValues: FormValues = { icon: "", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [], diff --git a/apps/web/src/features/resume/preview/resume-accessible-text.test.tsx b/apps/web/src/features/resume/preview/resume-accessible-text.test.tsx index eff296779..94437a3df 100644 --- a/apps/web/src/features/resume/preview/resume-accessible-text.test.tsx +++ b/apps/web/src/features/resume/preview/resume-accessible-text.test.tsx @@ -38,3 +38,28 @@ it.each([false, true])("exposes keyword list semantics for custom=%s", (custom) if (!list) throw new Error("Missing keyword list"); expect(within(list).getAllByRole("listitem")).toHaveLength(3); }); + +it("retains section labels when visual heading is disabled", () => { + const data = structuredClone(defaultResumeData); + data.sections.skills.showHeading = false; + data.sections.skills.items = [ + { + id: "skill", + name: "TypeScript", + hidden: false, + icon: "", + iconColor: "", + proficiency: "Expert", + level: 3, + keywords: [], + }, + ]; + + render( + + + , + ); + + expect(screen.getByRole("heading", { level: 2, name: "Skills" })).toBeInTheDocument(); +}); diff --git a/apps/web/src/libs/resume/move-item.test.ts b/apps/web/src/libs/resume/move-item.test.ts index 8408da501..d00bd7fc5 100644 --- a/apps/web/src/libs/resume/move-item.test.ts +++ b/apps/web/src/libs/resume/move-item.test.ts @@ -213,6 +213,7 @@ describe("createCustomSectionWithItem", () => { expect(result.customSections).toHaveLength(1); expect(result.customSections[0]?.title).toBe("My Section"); + expect(result.customSections[0]?.showHeading).toBe(true); expect(result.customSections[0]?.items).toHaveLength(1); expect(result.metadata.layout.pages[0]?.main).toContain(newSectionId); }); diff --git a/apps/web/src/libs/resume/move-item.ts b/apps/web/src/libs/resume/move-item.ts index b76f1abb4..f3a1922bc 100644 --- a/apps/web/src/libs/resume/move-item.ts +++ b/apps/web/src/libs/resume/move-item.ts @@ -204,6 +204,7 @@ function makeCustomSection(id: string, type: CustomSectionType, title: string, i icon: "", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [item as never], diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx index 020bc5498..455422e6c 100644 --- a/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx +++ b/apps/web/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx @@ -225,6 +225,7 @@ function CustomSectionDropdownMenu({ section }: CustomSectionDropdownMenuProps) const confirm = useConfirm(); const { openDialog } = useDialogStore(); const updateResumeData = useUpdateResumeData(); + const showHeading = section.showHeading !== false; const onToggleSectionVisibility = () => { updateResumeData((draft) => { @@ -234,6 +235,14 @@ function CustomSectionDropdownMenu({ section }: CustomSectionDropdownMenuProps) }); }; + const onToggleHeading = () => { + updateResumeData((draft) => { + const sectionIndex = draft.customSections.findIndex((_section) => _section.id === section.id); + if (sectionIndex === -1) return; + draft.customSections[sectionIndex].showHeading = !(draft.customSections[sectionIndex].showHeading !== false); + }); + }; + const onUpdateSection = () => { openDialog("resume.sections.custom.update", section); }; @@ -287,6 +296,11 @@ function CustomSectionDropdownMenu({ section }: CustomSectionDropdownMenuProps) {section.hidden ? Show : Hide} + + {showHeading ? : } + {showHeading ? Hide heading : Show heading} + + Update diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/left/shared/section-menu.test.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/left/shared/section-menu.test.tsx index dc2feb49c..03779836c 100644 --- a/apps/web/src/routes/builder/$resumeId/-sidebar/left/shared/section-menu.test.tsx +++ b/apps/web/src/routes/builder/$resumeId/-sidebar/left/shared/section-menu.test.tsx @@ -170,6 +170,27 @@ describe("SkillsSectionDropdownMenu", () => { }); }); +describe("heading visibility", () => { + it("toggles heading visibility without changing section visibility", async () => { + render( + + + , + ); + + await openSectionOptions(); + fireEvent.click(await screen.findByRole("menuitem", { name: "Hide heading" })); + + expect(mocks.updateResumeData).toHaveBeenCalledTimes(1); + const mutation = mocks.updateResumeData.mock.calls[0]?.[0] as (draft: Resume["data"]) => void; + const draft = structuredClone(mocks.resume.value.data); + mutation(draft); + + expect(draft.sections.experience.showHeading).toBe(false); + expect(draft.sections.experience.hidden).toBe(false); + }); +}); + describe("chronological section sorting", () => { it("sorts Experience once through one draft mutation and names only unresolved items", async () => { render( diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/left/shared/section-menu.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/left/shared/section-menu.tsx index 29bab2840..735598bab 100644 --- a/apps/web/src/routes/builder/$resumeId/-sidebar/left/shared/section-menu.tsx +++ b/apps/web/src/routes/builder/$resumeId/-sidebar/left/shared/section-menu.tsx @@ -45,6 +45,7 @@ export function SectionDropdownMenu({ type }: Props) { const updateResumeData = useUpdateResumeData(); const resume = useCurrentResume(); const section = type === "summary" ? resume.data.summary : resume.data.sections[type]; + const showHeading = section.showHeading !== false; const dropDownValue = type === "skills" && resume.data.sections[type].layout === "inline" ? "inline" : section.columns.toString(); @@ -93,6 +94,16 @@ export function SectionDropdownMenu({ type }: Props) { }); }; + const onToggleHeading = () => { + updateResumeData((draft) => { + if (type === "summary") { + draft.summary.showHeading = !(draft.summary.showHeading !== false); + } else { + draft.sections[type].showHeading = !(draft.sections[type].showHeading !== false); + } + }); + }; + const onRenameSection = async () => { const newTitle = await prompt(t`What do you want to rename this section to?`, { description: t`Leave empty to reset the title to the original.`, @@ -190,6 +201,11 @@ export function SectionDropdownMenu({ type }: Props) { {section.hidden ? Show : Hide} + + {showHeading ? : } + {showHeading ? Hide heading : Show heading} + + Rename diff --git a/docs/guides/json-resume-schema.mdx b/docs/guides/json-resume-schema.mdx index 9ee639ceb..06688e6ae 100644 --- a/docs/guides/json-resume-schema.mdx +++ b/docs/guides/json-resume-schema.mdx @@ -338,6 +338,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the summary from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the summary heading, icon, and decoration while retaining summary content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the summary is kept on a single page instead of splitting across a page break.", @@ -390,6 +395,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -508,6 +518,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -663,6 +678,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -795,6 +815,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -908,6 +933,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -1030,6 +1060,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -1115,6 +1150,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -1207,6 +1247,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -1324,6 +1369,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -1441,6 +1491,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -1558,6 +1613,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -1675,6 +1735,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -1813,6 +1878,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -1901,6 +1971,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -1984,6 +2059,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -2114,6 +2194,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -2281,6 +2366,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -2425,6 +2515,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -2549,6 +2644,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -2674,6 +2774,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -2771,6 +2876,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -2875,6 +2985,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -3004,6 +3119,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -3133,6 +3253,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -3262,6 +3387,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", @@ -3391,6 +3521,11 @@ The full JSON Schema for Reactive Resume follows. You can also fetch the latest "type": "boolean", "description": "Whether to hide the section from the resume." }, + "showHeading": { + "default": true, + "description": "Whether to show the section heading, icon, and decoration while retaining section content.", + "type": "boolean" + }, "keepTogether": { "default": false, "description": "If true, the section is kept on a single page instead of splitting across a page break.", diff --git a/packages/docx/src/section-renderers.test.ts b/packages/docx/src/section-renderers.test.ts index f48f1e612..993dbe059 100644 --- a/packages/docx/src/section-renderers.test.ts +++ b/packages/docx/src/section-renderers.test.ts @@ -25,6 +25,7 @@ describe("renderSummary", () => { icon: getDefaultSectionIconName("summary"), content: "

Hello

", hidden: true, + showHeading: true, columns: 1, keepTogether: false, startOnNewPage: false, @@ -38,6 +39,7 @@ describe("renderSummary", () => { icon: getDefaultSectionIconName("summary"), content: "", hidden: false, + showHeading: true, columns: 1, keepTogether: false, startOnNewPage: false, @@ -51,6 +53,7 @@ describe("renderSummary", () => { icon: getDefaultSectionIconName("summary"), content: "

Hello world

", hidden: false, + showHeading: true, columns: 1, keepTogether: false, startOnNewPage: false, @@ -60,12 +63,30 @@ describe("renderSummary", () => { expect(paragraphs.length).toBeGreaterThanOrEqual(2); }); + it("omits a disabled heading while retaining summary content", () => { + const summary: ResumeData["summary"] = { + title: "Summary", + icon: getDefaultSectionIconName("summary"), + content: "

Hello world

", + hidden: false, + showHeading: false, + columns: 1, + keepTogether: false, + startOnNewPage: false, + }; + + const paragraphs = renderSummary(summary, HEX); + + expect(paragraphs).toHaveLength(1); + }); + it("omits the heading when title is empty but still renders content", () => { const summary: ResumeData["summary"] = { title: "", icon: getDefaultSectionIconName("summary"), content: "

Hello world

", hidden: false, + showHeading: true, columns: 1, keepTogether: false, startOnNewPage: false, @@ -81,6 +102,7 @@ const emptySection = (type: T): ResumeData["sections"][T] icon: getDefaultSectionIconName(type), columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [], @@ -110,6 +132,7 @@ describe("renderCustomSection", () => { icon: getDefaultSectionIconName("summary"), columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [], @@ -147,6 +170,16 @@ describe("renderCustomSection", () => { expect(paragraphs.length).toBeGreaterThanOrEqual(2); }); + it("omits a disabled custom section heading while retaining content", () => { + const section: CustomSection = { + ...baseCustom, + showHeading: false, + items: [{ id: "x", hidden: false, content: "

Hello

" } as never], + }; + + expect(renderCustomSection(section, HEX)).toHaveLength(1); + }); + it("renders recipient + content for a cover-letter custom section", () => { const section: CustomSection = { ...baseCustom, diff --git a/packages/docx/src/section-renderers.ts b/packages/docx/src/section-renderers.ts index 16969f1d2..c277fe0e1 100644 --- a/packages/docx/src/section-renderers.ts +++ b/packages/docx/src/section-renderers.ts @@ -78,6 +78,14 @@ function sectionHeading(title: string, colorHex: string): Paragraph { }); } +function sectionHeadingParagraph( + section: T, + title: string, + colorHex: string, +): Paragraph[] { + return section.showHeading !== false && title ? [sectionHeading(title, colorHex)] : []; +} + function titleAndSubtitle(primary: string, secondary: string, rightText?: string, bold = true): Paragraph { const baseRun = getBaseRun(); const children: (TextRun | ExternalHyperlink)[] = [new TextRun({ text: primary, bold, ...baseRun })]; @@ -144,9 +152,7 @@ export function renderSummary(summary: ResumeData["summary"], colorHex: string): if (summary.hidden || !summary.content) return []; const paragraphs: Paragraph[] = []; - if (summary.title) { - paragraphs.push(sectionHeading(summary.title, colorHex)); - } + paragraphs.push(...sectionHeadingParagraph(summary, summary.title, colorHex)); paragraphs.push(...htmlToParagraphs(summary.content, getHtmlStyle())); return paragraphs; } @@ -155,7 +161,7 @@ function renderExperience(section: Sections["experience"], colorHex: string): Pa const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); const baseRun = getBaseRun(); for (const item of items) { @@ -201,7 +207,7 @@ function renderEducation(section: Sections["education"], colorHex: string): Para const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); const baseRun = getBaseRun(); for (const item of items) { @@ -234,7 +240,7 @@ function renderProjects(section: Sections["projects"], colorHex: string): Paragr const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); for (const item of items) { paragraphs.push(titleAndSubtitle(item.name, "", item.period)); @@ -254,7 +260,7 @@ function renderSkills(section: Sections["skills"], colorHex: string): Paragraph[ const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); const baseRun = getBaseRun(); for (const item of items) { @@ -285,7 +291,7 @@ function renderLanguages(section: Sections["languages"], colorHex: string): Para const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); const baseRun = getBaseRun(); for (const item of items) { @@ -305,7 +311,7 @@ function renderInterests(section: Sections["interests"], colorHex: string): Para const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); const baseRun = getBaseRun(); for (const item of items) { @@ -325,7 +331,7 @@ function renderAwards(section: Sections["awards"], colorHex: string): Paragraph[ const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); for (const item of items) { paragraphs.push(titleAndSubtitle(item.title, item.awarder, item.date, false)); @@ -345,7 +351,7 @@ function renderCertifications(section: Sections["certifications"], colorHex: str const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); for (const item of items) { paragraphs.push(titleAndSubtitle(item.title, item.issuer, item.date)); @@ -365,7 +371,7 @@ function renderPublications(section: Sections["publications"], colorHex: string) const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); for (const item of items) { paragraphs.push(titleAndSubtitle(item.title, item.publisher, item.date)); @@ -385,7 +391,7 @@ function renderVolunteer(section: Sections["volunteer"], colorHex: string): Para const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); for (const item of items) { paragraphs.push(titleAndSubtitle(item.organization, "", item.period)); @@ -408,7 +414,7 @@ function renderReferences(section: Sections["references"], colorHex: string): Pa const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); const baseRun = getBaseRun(); for (const item of items) { @@ -443,7 +449,7 @@ function renderProfiles(section: Sections["profiles"], colorHex: string): Paragr const items = section.items.filter((item) => !item.hidden); if (section.hidden || items.length === 0) return []; - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); const baseRun = getBaseRun(); for (const item of items) { @@ -502,7 +508,7 @@ export function renderCustomSection(section: CustomSection, colorHex: string): P // Summary-type custom sections render HTML content if (sectionType === "summary") { - const paragraphs: Paragraph[] = [sectionHeading(section.title, colorHex)]; + const paragraphs: Paragraph[] = sectionHeadingParagraph(section, section.title, colorHex); for (const item of visibleItems) { if ("content" in item && item.content) { paragraphs.push(...htmlToParagraphs(item.content, getHtmlStyle())); @@ -533,6 +539,7 @@ export function renderCustomSection(section: CustomSection, colorHex: string): P title: section.title, columns: section.columns, hidden: false, + showHeading: section.showHeading, items: visibleItems, } as Sections[SectionType]; diff --git a/packages/pdf/src/section-icon.test.ts b/packages/pdf/src/section-icon.test.ts index 08847054e..7f13565dc 100644 --- a/packages/pdf/src/section-icon.test.ts +++ b/packages/pdf/src/section-icon.test.ts @@ -109,6 +109,7 @@ describe("getResumeSectionIcon", () => { icon: "lightning", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [], @@ -129,6 +130,7 @@ describe("getResumeSectionIcon", () => { icon: "", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [], @@ -149,6 +151,7 @@ describe("getResumeSectionIcon", () => { icon: "", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [], @@ -169,6 +172,7 @@ describe("getResumeSectionIcon", () => { icon: "none", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [], diff --git a/packages/pdf/src/section-title.test.ts b/packages/pdf/src/section-title.test.ts index 03bd40dd1..7451287ce 100644 --- a/packages/pdf/src/section-title.test.ts +++ b/packages/pdf/src/section-title.test.ts @@ -112,6 +112,7 @@ describe("getResumeSectionTitle", () => { icon: "", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [], @@ -132,6 +133,7 @@ describe("getResumeSectionTitle", () => { icon: "", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [], diff --git a/packages/pdf/src/semantic/binding-inventory.test.ts b/packages/pdf/src/semantic/binding-inventory.test.ts index 37de5f4f4..395e6129f 100644 --- a/packages/pdf/src/semantic/binding-inventory.test.ts +++ b/packages/pdf/src/semantic/binding-inventory.test.ts @@ -239,6 +239,7 @@ describe("semantic binding inventory", () => { columns: 1, hidden: false, keepTogether: false, + showHeading: true, startOnNewPage: false, items: [ { diff --git a/packages/pdf/src/semantic/issue-fixtures.test.tsx b/packages/pdf/src/semantic/issue-fixtures.test.tsx index 0825b209f..f3cdd4265 100644 --- a/packages/pdf/src/semantic/issue-fixtures.test.tsx +++ b/packages/pdf/src/semantic/issue-fixtures.test.tsx @@ -242,6 +242,34 @@ describe("semantic issue fixtures", () => { expect(containsStyle(document, "opacity", 0.2)).toBe(true); }); + it("hides section heading decoration without hiding section content", async () => { + const data = buildIssueFixture(); + data.sections.experience.showHeading = 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; + + expect(findText(document, "Experience")).toBeUndefined(); + expect(findText(document, "Analytical Engines")).toBeDefined(); + }); + + it("characterizes section-heading display:none as a CSS alternative", async () => { + const data = buildIssueFixture(); + data.metadata.page.hideSectionIcons = false; + data.metadata.stylesheet = { + mode: "semantic", + source: source('@version 1; section[id="experience"] section-heading { display: none; }'), + }; + 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; + + expect(findText(document, "Experience")).toBeUndefined(); + expect(findText(document, "Analytical Engines")).toBeDefined(); + }); + it("unbolds only the final skill-name primitive and preserves the experience title weight (#2223)", async () => { const data = buildIssueFixture(); const stylesheet = source(` diff --git a/packages/pdf/src/semantic/legacy-parity.test.ts b/packages/pdf/src/semantic/legacy-parity.test.ts index 2e243b6ad..179e63819 100644 --- a/packages/pdf/src/semantic/legacy-parity.test.ts +++ b/packages/pdf/src/semantic/legacy-parity.test.ts @@ -133,6 +133,7 @@ const buildFixture = (rules: StyleRule[]): ResumeData => { columns: 1, hidden: false, keepTogether: false, + showHeading: true, startOnNewPage: false, items: [ { diff --git a/packages/pdf/src/semantic/legacy-render-parity.test.tsx b/packages/pdf/src/semantic/legacy-render-parity.test.tsx index 086d8003b..2c98e59be 100644 --- a/packages/pdf/src/semantic/legacy-render-parity.test.tsx +++ b/packages/pdf/src/semantic/legacy-render-parity.test.tsx @@ -126,6 +126,7 @@ const buildFixture = (rules: StyleRule[]): ResumeData => { columns: 1, hidden: false, keepTogether: false, + showHeading: true, startOnNewPage: false, items: [ { diff --git a/packages/pdf/src/semantic/tree.test.ts b/packages/pdf/src/semantic/tree.test.ts index 5432a869c..e95540840 100644 --- a/packages/pdf/src/semantic/tree.test.ts +++ b/packages/pdf/src/semantic/tree.test.ts @@ -316,6 +316,7 @@ const buildCompleteFixture = (): ResumeData => { icon: "star", columns: 1, hidden: true, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ diff --git a/packages/pdf/src/templates/shared/sections.tsx b/packages/pdf/src/templates/shared/sections.tsx index c45733c8f..4697d8e94 100644 --- a/packages/pdf/src/templates/shared/sections.tsx +++ b/packages/pdf/src/templates/shared/sections.tsx @@ -320,6 +320,11 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio const sectionHeadingContainerStyle = useTemplateStyle("sectionHeadingContainer"); const sectionHeadingRuleStyle = useSectionStyleRule("heading"); const sectionTitle = getResumeSectionTitle(data, sectionId, title); + const sectionHeadingEnabled = (() => { + if (sectionId === "summary") return data.summary.showHeading !== false; + if (sectionId in data.sections) return data.sections[sectionId as SectionType].showHeading !== false; + return data.customSections.find((section) => section.id === sectionId)?.showHeading !== false; + })(); const sectionHeadingNodeKey = semanticNodeKeys.sectionHeading(sectionNodeKey); const sectionHeadingResolved = useResolvedNode(sectionHeadingNodeKey); const sectionHeadingVisible = useSemanticNodeVisible(sectionHeadingNodeKey); @@ -343,7 +348,7 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio return ( - {showHeading && ( + {showHeading && sectionHeadingEnabled && ( {sectionTitle} )} {children} @@ -356,7 +361,7 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio return ( - {showHeading && sectionHeadingVisible && ( + {showHeading && sectionHeadingEnabled && sectionHeadingVisible && ( { + it("defaults heading visibility to true for legacy summary and section data", () => { + const legacySummary = { + title: "", + icon: "article", + columns: 1, + hidden: false, + keepTogether: false, + startOnNewPage: false, + content: "

Summary

", + }; + const legacySection = { + title: "", + icon: "briefcase", + columns: 1, + hidden: false, + keepTogether: false, + startOnNewPage: false, + items: [], + }; + + expect( + parseResumeData({ + ...defaultResumeData, + summary: legacySummary, + sections: { ...defaultResumeData.sections, experience: legacySection }, + }).summary.showHeading, + ).toBe(true); + expect( + parseResumeData({ + ...defaultResumeData, + summary: legacySummary, + sections: { ...defaultResumeData.sections, experience: legacySection }, + }).sections.experience.showHeading, + ).toBe(true); + }); + it("validates the default resume", () => { expect(resumeDataSchema.safeParse(defaultResumeData).success).toBe(true); }); diff --git a/packages/schema/src/resume/data.ts b/packages/schema/src/resume/data.ts index bfb8a27d8..269a78408 100644 --- a/packages/schema/src/resume/data.ts +++ b/packages/schema/src/resume/data.ts @@ -107,6 +107,11 @@ export const summarySchema = z.object({ ), columns: z.number().int().min(1).max(6).catch(1).describe("The number of columns the summary should span across."), hidden: z.boolean().describe("Whether to hide the summary from the resume."), + showHeading: z + .boolean() + .optional() + .catch(true) + .describe("Whether to show the summary heading, icon, and decoration while retaining summary content."), keepTogether: z .boolean() .catch(false) @@ -295,6 +300,11 @@ export const baseSectionSchema = z.object({ ), columns: z.number().int().min(1).max(6).catch(1).describe("The number of columns the section should span across."), hidden: z.boolean().describe("Whether to hide the section from the resume."), + showHeading: z + .boolean() + .optional() + .catch(true) + .describe("Whether to show the section heading, icon, and decoration while retaining section content."), keepTogether: z .boolean() .catch(false) @@ -694,7 +704,13 @@ export const resumeDataSchema = z.looseObject({ export type ResumeData = z.infer; -export const parseResumeData = (data: unknown): ResumeData => resumeDataSchema.parse(data); +export const parseResumeData = (data: unknown): ResumeData => { + const parsed = resumeDataSchema.parse(data); + parsed.summary.showHeading ??= true; + for (const section of Object.values(parsed.sections)) section.showHeading ??= true; + for (const section of parsed.customSections) section.showHeading ??= true; + return parsed; +}; export type LayoutPage = z.infer; export type Typography = z.infer; diff --git a/packages/schema/src/resume/default.ts b/packages/schema/src/resume/default.ts index 7b547698e..1c66f8d98 100644 --- a/packages/schema/src/resume/default.ts +++ b/packages/schema/src/resume/default.ts @@ -6,6 +6,7 @@ const section = (icon: string) => ({ icon, columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [] as never[], @@ -39,6 +40,7 @@ export const defaultResumeData: ResumeData = { icon: "article", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, content: "", diff --git a/packages/schema/src/resume/sample.ts b/packages/schema/src/resume/sample.ts index 3f4aeb0f0..145563f61 100644 --- a/packages/schema/src/resume/sample.ts +++ b/packages/schema/src/resume/sample.ts @@ -44,6 +44,7 @@ export const sampleResumeData: ResumeData = { icon: "article", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, content: @@ -55,6 +56,7 @@ export const sampleResumeData: ResumeData = { icon: "messenger-logo", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -91,6 +93,7 @@ export const sampleResumeData: ResumeData = { icon: "briefcase", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -117,6 +120,7 @@ export const sampleResumeData: ResumeData = { icon: "graduation-cap", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -144,6 +148,7 @@ export const sampleResumeData: ResumeData = { icon: "code-simple", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -195,6 +200,7 @@ export const sampleResumeData: ResumeData = { layout: "default", keywordLayout: "inline", hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -265,6 +271,7 @@ export const sampleResumeData: ResumeData = { icon: "translate", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -289,6 +296,7 @@ export const sampleResumeData: ResumeData = { icon: "football", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -331,6 +339,7 @@ export const sampleResumeData: ResumeData = { icon: "trophy", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -369,6 +378,7 @@ export const sampleResumeData: ResumeData = { icon: "certificate", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -405,6 +415,7 @@ export const sampleResumeData: ResumeData = { icon: "books", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -443,6 +454,7 @@ export const sampleResumeData: ResumeData = { icon: "hand-heart", columns: 2, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -481,6 +493,7 @@ export const sampleResumeData: ResumeData = { icon: "phone", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, items: [ @@ -506,6 +519,7 @@ export const sampleResumeData: ResumeData = { icon: "briefcase", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, id: "019becaf-0b87-769d-98a6-46ccf558c0e8", @@ -550,6 +564,7 @@ export const sampleResumeData: ResumeData = { icon: "envelope-simple", columns: 1, hidden: false, + showHeading: true, keepTogether: false, startOnNewPage: false, id: "019bef5b-0b3d-7e2a-8a7c-12d9e23a4f6b", diff --git a/skills/resume-builder/references/schema.md b/skills/resume-builder/references/schema.md index e9e9b46b9..217d9c489 100644 --- a/skills/resume-builder/references/schema.md +++ b/skills/resume-builder/references/schema.md @@ -65,6 +65,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `summary.icon` | `string` | yes | default: "" | Phosphor icon name to display before the summary section title in the PDF output. Empty string uses the default summary icon; 'none' hides the icon. | | `summary.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the summary should span across. | | `summary.hidden` | `boolean` | yes | — | Whether to hide the summary from the resume. | +| `summary.showHeading` | `boolean` | no | default: true | Whether to show the summary heading, icon, and decoration while retaining summary content. | | `summary.keepTogether` | `boolean` | yes | default: false | If true, the summary is kept on a single page instead of splitting across a page break. | | `summary.startOnNewPage` | `boolean` | yes | default: false | If true, the summary always begins on a new page. | | `summary.content` | `string` | yes | — | The content of the summary of the resume. This should be a HTML-formatted string. | @@ -74,6 +75,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.profiles.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.profiles.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.profiles.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.profiles.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.profiles.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.profiles.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.profiles.items` | `array` | yes | — | The items to display in the profiles section. | @@ -93,6 +95,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.experience.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.experience.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.experience.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.experience.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.experience.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.experience.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.experience.items` | `array` | yes | — | The items to display in the experience section. | @@ -119,6 +122,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.education.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.education.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.education.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.education.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.education.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.education.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.education.items` | `array` | yes | — | The items to display in the education section. | @@ -141,6 +145,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.projects.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.projects.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.projects.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.projects.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.projects.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.projects.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.projects.items` | `array` | yes | — | The items to display in the projects section. | @@ -159,6 +164,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.skills.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.skills.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.skills.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.skills.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.skills.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.skills.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.skills.items` | `array` | yes | — | The items to display in the skills section. | @@ -179,6 +185,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.languages.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.languages.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.languages.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.languages.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.languages.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.languages.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.languages.items` | `array` | yes | — | The items to display in the languages section. | @@ -193,6 +200,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.interests.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.interests.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.interests.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.interests.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.interests.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.interests.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.interests.items` | `array` | yes | — | The items to display in the interests section. | @@ -209,6 +217,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.awards.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.awards.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.awards.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.awards.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.awards.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.awards.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.awards.items` | `array` | yes | — | The items to display in the awards section. | @@ -228,6 +237,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.certifications.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.certifications.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.certifications.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.certifications.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.certifications.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.certifications.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.certifications.items` | `array` | yes | — | The items to display in the certifications section. | @@ -247,6 +257,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.publications.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.publications.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.publications.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.publications.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.publications.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.publications.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.publications.items` | `array` | yes | — | The items to display in the publications section. | @@ -266,6 +277,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.volunteer.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.volunteer.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.volunteer.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.volunteer.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.volunteer.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.volunteer.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.volunteer.items` | `array` | yes | — | The items to display in the volunteer section. | @@ -285,6 +297,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `sections.references.icon` | `string` | yes | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `sections.references.columns` | `integer` | yes | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `sections.references.hidden` | `boolean` | yes | — | Whether to hide the section from the resume. | +| `sections.references.showHeading` | `boolean` | no | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `sections.references.keepTogether` | `boolean` | yes | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `sections.references.startOnNewPage` | `boolean` | yes | default: false | If true, the section always begins on a new page. | | `sections.references.items` | `array` | yes | — | The items to display in the references section. | @@ -306,6 +319,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type summary, schema summaryItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type summary, schema summaryItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type summary, schema summaryItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type summary, schema summaryItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type summary, schema summaryItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type summary, schema summaryItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type summary, schema summaryItemSchema at customSections[]) | — | — | @@ -321,6 +335,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type profiles, schema profileItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type profiles, schema profileItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type profiles, schema profileItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type profiles, schema profileItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type profiles, schema profileItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type profiles, schema profileItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type profiles, schema profileItemSchema at customSections[]) | — | — | @@ -343,6 +358,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type experience, schema experienceItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type experience, schema experienceItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type experience, schema experienceItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type experience, schema experienceItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type experience, schema experienceItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type experience, schema experienceItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type experience, schema experienceItemSchema at customSections[]) | — | — | @@ -372,6 +388,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type education, schema educationItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type education, schema educationItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type education, schema educationItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type education, schema educationItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type education, schema educationItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type education, schema educationItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type education, schema educationItemSchema at customSections[]) | — | — | @@ -397,6 +414,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type projects, schema projectItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type projects, schema projectItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type projects, schema projectItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type projects, schema projectItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type projects, schema projectItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type projects, schema projectItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type projects, schema projectItemSchema at customSections[]) | — | — | @@ -418,6 +436,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type skills, schema skillItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type skills, schema skillItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type skills, schema skillItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type skills, schema skillItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type skills, schema skillItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type skills, schema skillItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `string` | no (type skills, schema skillItemSchema at customSections[]) | enum: ["inline","list"]; default: "inline" | How skill keywords are displayed: inline separated by commas, or one bullet per keyword. | @@ -439,6 +458,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type languages, schema languageItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type languages, schema languageItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type languages, schema languageItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type languages, schema languageItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type languages, schema languageItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type languages, schema languageItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type languages, schema languageItemSchema at customSections[]) | — | — | @@ -456,6 +476,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type interests, schema interestItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type interests, schema interestItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type interests, schema interestItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type interests, schema interestItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type interests, schema interestItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type interests, schema interestItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type interests, schema interestItemSchema at customSections[]) | — | — | @@ -475,6 +496,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type awards, schema awardItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type awards, schema awardItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type awards, schema awardItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type awards, schema awardItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type awards, schema awardItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type awards, schema awardItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type awards, schema awardItemSchema at customSections[]) | — | — | @@ -497,6 +519,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type certifications, schema certificationItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type certifications, schema certificationItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type certifications, schema certificationItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type certifications, schema certificationItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type certifications, schema certificationItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type certifications, schema certificationItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type certifications, schema certificationItemSchema at customSections[]) | — | — | @@ -519,6 +542,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type publications, schema publicationItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type publications, schema publicationItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type publications, schema publicationItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type publications, schema publicationItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type publications, schema publicationItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type publications, schema publicationItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type publications, schema publicationItemSchema at customSections[]) | — | — | @@ -541,6 +565,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type volunteer, schema volunteerItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type volunteer, schema volunteerItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type volunteer, schema volunteerItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type volunteer, schema volunteerItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type volunteer, schema volunteerItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type volunteer, schema volunteerItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type volunteer, schema volunteerItemSchema at customSections[]) | — | — | @@ -563,6 +588,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type references, schema referenceItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type references, schema referenceItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type references, schema referenceItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type references, schema referenceItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type references, schema referenceItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type references, schema referenceItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type references, schema referenceItemSchema at customSections[]) | — | — | @@ -585,6 +611,7 @@ Choose one coherent shape for each union value. Required fields are local to tha | `customSections[].icon` | `string` | yes (type cover-letter, schema coverLetterItemSchema at customSections[]) | default: "" | Phosphor icon name to display before the section title in the PDF output. Empty string uses the default section icon; 'none' hides the icon. | | `customSections[].columns` | `integer` | yes (type cover-letter, schema coverLetterItemSchema at customSections[]) | minimum: 1; maximum: 6; default: 1 | The number of columns the section should span across. | | `customSections[].hidden` | `boolean` | yes (type cover-letter, schema coverLetterItemSchema at customSections[]) | — | Whether to hide the section from the resume. | +| `customSections[].showHeading` | `boolean` | no (type cover-letter, schema coverLetterItemSchema at customSections[]) | default: true | Whether to show the section heading, icon, and decoration while retaining section content. | | `customSections[].keepTogether` | `boolean` | yes (type cover-letter, schema coverLetterItemSchema at customSections[]) | default: false | If true, the section is kept on a single page instead of splitting across a page break. | | `customSections[].startOnNewPage` | `boolean` | yes (type cover-letter, schema coverLetterItemSchema at customSections[]) | default: false | If true, the section always begins on a new page. | | `customSections[].keywordLayout` | `any` | no (type cover-letter, schema coverLetterItemSchema at customSections[]) | — | — | diff --git a/tooling/recovery/compare-resume.test.ts b/tooling/recovery/compare-resume.test.ts index 8e11e5908..81539432d 100644 --- a/tooling/recovery/compare-resume.test.ts +++ b/tooling/recovery/compare-resume.test.ts @@ -2,10 +2,10 @@ import { describe, expect, it } from "vitest"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; import { compareResumeRecovery } from "./compare-resume"; -const SYNTHETIC_SOURCE_HASH = "2fc24960657492304068db58c4f4f8b0861ef4937bf12aadeced12ed03856d41"; -const RECOVERED_COPY_HASH = "87b3c6293158e14591387969b804dda0233ae83df79e59043b340c9f0180bf6d"; -const CURRENT_COPY_HASH = "701ee9fde9161c974ee6325e169132249ccb77adb31d875541ab70dd20835017"; -const DEFAULT_RESUME_HASH = "bb0b5937876766c5a0d5014538257870396f30902d86a2d32d2c901f644ffb1f"; +const SYNTHETIC_SOURCE_HASH = "33bd2901474d077a37ed73f0646ac2812a0fbb6ca8ca7bd5825c56ec4c598b8c"; +const RECOVERED_COPY_HASH = "1c0e1eefac8375d15b5d0d5fc040a970d1f10242c31a224f3c2a0ea5750c719e"; +const CURRENT_COPY_HASH = "0e8a2e90ccb44068f500a92bc0d312b4290269f1b68ba925fdfe6109a15e739e"; +const DEFAULT_RESUME_HASH = "1870f56666738b8748ac2769f5c79fe7b19863efecc07142a34c425254d871d8"; const FORMAT_CHARACTERS = [ ["zero-width space (U+200B)", "\u200B"],