fix(pdf): apply custom style fontSize to icons and level indicators (#3120) and

* fix(pdf): apply custom style fontSize to icon and level indicator sizes

Map fontSize from Icon and Level Indicator custom style slots to Phosphor
icon size and level indicator dimensions, since react-pdf icons ignore
fontSize in favor of the size prop.

* fix: separate global icon and scoped level indicator font sizes

Icon slot fontSize now drives all resume icons plus level display
decorations. Level indicator fontSize overrides only within level display.
Shared sizing logic lives in schema; design sidebar preview uses global rules.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Amruth Pillai
2026-05-29 00:41:21 +02:00
committed by GitHub
parent c1d11236ae
commit 1414fecade
12 changed files with 350 additions and 72 deletions
@@ -71,6 +71,21 @@ describe("LevelDisplay", () => {
expect(wrapper.getAttribute("aria-label")).toContain("4");
});
it("applies custom decoration and icon sizes in pixels", () => {
const { container } = render(
<LevelDisplay type="circle" icon="star" level={2} decorationSizePx={18} iconSizePx={20} />,
);
const shapes = container.querySelectorAll("div[data-active]");
expect(shapes[0]).toHaveStyle({ width: "18px", height: "18px" });
const { container: iconContainer } = render(
<LevelDisplay type="icon" icon="star" level={2} decorationSizePx={16} />,
);
const icon = iconContainer.querySelector("i");
expect(icon).toHaveStyle({ fontSize: "16px", width: "16px", height: "16px" });
});
it("merges extra className into the wrapper", () => {
const { container } = render(<LevelDisplay type="circle" icon="star" level={1} className="extra" />);
const wrapper = container.firstChild as HTMLElement;
+36 -5
View File
@@ -3,14 +3,35 @@ import type z from "zod";
import { t } from "@lingui/core/macro";
import { cn } from "@reactive-resume/utils/style";
type Props = z.infer<typeof levelDesignSchema> & React.ComponentProps<"div"> & { level: number };
type Props = z.infer<typeof levelDesignSchema> &
React.ComponentProps<"div"> & {
level: number;
decorationSizePx?: number | undefined;
iconSizePx?: number | undefined;
};
const LEVEL_ITEM_KEYS = ["level-1", "level-2", "level-3", "level-4", "level-5"] as const;
export function LevelDisplay({ icon, type, level, className, ...props }: Props) {
const defaultDecorationClassName = "size-2.5";
export function LevelDisplay({ icon, type, level, className, decorationSizePx, iconSizePx, ...props }: Props) {
if (level === 0) return null;
if (type === "hidden" || icon === "") return null;
const decorationStyle =
decorationSizePx === undefined
? undefined
: ({ width: decorationSizePx, height: decorationSizePx } satisfies React.CSSProperties);
const resolvedIconSizePx = iconSizePx ?? decorationSizePx;
const iconStyle =
resolvedIconSizePx === undefined
? undefined
: ({
fontSize: resolvedIconSizePx,
width: resolvedIconSizePx,
height: resolvedIconSizePx,
} satisfies React.CSSProperties);
return (
<div
role="img"
@@ -29,8 +50,10 @@ export function LevelDisplay({ icon, type, level, className, ...props }: Props)
<div
key={itemKey}
data-active={isActive}
style={decorationStyle}
className={cn(
"h-2.5 flex-1 border border-(--page-primary-color) border-x-0 first:border-l last:border-r",
"flex-1 border border-(--page-primary-color) border-x-0 first:border-l last:border-r",
decorationSizePx === undefined && "h-2.5",
isActive && "bg-(--page-primary-color)",
)}
/>
@@ -41,7 +64,13 @@ export function LevelDisplay({ icon, type, level, className, ...props }: Props)
return (
<i
key={itemKey}
className={cn("ph size-2.5 text-(--page-primary-color)", `ph-${icon}`, !isActive && "opacity-40")}
style={iconStyle}
className={cn(
"ph text-(--page-primary-color)",
resolvedIconSizePx === undefined && defaultDecorationClassName,
`ph-${icon}`,
!isActive && "opacity-40",
)}
/>
);
}
@@ -50,8 +79,10 @@ export function LevelDisplay({ icon, type, level, className, ...props }: Props)
<div
key={itemKey}
data-active={isActive}
style={decorationStyle}
className={cn(
"size-2.5 border border-(--page-primary-color)",
"border border-(--page-primary-color)",
decorationSizePx === undefined && defaultDecorationClassName,
isActive && "bg-(--page-primary-color)",
type === "circle" && "rounded-full",
type === "rectangle" && "w-7",
@@ -3,6 +3,8 @@ import { Trans } from "@lingui/react/macro";
import { useStore } from "@tanstack/react-form";
import { AnimatePresence, m } from "motion/react";
import { colorDesignSchema, levelDesignSchema } from "@reactive-resume/schema/resume/data";
import { resolveLevelDisplaySizes } from "@reactive-resume/schema/resume/level-display-sizes";
import { resolveStyleRuleFontSize } from "@reactive-resume/schema/resume/style-rules";
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { Separator } from "@reactive-resume/ui/components/separator";
@@ -276,6 +278,13 @@ function LevelSectionForm() {
const previewType = useStore(form.store, (s) => s.values.type);
const previewIcon = useStore(form.store, (s) => s.values.icon);
const iconFontSize = resolveStyleRuleFontSize(resume.data, { slot: "icon" });
const levelFontSize = resolveStyleRuleFontSize(resume.data, { slot: "level" });
const { decorationSize, levelIconExplicitSize } = resolveLevelDisplaySizes({
bodyFontSize: resume.data.metadata.typography.body.fontSize,
iconFontSize,
levelFontSize,
});
return (
<form
@@ -294,7 +303,14 @@ function LevelSectionForm() {
style={{ "--page-primary-color": colors.primary, backgroundColor: colors.background } as React.CSSProperties}
className="flex items-center justify-center rounded-md p-6"
>
<LevelDisplay level={3} type={previewType} icon={previewIcon} className="w-full max-w-[220px] justify-center" />
<LevelDisplay
level={3}
type={previewType}
icon={previewIcon}
decorationSizePx={decorationSize}
iconSizePx={levelIconExplicitSize}
className="w-full max-w-[220px] justify-center"
/>
</div>
<div className="flex items-center gap-3">