diff --git a/packages/pdf/src/forme/to-forme.tsx b/packages/pdf/src/forme/to-forme.tsx index fddd3d3dd..687d91643 100644 --- a/packages/pdf/src/forme/to-forme.tsx +++ b/packages/pdf/src/forme/to-forme.tsx @@ -518,6 +518,13 @@ function convertNode(node: HostNode, parentContext: Context, key: number): React let inline: HostNode[] = []; const flush = () => { if (inline.length === 0) return; + // A lone text is a block of its own, as in react-pdf: it keeps its box styles (how it flexes in a row). + const [only] = inline; + if (inline.length === 1 && only && only.type !== "#text") { + children.push(convertNode(only, inner, children.length)); + inline = []; + return; + } const runs = convertChildren(inline, { ...inner, inText: true }); children.push(createElement(FormeText, { key: children.length, style: context.textDefaults }, ...runs)); inline = []; diff --git a/packages/pdf/src/templates/glalie/GlaliePage.tsx b/packages/pdf/src/templates/glalie/GlaliePage.tsx index ec8e30df1..bbeb6e5a9 100644 --- a/packages/pdf/src/templates/glalie/GlaliePage.tsx +++ b/packages/pdf/src/templates/glalie/GlaliePage.tsx @@ -30,6 +30,7 @@ type GlalieStyles = Omit & { headerName: Style; contactList: Style; contactItem: Style; + contactText: Style; }; type GlalieTemplate = { @@ -122,6 +123,7 @@ const Header = ({ styles }: GlalieHeaderProps) => ( name: styles.headerName, contactList: styles.contactList, contactItem: styles.contactItem, + contactText: styles.contactText, }} contactListOutsideTitle /> @@ -221,6 +223,11 @@ const useGlalieTemplate = (): GlalieTemplate => { alignItems: "center", columnGap: metrics.gapX(1 / 6), }, + // The text takes the rest of the row, so a long email or link wraps inside the box instead of running out. + contactText: { + flexGrow: 1, + flexBasis: 0, + }, }); const accentFor = ({ colors }: TemplateStyleContext) => colors.primary; diff --git a/packages/pdf/src/templates/shared/contact-item.tsx b/packages/pdf/src/templates/shared/contact-item.tsx index 729a47380..6fecc42f0 100644 --- a/packages/pdf/src/templates/shared/contact-item.tsx +++ b/packages/pdf/src/templates/shared/contact-item.tsx @@ -32,7 +32,7 @@ const useContactNodeKeys = (name: string, id?: string, primitiveNodeKey?: string type WebsiteContactItemProps = { website: WebsiteDisplay; style?: ContactStyle; - textStyle?: ContactStyle; + textStyle?: ContactStyle | undefined; iconColor?: string; primitiveNodeKey?: string | undefined; }; @@ -40,7 +40,7 @@ type WebsiteContactItemProps = { type CustomFieldContactItemProps = { field: CustomField; style?: ContactStyle; - textStyle?: ContactStyle; + textStyle?: ContactStyle | undefined; iconColor?: string; primitiveNodeKey?: string | undefined; }; @@ -105,7 +105,7 @@ export const CustomFieldContactItem = ({ type EmailContactItemProps = { email: string; style?: ContactStyle; - textStyle?: ContactStyle; + textStyle?: ContactStyle | undefined; iconColor?: string; /** Override icon; defaults to "envelope". ditgar uses "at". */ iconName?: IconName; @@ -136,7 +136,7 @@ export const EmailContactItem = ({ type PhoneContactItemProps = { phone: string; style?: ContactStyle; - textStyle?: ContactStyle; + textStyle?: ContactStyle | undefined; iconColor?: string; primitiveNodeKey?: string | undefined; }; @@ -158,7 +158,7 @@ export const PhoneContactItem = ({ phone, style, textStyle, iconColor, primitive type LocationContactItemProps = { location: string; style?: ContactStyle; - textStyle?: ContactStyle; + textStyle?: ContactStyle | undefined; iconColor?: string; primitiveNodeKey?: string | undefined; }; diff --git a/packages/pdf/src/templates/shared/primitives.tsx b/packages/pdf/src/templates/shared/primitives.tsx index df8c08b4f..ce34d0fcb 100644 --- a/packages/pdf/src/templates/shared/primitives.tsx +++ b/packages/pdf/src/templates/shared/primitives.tsx @@ -526,8 +526,13 @@ export const SectionHeadingIcon = ({ style, size: sizeProp, nodeKey, + titleLineHeight, ...props -}: ComponentProps & { nodeKey?: string | undefined }) => { +}: ComponentProps & { + nodeKey?: string | undefined; + /** Height in points of a line of the title beside the icon, which the icon is centred on. */ + titleLineHeight?: number | undefined; +}) => { const data = useRender(); const { style: sectionIconStyle, ...sectionIconProps } = useTemplateIconSlot("sectionHeadingIcon"); const { style: fallbackIconStyle, ...fallbackIconProps } = useTemplateIconSlot("icon"); @@ -545,13 +550,20 @@ export const SectionHeadingIcon = ({ // Icon size follows heading fontSize so they scale together const headingFontSize = data.metadata.typography.heading.fontSize; - const resolvedSize = + let resolvedSize = resolveIconSize({ size: sizeProp, styles: [asStyleInput(iconStyle), asStyleInput(style)], }) ?? templateIconSize ?? headingFontSize; + // The row lines its children up at the top, and a line's text sits in the middle of its line height: the icon + // moves down by half what's left of the line, and never grows past the line, so it stays level with the title. + let lineStyle: Style | undefined; + if (typeof resolvedSize === "number" && titleLineHeight !== undefined) { + resolvedSize = Math.min(resolvedSize, titleLineHeight); + lineStyle = { marginTop: (titleLineHeight - resolvedSize) / 2 }; + } const resolved = useResolvedNode(nodeKey); const visible = useSemanticNodeVisible(nodeKey); if (!visible) return null; @@ -560,9 +572,9 @@ export const SectionHeadingIcon = ({ ); }; diff --git a/packages/pdf/src/templates/shared/sections.tsx b/packages/pdf/src/templates/shared/sections.tsx index 47df3b751..76dc4ef01 100644 --- a/packages/pdf/src/templates/shared/sections.tsx +++ b/packages/pdf/src/templates/shared/sections.tsx @@ -54,6 +54,7 @@ import { useTemplateStyle, } from "./context"; import { filterItems, hasVisibleItems, isSectionVisible, isVisibleSummary } from "./filtering"; +import { parseStyleFontSize } from "./icon-size"; import { LevelDisplay } from "./level-display"; import { getTemplateMetrics } from "./metrics"; import { @@ -339,6 +340,7 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio const resolved = useResolvedNode(sectionNodeKey); const visible = useSemanticNodeVisible(sectionNodeKey); const sectionStyle = useTemplateStyle("section"); + const headingStyle = useTemplateStyle("heading"); const sectionHeadingStyle = useTemplateStyle("sectionHeading"); const sectionHeadingContainerStyle = useTemplateStyle("sectionHeadingContainer"); const sectionTitle = getResumeSectionTitle(data, sectionId, title); @@ -352,6 +354,15 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio const sectionHeadingVisible = useSemanticNodeVisible(sectionHeadingNodeKey); const sectionIcon = getResumeSectionIcon(data, sectionId); const showIcon = Boolean(sectionIcon) && !data.metadata.page.hideSectionIcons; + const sectionHeadingTextStyle = getSectionHeadingTextStyle( + sectionHeadingStyle, + sectionHeadingTextOf(sectionHeadingResolved.style), + ); + // The title as `Heading` draws it; a line height in other units than a multiplier falls back to the typography's. + const { fontSize: titleFontSize, lineHeight: titleLineHeight } = mergeStyles(headingStyle, sectionHeadingTextStyle); + const titleLineBox = + (parseStyleFontSize(titleFontSize) ?? data.metadata.typography.heading.fontSize) * + (typeof titleLineHeight === "number" ? titleLineHeight : data.metadata.typography.heading.lineHeight); const { keepTogether, startOnNewPage } = getSectionBreaks(data, sectionId); // wrap={false} keeps the whole section on one page; break forces it onto a fresh page. // Only set the props when enabled so we never pass undefined (exactOptionalPropertyTypes). @@ -396,14 +407,9 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio - + {sectionTitle} diff --git a/packages/pdf/src/templates/shared/template-base.tsx b/packages/pdf/src/templates/shared/template-base.tsx index bbe9396b5..47d38ff0f 100644 --- a/packages/pdf/src/templates/shared/template-base.tsx +++ b/packages/pdf/src/templates/shared/template-base.tsx @@ -55,6 +55,7 @@ type TemplateHeaderStyles = { name: Style; contactList: Style; contactItem: Style; + contactText?: Style | undefined; }; export const TemplateHeader = ({ @@ -68,12 +69,17 @@ export const TemplateHeader = ({ const hasPicture = hasTemplatePicture(picture); const contactList = ( - - - - + + + + {basics.customFields.map((field) => ( - + ))} );