diff --git a/apps/web/src/components/typography/combobox.test.tsx b/apps/web/src/components/typography/combobox.test.tsx
new file mode 100644
index 000000000..76dbf1f66
--- /dev/null
+++ b/apps/web/src/components/typography/combobox.test.tsx
@@ -0,0 +1,31 @@
+// @vitest-environment happy-dom
+import { render } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { FontWeightCombobox } from "./combobox";
+
+const comboboxMock = vi.hoisted(() => ({
+ props: undefined as { onValueChange?: (value: string[] | null) => void } | undefined,
+}));
+
+vi.mock("@/components/ui/combobox", () => ({
+ Combobox: (props: { onValueChange?: (value: string[] | null) => void }) => {
+ comboboxMock.props = props;
+ return null;
+ },
+}));
+
+describe("FontWeightCombobox", () => {
+ beforeEach(() => {
+ comboboxMock.props = undefined;
+ });
+
+ it("emits selected font weights in ascending order", () => {
+ const onValueChange = vi.fn();
+
+ render();
+
+ comboboxMock.props?.onValueChange?.(["800", "600", "400"]);
+
+ expect(onValueChange).toHaveBeenCalledWith(["400", "600", "800"]);
+ });
+});
diff --git a/apps/web/src/components/typography/combobox.tsx b/apps/web/src/components/typography/combobox.tsx
index f1b72e426..5f351132d 100644
--- a/apps/web/src/components/typography/combobox.tsx
+++ b/apps/web/src/components/typography/combobox.tsx
@@ -1,6 +1,6 @@
import type { MultiComboboxProps, SingleComboboxProps } from "@/components/ui/combobox";
-import { useMemo } from "react";
-import { fontList, getFont, getFontDisplayName, getFontSearchKeywords } from "@reactive-resume/fonts";
+import { useCallback, useMemo } from "react";
+import { fontList, getFont, getFontDisplayName, getFontSearchKeywords, sortFontWeights } from "@reactive-resume/fonts";
import { cn } from "@reactive-resume/utils/style";
import { Combobox } from "@/components/ui/combobox";
import { FontDisplay } from "./font-display";
@@ -54,14 +54,20 @@ export function FontFamilyCombobox({ className, ...props }: FontFamilyComboboxPr
type FontWeightComboboxProps = Omit & { fontFamily: string };
-export function FontWeightCombobox({ fontFamily, ...props }: FontWeightComboboxProps) {
+export function FontWeightCombobox({
+ fontFamily,
+ onValueChange,
+ value,
+ defaultValue,
+ ...props
+}: FontWeightComboboxProps) {
const options = useMemo(() => {
const fontData = getFont(fontFamily);
let weights: string[] = [];
if (fontData && Array.isArray(fontData.weights) && fontData.weights.length > 0) {
- weights = fontData.weights as string[];
+ weights = sortFontWeights(fontData.weights);
} else {
// Fallback to all possible weights
weights = ["100", "200", "300", "400", "500", "600", "700", "800", "900"];
@@ -74,5 +80,27 @@ export function FontWeightCombobox({ fontFamily, ...props }: FontWeightComboboxP
}));
}, [fontFamily]);
- return ;
+ const sortedValue = useMemo(() => (value ? sortFontWeights(value) : value), [value]);
+ const sortedDefaultValue = useMemo(
+ () => (defaultValue ? sortFontWeights(defaultValue) : defaultValue),
+ [defaultValue],
+ );
+
+ const handleValueChange = useCallback(
+ (nextValue: string[] | null) => {
+ onValueChange?.(nextValue ? sortFontWeights(nextValue) : nextValue);
+ },
+ [onValueChange],
+ );
+
+ return (
+
+ );
}
diff --git a/packages/fonts/src/index.ts b/packages/fonts/src/index.ts
index fd958bf80..8fc877102 100644
--- a/packages/fonts/src/index.ts
+++ b/packages/fonts/src/index.ts
@@ -171,6 +171,10 @@ export function getWebFontSource(family: string, weight: FontWeight = "400", ita
return webFont.files[key] ?? (italic ? webFont.files[weight] : undefined) ?? webFont.preview;
}
+export function sortFontWeights(fontWeights: T[]): T[] {
+ return [...fontWeights].sort((a, b) => Number(a) - Number(b));
+}
+
export function getFallbackWebFontFamilies(family: string) {
if (isStandardPdfFontFamily(family)) return [];
diff --git a/packages/pdf/src/document.tsx b/packages/pdf/src/document.tsx
index ef10c108e..99697e72d 100644
--- a/packages/pdf/src/document.tsx
+++ b/packages/pdf/src/document.tsx
@@ -3,6 +3,7 @@ import type { Template } from "@reactive-resume/schema/templates";
import type { ComponentType } from "react";
import type { SectionTitleResolver } from "./section-title";
import { Document } from "@react-pdf/renderer";
+import { useMemo } from "react";
import { RenderProvider } from "./context";
import { registerFonts } from "./hooks/use-register-fonts";
import { getTemplatePage } from "./templates";
@@ -22,14 +23,12 @@ export type ResumeDocumentProps = {
export const ResumeDocument = ({ data, template, resolveSectionTitle }: ResumeDocumentProps) => {
const TemplatePageComponent = getTemplatePage(template);
- const typography = registerFonts(data.metadata.typography);
+ const typography = registerFonts(data.metadata.typography) as Typography;
+
// `registerFonts` widens `fontFamily` to `string | string[]` for CJK
// fallback (#2986); the cast carries that wider runtime value through
// `ResumeData` without changing the public schema.
- const resumeData =
- typography === data.metadata.typography
- ? data
- : { ...data, metadata: { ...data.metadata, typography: typography as unknown as Typography } };
+ const resumeData = useMemo(() => ({ ...data, metadata: { ...data.metadata, typography } }), [data, typography]);
return (
diff --git a/packages/pdf/src/hooks/use-register-fonts.test.ts b/packages/pdf/src/hooks/use-register-fonts.test.ts
index 4f1ce1959..b6df0d740 100644
--- a/packages/pdf/src/hooks/use-register-fonts.test.ts
+++ b/packages/pdf/src/hooks/use-register-fonts.test.ts
@@ -85,4 +85,18 @@ describe("registerFonts", () => {
}),
);
});
+
+ it("returns typography with font weights sorted ascending", async () => {
+ vi.spyOn(Font, "register").mockImplementation(() => {});
+ const { registerFonts } = await import("./use-register-fonts");
+
+ const pdfTypography = registerFonts({
+ ...typography,
+ body: { ...typography.body, fontFamily: "Source Sans 3", fontWeights: ["800", "600", "400"] },
+ heading: { ...typography.heading, fontFamily: "Source Sans 3", fontWeights: ["900", "500"] },
+ });
+
+ expect(pdfTypography.body.fontWeights).toEqual(["400", "600", "800"]);
+ expect(pdfTypography.heading.fontWeights).toEqual(["500", "900"]);
+ });
});
diff --git a/packages/pdf/src/hooks/use-register-fonts.ts b/packages/pdf/src/hooks/use-register-fonts.ts
index 565c3831f..68f294253 100644
--- a/packages/pdf/src/hooks/use-register-fonts.ts
+++ b/packages/pdf/src/hooks/use-register-fonts.ts
@@ -6,6 +6,7 @@ import {
getPdfCjkFallbackFontFamily,
getWebFontSource,
isStandardPdfFontFamily,
+ sortFontWeights,
} from "@reactive-resume/fonts";
type FontWeightRange = {
@@ -53,15 +54,13 @@ const resolvePdfFontFamily = (family: string) => {
const resolvePdfTypography = (typography: Typography): Typography => {
const bodyFontFamily = resolvePdfFontFamily(typography.body.fontFamily);
const headingFontFamily = resolvePdfFontFamily(typography.heading.fontFamily);
-
- if (bodyFontFamily === typography.body.fontFamily && headingFontFamily === typography.heading.fontFamily) {
- return typography;
- }
+ const bodyFontWeights = sortFontWeights(typography.body.fontWeights);
+ const headingFontWeights = sortFontWeights(typography.heading.fontWeights);
return {
...typography,
- body: { ...typography.body, fontFamily: bodyFontFamily },
- heading: { ...typography.heading, fontFamily: headingFontFamily },
+ body: { ...typography.body, fontFamily: bodyFontFamily, fontWeights: bodyFontWeights },
+ heading: { ...typography.heading, fontFamily: headingFontFamily, fontWeights: headingFontWeights },
};
};
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 5ebef951a..f26ccf56c 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -39,6 +39,7 @@
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
+ "postcss": "^8.5.14",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0b77e264c..84e0af9f8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+overrides:
+ postcss@<8.5.10: ^8.5.14
+
importers:
.:
@@ -813,6 +816,9 @@ importers:
'@typescript/native-preview':
specifier: 7.0.0-dev.20260510.1
version: 7.0.0-dev.20260510.1
+ postcss:
+ specifier: ^8.5.14
+ version: 8.5.14
tailwindcss:
specifier: ^4.3.0
version: 4.3.0
@@ -7558,10 +7564,6 @@ packages:
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
- postcss@8.4.31:
- resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
- engines: {node: ^10 || ^12 || >=14}
-
postcss@8.5.14:
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
engines: {node: ^10 || ^12 || >=14}
@@ -15419,7 +15421,7 @@ snapshots:
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.29
caniuse-lite: 1.0.30001792
- postcss: 8.4.31
+ postcss: 8.5.14
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
styled-jsx: 5.1.6(@babel/core@7.29.0)(babel-plugin-macros@3.1.0)(react@19.2.6)
@@ -15836,12 +15838,6 @@ snapshots:
postcss-value-parser@4.2.0: {}
- postcss@8.4.31:
- dependencies:
- nanoid: 3.3.12
- picocolors: 1.1.1
- source-map-js: 1.2.1
-
postcss@8.5.14:
dependencies:
nanoid: 3.3.12
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 0e07d0c77..af666788c 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -8,5 +8,6 @@ allowBuilds:
msw: true
sharp: true
minimumReleaseAgeExclude:
- - postcss@8.5.10
- - ip-address@10.1.1
\ No newline at end of file
+ - postcss@8.5.14
+overrides:
+ postcss@<8.5.10: ^8.5.14