mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
feat: add hide link underline option to resume settings, resolves #3134
This commit is contained in:
@@ -258,6 +258,30 @@ function PageSectionForm() {
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="hideLinkUnderline">
|
||||
{(field) => (
|
||||
<FormItem
|
||||
className="col-span-full flex items-center gap-x-3 py-1"
|
||||
hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}
|
||||
>
|
||||
<FormControl
|
||||
render={
|
||||
<Switch
|
||||
checked={field.state.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.handleChange(checked);
|
||||
handleAutoSave("hideLinkUnderline", checked);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormLabel>
|
||||
<Trans>Hide Link Underline</Trans>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="hideIcons">
|
||||
{(field) => (
|
||||
<FormItem
|
||||
|
||||
@@ -227,6 +227,10 @@ describe("ReactiveResumeV4JSONImporter — broad section mapping", () => {
|
||||
expect(result.picture.borderWidth).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("maps v4 underline link preference to the v5 hide underline page setting", () => {
|
||||
expect(result.metadata.page.hideLinkUnderline).toBe(true);
|
||||
});
|
||||
|
||||
it("maps summary content", () => {
|
||||
expect(result.summary.content).toBe("<p>About me</p>");
|
||||
expect(result.summary.hidden).toBe(false);
|
||||
|
||||
@@ -744,6 +744,7 @@ export class ReactiveResumeV4JSONImporter {
|
||||
marginY: nonNegative(v4Data.metadata.page?.margin ?? 14),
|
||||
format: v4Data.metadata.page?.format ?? "a4",
|
||||
locale: "en-US",
|
||||
hideLinkUnderline: v4Data.metadata.typography?.underlineLinks === false,
|
||||
hideIcons: v4Data.metadata.typography?.hideIcons ?? false,
|
||||
hideSectionIcons: true,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
+2713
-152
File diff suppressed because it is too large
Load Diff
@@ -280,6 +280,13 @@ describe("pageSchema", () => {
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.hideSectionIcons).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults hideLinkUnderline to false when missing", () => {
|
||||
const { hideLinkUnderline: _, ...pageWithout } = defaultResumeData.metadata.page;
|
||||
const result = pageSchema.safeParse(pageWithout);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) expect(result.data.hideLinkUnderline).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("baseSectionSchema", () => {
|
||||
|
||||
@@ -469,6 +469,7 @@ export const pageSchema = z.object({
|
||||
.string()
|
||||
.describe("The locale of the page. Used for displaying pre-translated section headings, if not overridden.")
|
||||
.catch("en-US"),
|
||||
hideLinkUnderline: z.boolean().describe("Whether to hide the underlines of the links.").catch(false),
|
||||
hideIcons: z.boolean().describe("Whether to hide the item-level icons (skills, profiles, interests).").catch(false),
|
||||
hideSectionIcons: z
|
||||
.boolean()
|
||||
|
||||
@@ -20,6 +20,10 @@ describe("defaultResumeData", () => {
|
||||
expect(defaultResumeData.metadata.page.format).toBe("a4");
|
||||
});
|
||||
|
||||
it("shows link underlines by default", () => {
|
||||
expect(defaultResumeData.metadata.page.hideLinkUnderline).toBe(false);
|
||||
});
|
||||
|
||||
it("starts with no resume content", () => {
|
||||
expect(defaultResumeData.basics.name).toBe("");
|
||||
expect(defaultResumeData.summary.content).toBe("");
|
||||
|
||||
@@ -135,6 +135,7 @@ export const defaultResumeData: ResumeData = {
|
||||
marginY: 12,
|
||||
format: "a4",
|
||||
locale: "en-US",
|
||||
hideLinkUnderline: false,
|
||||
hideIcons: false,
|
||||
hideSectionIcons: true,
|
||||
},
|
||||
|
||||
@@ -567,6 +567,7 @@ export const sampleResumeData: ResumeData = {
|
||||
marginY: 16,
|
||||
format: "a4",
|
||||
locale: "en-US",
|
||||
hideLinkUnderline: false,
|
||||
hideIcons: false,
|
||||
hideSectionIcons: false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user