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"],