feat: add section heading icons to PDF templates (#3127)

* feat: add section heading icons to PDF templates

Add customizable Phosphor icons before section titles in PDF output.
Users can toggle visibility globally via a new "Hide section heading icons"
switch (independent of item-level icons) and customize individual section
icons through the builder sidebar icon picker.

- Add `icon` field to `baseSectionSchema` and `summarySchema`
- Add `hideSectionIcons` to `pageSchema` (defaults to true for backward compat)
- Implement `SectionHeadingIcon` component with heading font-size scaling
- Support "none" sentinel for per-section icon hiding
- Fallback to sensible defaults (briefcase, graduation-cap, etc.) for legacy data
- Add icon picker to builder sidebar sections and custom section dialogs

Closes #2632

* test: add unit tests for section heading icons

- Add tests for getResumeSectionIcon() covering built-in sections,
  summary, custom sections, "none" sentinel, and default fallbacks
- Add schema tests for baseSectionSchema icon field, summarySchema icon,
  and pageSchema hideSectionIcons default behavior

* refactor: minor updates to icon display

* Update apps/web/locales/es-ES.po

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
JamesGoslings
2026-06-01 14:02:42 +02:00
committed by GitHub
co-authored by coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Amruth Pillai
parent 1522794733
commit b932711f08
81 changed files with 1153 additions and 306 deletions
+177
View File
@@ -0,0 +1,177 @@
import { describe, expect, it } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { getResumeSectionIcon } from "./section-icon";
describe("getResumeSectionIcon", () => {
describe("built-in sections", () => {
it("returns the icon from section data when set", () => {
const data = {
...defaultResumeData,
sections: {
...defaultResumeData.sections,
experience: { ...defaultResumeData.sections.experience, icon: "rocket" },
},
};
expect(getResumeSectionIcon(data, "experience")).toBe("rocket");
});
it("falls back to default icon when section icon is empty string", () => {
const data = {
...defaultResumeData,
sections: {
...defaultResumeData.sections,
experience: { ...defaultResumeData.sections.experience, icon: "" },
},
};
expect(getResumeSectionIcon(data, "experience")).toBe("briefcase");
});
it("returns empty string when icon is 'none' (user explicitly hid it)", () => {
const data = {
...defaultResumeData,
sections: {
...defaultResumeData.sections,
skills: { ...defaultResumeData.sections.skills, icon: "none" },
},
};
expect(getResumeSectionIcon(data, "skills")).toBe("");
});
it("returns correct default icons for all built-in sections", () => {
const data = {
...defaultResumeData,
sections: {
profiles: { ...defaultResumeData.sections.profiles, icon: "" },
experience: { ...defaultResumeData.sections.experience, icon: "" },
education: { ...defaultResumeData.sections.education, icon: "" },
projects: { ...defaultResumeData.sections.projects, icon: "" },
skills: { ...defaultResumeData.sections.skills, icon: "" },
languages: { ...defaultResumeData.sections.languages, icon: "" },
interests: { ...defaultResumeData.sections.interests, icon: "" },
awards: { ...defaultResumeData.sections.awards, icon: "" },
certifications: { ...defaultResumeData.sections.certifications, icon: "" },
publications: { ...defaultResumeData.sections.publications, icon: "" },
volunteer: { ...defaultResumeData.sections.volunteer, icon: "" },
references: { ...defaultResumeData.sections.references, icon: "" },
},
};
expect(getResumeSectionIcon(data, "profiles")).toBe("messenger-logo");
expect(getResumeSectionIcon(data, "experience")).toBe("briefcase");
expect(getResumeSectionIcon(data, "education")).toBe("graduation-cap");
expect(getResumeSectionIcon(data, "projects")).toBe("code-simple");
expect(getResumeSectionIcon(data, "skills")).toBe("compass-tool");
expect(getResumeSectionIcon(data, "languages")).toBe("translate");
expect(getResumeSectionIcon(data, "interests")).toBe("football");
expect(getResumeSectionIcon(data, "awards")).toBe("trophy");
expect(getResumeSectionIcon(data, "certifications")).toBe("certificate");
expect(getResumeSectionIcon(data, "publications")).toBe("books");
expect(getResumeSectionIcon(data, "volunteer")).toBe("hand-heart");
expect(getResumeSectionIcon(data, "references")).toBe("phone");
});
});
describe("summary section", () => {
it("returns the icon from summary data when set", () => {
const data = {
...defaultResumeData,
summary: { ...defaultResumeData.summary, icon: "notebook" },
};
expect(getResumeSectionIcon(data, "summary")).toBe("notebook");
});
it("falls back to default 'article' when summary icon is empty", () => {
const data = {
...defaultResumeData,
summary: { ...defaultResumeData.summary, icon: "" },
};
expect(getResumeSectionIcon(data, "summary")).toBe("article");
});
it("returns empty when summary icon is 'none'", () => {
const data = {
...defaultResumeData,
summary: { ...defaultResumeData.summary, icon: "none" },
};
expect(getResumeSectionIcon(data, "summary")).toBe("");
});
});
describe("custom sections", () => {
it("returns the icon from custom section data when set", () => {
const data = {
...defaultResumeData,
customSections: [
{
id: "custom-1",
type: "experience" as const,
title: "Freelance",
icon: "lightning",
columns: 1,
hidden: false,
items: [],
},
],
};
expect(getResumeSectionIcon(data, "custom-1")).toBe("lightning");
});
it("falls back to the base type default when custom section icon is empty", () => {
const data = {
...defaultResumeData,
customSections: [
{
id: "custom-1",
type: "education" as const,
title: "Courses",
icon: "",
columns: 1,
hidden: false,
items: [],
},
],
};
expect(getResumeSectionIcon(data, "custom-1")).toBe("graduation-cap");
});
it("falls back to the cover letter default for cover-letter custom sections", () => {
const data = {
...defaultResumeData,
customSections: [
{
id: "custom-cover-letter",
type: "cover-letter" as const,
title: "Cover Letter",
icon: "",
columns: 1,
hidden: false,
items: [],
},
],
};
expect(getResumeSectionIcon(data, "custom-cover-letter")).toBe("envelope-simple");
});
it("returns empty when custom section icon is 'none'", () => {
const data = {
...defaultResumeData,
customSections: [
{
id: "custom-1",
type: "experience" as const,
title: "Side Projects",
icon: "none",
columns: 1,
hidden: false,
items: [],
},
],
};
expect(getResumeSectionIcon(data, "custom-1")).toBe("");
});
it("returns empty string for non-existent section id", () => {
expect(getResumeSectionIcon(defaultResumeData, "non-existent-id")).toBe("");
});
});
});