mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 09:24:54 +10:00
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:
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveLevelDisplaySizes } from "./level-display-sizes";
|
||||
|
||||
describe("resolveLevelDisplaySizes", () => {
|
||||
it("uses typography defaults when no custom font sizes are set", () => {
|
||||
expect(resolveLevelDisplaySizes({ bodyFontSize: 12 })).toEqual({
|
||||
decorationSize: 10,
|
||||
levelIconExplicitSize: 14,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the global icon font size for decorations and defers icon sizing to icon rules", () => {
|
||||
expect(resolveLevelDisplaySizes({ bodyFontSize: 12, iconFontSize: 20 })).toEqual({
|
||||
decorationSize: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the level font size for all level display visuals", () => {
|
||||
expect(resolveLevelDisplaySizes({ bodyFontSize: 12, iconFontSize: 20, levelFontSize: 16 })).toEqual({
|
||||
decorationSize: 16,
|
||||
levelIconExplicitSize: 16,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
export type ResolveLevelDisplaySizesOptions = {
|
||||
bodyFontSize: number;
|
||||
iconFontSize?: number | undefined;
|
||||
levelFontSize?: number | undefined;
|
||||
};
|
||||
|
||||
export type LevelDisplaySizes = {
|
||||
decorationSize: number;
|
||||
levelIconExplicitSize?: number | undefined;
|
||||
};
|
||||
|
||||
export const resolveLevelDisplaySizes = (options: ResolveLevelDisplaySizesOptions): LevelDisplaySizes => {
|
||||
const defaultDecorationSize = options.bodyFontSize - 2;
|
||||
const legacyLevelIconSize = defaultDecorationSize + 4;
|
||||
|
||||
const decorationSize = options.levelFontSize ?? options.iconFontSize ?? defaultDecorationSize;
|
||||
|
||||
if (options.levelFontSize !== undefined) {
|
||||
return { decorationSize, levelIconExplicitSize: options.levelFontSize };
|
||||
}
|
||||
|
||||
if (options.iconFontSize !== undefined) {
|
||||
return { decorationSize };
|
||||
}
|
||||
|
||||
return { decorationSize, levelIconExplicitSize: legacyLevelIconSize };
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { defaultResumeData } from "./default";
|
||||
import { resolveStyleRuleFontSize } from "./style-rules";
|
||||
|
||||
describe("resolveStyleRuleFontSize", () => {
|
||||
it("returns global icon font sizes without section context", () => {
|
||||
const data = {
|
||||
...defaultResumeData,
|
||||
metadata: {
|
||||
...defaultResumeData.metadata,
|
||||
styleRules: [
|
||||
{
|
||||
id: "icon-global",
|
||||
label: "",
|
||||
enabled: true,
|
||||
target: { scope: "global" as const },
|
||||
slots: { icon: { fontSize: 22 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveStyleRuleFontSize(data, { slot: "icon" })).toBe(22);
|
||||
});
|
||||
|
||||
it("does not apply level font sizes to the icon slot", () => {
|
||||
const data = {
|
||||
...defaultResumeData,
|
||||
metadata: {
|
||||
...defaultResumeData.metadata,
|
||||
styleRules: [
|
||||
{
|
||||
id: "level-global",
|
||||
label: "",
|
||||
enabled: true,
|
||||
target: { scope: "global" as const },
|
||||
slots: { level: { fontSize: 18 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveStyleRuleFontSize(data, { slot: "icon" })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { CustomSectionType, ResumeData, SectionType, StyleIntent, StyleSlot } from "./data";
|
||||
|
||||
export type SectionStyleRuleContext = {
|
||||
sectionId: string;
|
||||
sectionType?: CustomSectionType | undefined;
|
||||
};
|
||||
|
||||
export type ResolveStyleRuleSlotOptions = {
|
||||
slot: StyleSlot;
|
||||
sectionId?: string | undefined;
|
||||
sectionType?: CustomSectionType | undefined;
|
||||
};
|
||||
|
||||
const builtInSectionTypes = new Set<SectionType>([
|
||||
"profiles",
|
||||
"experience",
|
||||
"education",
|
||||
"projects",
|
||||
"skills",
|
||||
"languages",
|
||||
"interests",
|
||||
"awards",
|
||||
"certifications",
|
||||
"publications",
|
||||
"volunteer",
|
||||
"references",
|
||||
]);
|
||||
|
||||
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
||||
|
||||
export const getSectionStyleRuleContext = (data: ResumeData, sectionId: string): SectionStyleRuleContext => {
|
||||
if (sectionId === "summary") return { sectionId, sectionType: "summary" };
|
||||
if (builtInSectionTypes.has(sectionId as SectionType)) {
|
||||
return { sectionId, sectionType: sectionId as SectionType };
|
||||
}
|
||||
|
||||
const customSection = data.customSections.find((section) => section.id === sectionId);
|
||||
|
||||
return { sectionId, sectionType: customSection?.type };
|
||||
};
|
||||
|
||||
export const resolveStyleIntentForSlot = (data: ResumeData, options: ResolveStyleRuleSlotOptions): StyleIntent => {
|
||||
const matchingRules = (data.metadata.styleRules ?? []).filter((rule) => {
|
||||
if (!rule.enabled) return false;
|
||||
if (!rule.slots[options.slot]) return false;
|
||||
|
||||
if (rule.target.scope === "global") return true;
|
||||
if (rule.target.scope === "sectionType") return rule.target.sectionType === options.sectionType;
|
||||
if (rule.target.scope === "sectionId") return rule.target.sectionId === options.sectionId;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const specificity = { global: 0, sectionType: 1, sectionId: 2 } satisfies Record<
|
||||
"global" | "sectionType" | "sectionId",
|
||||
number
|
||||
>;
|
||||
|
||||
const bySpecificity = [...matchingRules].sort((a, b) => {
|
||||
return specificity[a.target.scope] - specificity[b.target.scope];
|
||||
});
|
||||
|
||||
return Object.assign({}, ...bySpecificity.map((rule) => rule.slots[options.slot]));
|
||||
};
|
||||
|
||||
export const resolveStyleRuleFontSize = (
|
||||
data: ResumeData,
|
||||
options: ResolveStyleRuleSlotOptions,
|
||||
): number | undefined => {
|
||||
const fontSize = resolveStyleIntentForSlot(data, options).fontSize;
|
||||
if (fontSize === undefined) return undefined;
|
||||
|
||||
return clamp(fontSize, 6, 48);
|
||||
};
|
||||
Reference in New Issue
Block a user