feat: add hide link underline option to resume settings, resolves #3134

This commit is contained in:
Amruth Pillai
2026-06-01 15:30:49 +02:00
parent d6a9bc6c4b
commit 5fb4976ec9
16 changed files with 2820 additions and 157 deletions
@@ -4,6 +4,13 @@ import { describe, expect, it } from "vitest";
const source = readFileSync(fileURLToPath(new URL("./primitives.tsx", import.meta.url)), "utf8");
describe("Link", () => {
it("passes the resume page underline preference to shared link styles", () => {
expect(source).toContain("metadata.page.hideLinkUnderline");
expect(source).toContain("hideUnderline: metadata.page.hideLinkUnderline");
});
});
describe("SectionHeadingIcon", () => {
it("passes the resolved heading icon size through the icon size prop", () => {
expect(source).toContain("size: sizeProp");
@@ -34,10 +34,22 @@ export const Heading = ({ style, ...props }: ComponentProps<typeof PdfText>) =>
};
export const Link = ({ style, ...props }: ComponentProps<typeof PdfLink>) => {
const { metadata } = useRender();
const linkStyle = useTemplateStyle("link");
const linkRuleStyle = useSectionStyleRule("link");
return <PdfLink style={composeLinkStyles(linkStyle, linkRuleStyle, asStyleInput(style), safeTextStyle)} {...props} />;
return (
<PdfLink
style={composeLinkStyles(
{ hideUnderline: metadata.page.hideLinkUnderline },
linkStyle,
linkRuleStyle,
asStyleInput(style),
safeTextStyle,
)}
{...props}
/>
);
};
export const Small = ({ style, ...props }: ComponentProps<typeof PdfText>) => {
@@ -11,6 +11,7 @@ type RichTextProseSpacing = {
type CreateRichTextStylesheetOptions = {
boldStyle?: StyleInput;
hideLinkUnderline?: boolean;
linkStyle?: StyleInput;
richParagraphStyle?: StyleInput;
richParagraphRuleStyle?: StyleInput;
@@ -32,6 +33,7 @@ const emptyProseSpacing: RichTextProseSpacing = {
export const createRichTextStylesheet = ({
boldStyle,
hideLinkUnderline,
linkStyle,
richParagraphStyle,
richParagraphRuleStyle,
@@ -48,5 +50,5 @@ export const createRichTextStylesheet = ({
li: mergeStyles(proseSpacing.listItem),
[`.${richTextMarkClassName}`]: mergeStyles(richMarkStyle, richMarkRuleStyle, safeTextStyle),
p: mergeStyles(richParagraphStyle, richParagraphRuleStyle, safeTextStyle, proseSpacing.paragraph),
a: mergeLinkStyles(linkStyle, richLinkRuleStyle, safeTextStyle),
a: mergeLinkStyles({ hideUnderline: hideLinkUnderline === true }, linkStyle, richLinkRuleStyle, safeTextStyle),
});
@@ -66,4 +66,13 @@ describe("createRichTextStylesheet", () => {
expect(stylesheet.ol).toEqual({ rowGap: 8 });
expect(stylesheet.li).toEqual({ marginTop: 2, marginBottom: 2 });
});
it("can force rich text links to render without underlines", () => {
const stylesheet = createRichTextStylesheet({
hideLinkUnderline: true,
richLinkRuleStyle: { textDecoration: "underline", textDecorationStyle: "dotted" },
});
expect(stylesheet.a).toMatchObject({ textDecoration: "none", textDecorationStyle: "dotted" });
});
});
@@ -58,7 +58,7 @@ const applyRtlDirectionRecursively = (node: ReactNode): ReactNode => {
};
export const RichText = ({ children }: RichTextProps) => {
const { rtl } = useRender();
const { metadata, rtl } = useRender();
const rtlTextWrapStyle: Style | undefined = rtl ? { direction: "rtl", textAlign: "right" } : undefined;
const boldStyle = useTemplateStyle("bold");
const linkStyle = useTemplateStyle("link");
@@ -169,6 +169,7 @@ export const RichText = ({ children }: RichTextProps) => {
}}
stylesheet={createRichTextStylesheet({
boldStyle,
hideLinkUnderline: metadata.page.hideLinkUnderline,
linkStyle,
richParagraphStyle,
richParagraphRuleStyle,
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { composeLinkStyles, mergeLinkStyles } from "./styles";
describe("link styles", () => {
it("underlines links by default", () => {
expect(composeLinkStyles({}, { color: "#111" }).at(-1)).toEqual({ textDecoration: "underline" });
expect(mergeLinkStyles({}, { color: "#111" })).toMatchObject({ color: "#111", textDecoration: "underline" });
});
it("forces links to render without underlines when requested", () => {
expect(composeLinkStyles({ hideUnderline: true }, { textDecoration: "underline" }).at(-1)).toEqual({
textDecoration: "none",
});
expect(mergeLinkStyles({ hideUnderline: true }, { textDecoration: "underline" })).toMatchObject({
textDecoration: "none",
});
});
});
+12 -2
View File
@@ -4,6 +4,10 @@ export type TemplatePlacement = "main" | "sidebar";
export type StyleInput = Style | Style[] | null | undefined;
type LinkStyleOptions = {
hideUnderline?: boolean;
};
export const composeStyles = (...styles: StyleInput[]): Style[] => {
return styles.flatMap((style) => {
if (!style) return [];
@@ -14,12 +18,18 @@ export const composeStyles = (...styles: StyleInput[]): Style[] => {
};
const linkUnderlineStyle = { textDecoration: "underline" } satisfies Style;
const linkNoUnderlineStyle = { textDecoration: "none" } satisfies Style;
export const composeLinkStyles = (...styles: StyleInput[]): Style[] => composeStyles(...styles, linkUnderlineStyle);
const resolveLinkDecorationStyle = ({ hideUnderline = false }: LinkStyleOptions = {}) =>
hideUnderline ? linkNoUnderlineStyle : linkUnderlineStyle;
export const composeLinkStyles = (options: LinkStyleOptions = {}, ...styles: StyleInput[]): Style[] =>
composeStyles(...styles, resolveLinkDecorationStyle(options));
export const mergeStyles = (...styles: StyleInput[]): Style => Object.assign({}, ...composeStyles(...styles));
export const mergeLinkStyles = (...styles: StyleInput[]): Style => mergeStyles(...styles, linkUnderlineStyle);
export const mergeLinkStyles = (options: LinkStyleOptions = {}, ...styles: StyleInput[]): Style =>
mergeStyles(...styles, resolveLinkDecorationStyle(options));
// Increased from 1.2 to 1.3 to prevent descenders (g, p, y, etc.) from being
// clipped by the overflow:hidden applied in safeTextStyle on all Heading elements.