diff --git a/packages/pdf/src/templates/shared/meta-line.tsx b/packages/pdf/src/templates/shared/meta-line.tsx deleted file mode 100644 index 54ff154cb..000000000 --- a/packages/pdf/src/templates/shared/meta-line.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Small } from "./primitives"; - -export const MetaLine = ({ children }: { children: Array }) => { - const content = children.filter(Boolean).join(" • "); - - if (!content) return null; - - return {content}; -}; diff --git a/packages/pdf/src/templates/shared/sections.tsx b/packages/pdf/src/templates/shared/sections.tsx index 2a6432a9f..5e1dd907c 100644 --- a/packages/pdf/src/templates/shared/sections.tsx +++ b/packages/pdf/src/templates/shared/sections.tsx @@ -35,11 +35,11 @@ import { } from "./context"; import { filterItems, hasVisibleItems, isSectionVisible, isVisibleSummary } from "./filtering"; import { LevelDisplay } from "./level-display"; -import { MetaLine } from "./meta-line"; import { getTemplateMetrics } from "./metrics"; import { Bold, Div, Heading, Icon, Link, Small, Text } from "./primitives"; import { RichText } from "./rich-text"; import { getInlineItemWebsiteUrl, shouldRenderSeparateItemWebsite } from "./section-links"; +import { hasSplitRowText, promoteSplitRowRight } from "./split-row"; import { composeStyles } from "./styles"; type SectionItemsContextValue = { @@ -195,8 +195,8 @@ const awardTitleDateRowStyle = { } satisfies Style; const useSectionSplitRowStyle = () => { - const splitRowStyle = useTemplateStyle("splitRow"); const placement = useTemplatePlacement(); + const splitRowStyle = useTemplateStyle("splitRow"); const stackSidebarItemHeader = useTemplateFeature("stackSidebarItemHeader"); return composeStyles( @@ -331,6 +331,10 @@ const ExperienceSection = ({ {items.map((item) => { const hasPosition = Boolean(item.position.trim()); const hasLocation = Boolean(item.location.trim()); + const { top: headerLocation, bottom: headerPeriod } = promoteSplitRowRight({ + top: item.location, + bottom: item.period, + }); const renderInlineHeader = () => ( {item.company} - {item.location} + {hasSplitRowText(headerLocation) && ( + {headerLocation} + )} - {item.roles.length === 0 && ( + {item.roles.length === 0 && (hasPosition || hasSplitRowText(headerPeriod)) && ( - {item.position} - {item.period} + {hasPosition && {item.position}} + {hasSplitRowText(headerPeriod) && {headerPeriod}} )} @@ -368,7 +374,7 @@ const ExperienceSection = ({ {inlineItemHeader ? renderInlineHeader() : renderSplitHeader()} - {item.roles.length > 0 && {[item.period]}} + {item.roles.length > 0 && {item.period}} {item.roles.map((role) => ( @@ -416,6 +422,10 @@ const EducationSection = ({ const gradeAndLocation = [item.grade, item.location].filter(Boolean).join(" • "); const hasArea = Boolean(item.area.trim()); const hasDegree = Boolean(item.degree.trim()); + const { top: headerDegreeAndGrade, bottom: headerLocationAndPeriod } = promoteSplitRowRight({ + top: degreeAndGrade, + bottom: locationAndPeriod, + }); const renderInlineHeader = () => ( <> @@ -440,13 +450,19 @@ const EducationSection = ({ <> {item.school} - {degreeAndGrade} + {hasSplitRowText(headerDegreeAndGrade) && ( + {headerDegreeAndGrade} + )} - - {item.area} - {locationAndPeriod} - + {(hasArea || hasSplitRowText(headerLocationAndPeriod)) && ( + + {hasArea && {item.area}} + {hasSplitRowText(headerLocationAndPeriod) && ( + {headerLocationAndPeriod} + )} + + )} ); @@ -629,7 +645,7 @@ const AwardsSection = ({ {item.title} - {item.date} + {item.date} {item.awarder} @@ -653,6 +669,8 @@ const CertificationsSection = ({ const data = useRender(); const certifications = sectionData ?? data.sections.certifications; const items = getVisibleItems(certifications, "certifications"); + const splitRowStyle = useSectionSplitRowStyle(); + const alignRightStyle = useTemplateStyle("alignRight"); if (items.length === 0) return null; @@ -662,10 +680,13 @@ const CertificationsSection = ({ {items.map((item) => ( - {item.title} + + {item.title} + {item.date} + {item.issuer} - {item.date} + {item.description} @@ -686,6 +707,8 @@ const PublicationsSection = ({ const data = useRender(); const publications = sectionData ?? data.sections.publications; const items = getVisibleItems(publications, "publications"); + const splitRowStyle = useSectionSplitRowStyle(); + const alignRightStyle = useTemplateStyle("alignRight"); if (items.length === 0) return null; @@ -695,10 +718,14 @@ const PublicationsSection = ({ {items.map((item) => ( - {item.title} + + {item.title} + {item.date} + + {item.publisher} - {item.date} + {item.description} @@ -728,31 +755,35 @@ const VolunteerSection = ({ return ( - {items.map((item) => ( - - - {inlineItemHeader ? ( - {item.location}} - middle={{item.organization}} - trailing={{item.period}} - /> - ) : ( - <> - - {item.organization} - {item.location} - + {items.map((item) => { + return ( + + + {inlineItemHeader ? ( + {item.location} : null} + middle={{item.organization}} + trailing={{item.period}} + /> + ) : ( + <> + + {item.organization} + {hasSplitRowText(item.period) && ( + {item.period} + )} + - {[item.period]} - - )} - - {item.description} + {item.location} + + )} + + {item.description} - - - ))} + + + ); + })} ); @@ -779,7 +810,7 @@ const ReferencesSection = ({ {item.name} {item.position} - {[item.phone]} + {item.phone} {item.description} diff --git a/packages/pdf/src/templates/shared/split-row.test.ts b/packages/pdf/src/templates/shared/split-row.test.ts new file mode 100644 index 000000000..468c1f5a2 --- /dev/null +++ b/packages/pdf/src/templates/shared/split-row.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { hasSplitRowText, promoteSplitRowRight } from "./split-row"; + +describe("hasSplitRowText", () => { + it("returns true only for non-empty text", () => { + expect(hasSplitRowText("2019 - 2024")).toBe(true); + expect(hasSplitRowText(" ")).toBe(false); + expect(hasSplitRowText(undefined)).toBe(false); + }); +}); + +describe("promoteSplitRowRight", () => { + it("keeps both right cells when the top-right cell has content", () => { + expect(promoteSplitRowRight({ top: "Remote", bottom: "2019 - 2024" })).toEqual({ + top: "Remote", + bottom: "2019 - 2024", + }); + }); + + it("moves bottom-right content to the top right when top-right content is missing", () => { + expect(promoteSplitRowRight({ top: "", bottom: "2019 - 2024" })).toEqual({ top: "2019 - 2024", bottom: "" }); + }); + + it("treats whitespace-only cells as missing", () => { + expect(promoteSplitRowRight({ top: " ", bottom: "\t" })).toEqual({ top: "", bottom: "" }); + }); +}); diff --git a/packages/pdf/src/templates/shared/split-row.ts b/packages/pdf/src/templates/shared/split-row.ts new file mode 100644 index 000000000..a4a61cfcf --- /dev/null +++ b/packages/pdf/src/templates/shared/split-row.ts @@ -0,0 +1,11 @@ +export const hasSplitRowText = (value: string | undefined): value is string => { + return typeof value === "string" && value.trim().length > 0; +}; + +type SplitRowContent = { top: string; bottom: string }; + +export const promoteSplitRowRight = ({ top, bottom }: SplitRowContent): SplitRowContent => { + if (hasSplitRowText(top)) return { top, bottom: hasSplitRowText(bottom) ? bottom : "" }; + + return { top: hasSplitRowText(bottom) ? bottom : "", bottom: "" }; +};