mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 10:13:47 +10:00
fix(pdf): wrap glalie contact text and center section heading icons
Glalie's contact box let long emails and links run past its border: the converter dropped a link's lone text into a shared inline run, losing its box styles, so Forme sized it to its full width. A lone text now converts as its own block, and Glalie's contact text takes the rest of the row so Forme wraps it inside the box. Section heading icons sat at the top of the title's line box, above the text. The icon now moves down by half the leftover line height and never exceeds the line, so it stays level with the title in every template.
This commit is contained in:
@@ -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 = [];
|
||||
|
||||
@@ -30,6 +30,7 @@ type GlalieStyles = Omit<TemplateStyleSlots, "page"> & {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -526,8 +526,13 @@ export const SectionHeadingIcon = ({
|
||||
style,
|
||||
size: sizeProp,
|
||||
nodeKey,
|
||||
titleLineHeight,
|
||||
...props
|
||||
}: ComponentProps<typeof PhosphorIcon> & { nodeKey?: string | undefined }) => {
|
||||
}: ComponentProps<typeof PhosphorIcon> & {
|
||||
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 = ({
|
||||
<PhosphorIcon
|
||||
{...iconPropsWithoutDisplay}
|
||||
{...props}
|
||||
{...(resolvedSize === undefined ? {} : { size: resolvedSize })}
|
||||
size={resolvedSize}
|
||||
{...(resolved.style?.color === undefined ? {} : { color: resolved.style.color })}
|
||||
style={composeStyles(asStyleInput(iconStyle), asStyleInput(style), resolved.style)}
|
||||
style={composeStyles(lineStyle, asStyleInput(iconStyle), asStyleInput(style), resolved.style)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
<SectionHeadingIcon
|
||||
nodeKey={semanticNodeKeys.icon(sectionHeadingNodeKey, "section")}
|
||||
name={sectionIcon as IconName}
|
||||
titleLineHeight={titleLineBox}
|
||||
/>
|
||||
<Heading
|
||||
bindSemanticNode={false}
|
||||
style={getSectionHeadingTextStyle(
|
||||
sectionHeadingStyle,
|
||||
sectionHeadingTextOf(sectionHeadingResolved.style),
|
||||
)}
|
||||
>
|
||||
<Heading bindSemanticNode={false} style={sectionHeadingTextStyle}>
|
||||
{sectionTitle}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
@@ -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 = (
|
||||
<SemanticContactListView style={styles.contactList}>
|
||||
<EmailContactItem email={basics.email} style={styles.contactItem} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.contactItem} />
|
||||
<LocationContactItem location={basics.location} style={styles.contactItem} />
|
||||
<WebsiteContactItem website={basics.website} style={styles.contactItem} />
|
||||
<EmailContactItem email={basics.email} style={styles.contactItem} textStyle={styles.contactText} />
|
||||
<PhoneContactItem phone={basics.phone} style={styles.contactItem} textStyle={styles.contactText} />
|
||||
<LocationContactItem location={basics.location} style={styles.contactItem} textStyle={styles.contactText} />
|
||||
<WebsiteContactItem website={basics.website} style={styles.contactItem} textStyle={styles.contactText} />
|
||||
{basics.customFields.map((field) => (
|
||||
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
|
||||
<CustomFieldContactItem
|
||||
key={field.id}
|
||||
field={field}
|
||||
style={styles.contactItem}
|
||||
textStyle={styles.contactText}
|
||||
/>
|
||||
))}
|
||||
</SemanticContactListView>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user