feat: add Chinese font options (#2905)

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
Platinum1154
2026-04-26 06:32:42 +08:00
committed by GitHub
parent 77ad14b359
commit a4e7d6680d
11 changed files with 474 additions and 94 deletions
+5 -7
View File
@@ -2,7 +2,7 @@ import { Trans } from "@lingui/react/macro";
import { ArrowLeftIcon, WarningIcon } from "@phosphor-icons/react";
import { Link, type NotFoundRouteProps } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { buttonVariants } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { BrandIcon } from "../ui/brand-icon";
@@ -20,12 +20,10 @@ export function NotFoundScreen({ routeId }: NotFoundRouteProps) {
<AlertDescription>{routeId}</AlertDescription>
</Alert>
<Button>
<Link to="..">
<ArrowLeftIcon />
<Trans>Go Back</Trans>
</Link>
</Button>
<Link to=".." className={buttonVariants()}>
<ArrowLeftIcon />
<Trans>Go Back</Trans>
</Link>
</div>
);
}
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import type { resumeDataSchema } from "@/schema/resume/data";
import { pageDimensionsAsMillimeters } from "@/schema/page";
import { buildResumeFontFamily } from "@/utils/fonts";
type UseCssVariablesProps = Pick<z.infer<typeof resumeDataSchema>, "picture" | "metadata">;
@@ -36,12 +37,12 @@ export const useCSSVariables = ({ picture, metadata }: UseCssVariablesProps) =>
"--page-text-color": metadata.design.colors.text,
"--page-primary-color": metadata.design.colors.primary,
"--page-background-color": metadata.design.colors.background,
"--page-body-font-family": `'${metadata.typography.body.fontFamily}', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`,
"--page-body-font-family": buildResumeFontFamily(metadata.typography.body.fontFamily),
"--page-body-font-weight": fontWeightStyles.lowestBodyFontWeight,
"--page-body-font-weight-bold": fontWeightStyles.highestBodyFontWeight,
"--page-body-font-size": metadata.typography.body.fontSize,
"--page-body-line-height": metadata.typography.body.lineHeight,
"--page-heading-font-family": `'${metadata.typography.heading.fontFamily}', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`,
"--page-heading-font-family": buildResumeFontFamily(metadata.typography.heading.fontFamily),
"--page-heading-font-weight": fontWeightStyles.lowestHeadingFontWeight,
"--page-heading-font-weight-bold": fontWeightStyles.highestHeadingFontWeight,
"--page-heading-font-size": metadata.typography.heading.fontSize,
+30 -6
View File
@@ -5,7 +5,7 @@ import { useIsMounted } from "usehooks-ts";
import type { typographySchema } from "@/schema/resume/data";
import webfontlist from "@/components/typography/webfontlist.json";
import { getFallbackWebFontFamilies, getLoadableWebFontWeights, webFontMap } from "@/utils/fonts";
export function useWebfonts(typography: z.infer<typeof typographySchema>) {
const isMounted = useIsMounted();
@@ -17,7 +17,7 @@ export function useWebfonts(typography: z.infer<typeof typographySchema>) {
if (body) body.setAttribute("data-wf-loaded", "false");
async function loadFont(family: string, weights: string[]) {
const font = webfontlist.find((font) => font.family === family);
const font = webFontMap.get(family);
if (!font) return;
type FontUrl = { url: string; weight: string; style: "italic" | "normal" };
@@ -49,11 +49,35 @@ export function useWebfonts(typography: z.infer<typeof typographySchema>) {
const bodyTypography = typography.body;
const headingTypography = typography.heading;
const fontWeightsByFamily = new Map<string, Set<string>>();
void Promise.allSettled([
loadFont(bodyTypography.fontFamily, bodyTypography.fontWeights),
loadFont(headingTypography.fontFamily, headingTypography.fontWeights),
]).then(() => {
const addFontLoadPlan = (family: string, weights: string[]) => {
const loadableWeights = getLoadableWebFontWeights(family, weights);
if (loadableWeights.length === 0) return;
const existingWeights = fontWeightsByFamily.get(family) ?? new Set<string>();
for (const weight of loadableWeights) {
existingWeights.add(weight);
}
fontWeightsByFamily.set(family, existingWeights);
};
addFontLoadPlan(bodyTypography.fontFamily, bodyTypography.fontWeights);
addFontLoadPlan(headingTypography.fontFamily, headingTypography.fontWeights);
for (const fallbackFamily of getFallbackWebFontFamilies(bodyTypography.fontFamily)) {
addFontLoadPlan(fallbackFamily, bodyTypography.fontWeights);
}
for (const fallbackFamily of getFallbackWebFontFamilies(headingTypography.fontFamily)) {
addFontLoadPlan(fallbackFamily, headingTypography.fontWeights);
}
void Promise.all(
Array.from(fontWeightsByFamily.entries()).map(([family, weights]) => loadFont(family, Array.from(weights))),
).then(() => {
if (isMounted() && body) body.setAttribute("data-wf-loaded", "true");
});
+19 -36
View File
@@ -1,46 +1,22 @@
import { useMemo } from "react";
import {
fontList,
getFont,
getFontDisplayName,
getFontSearchKeywords,
localFontList,
webFontMap,
} from "@/utils/fonts";
import { cn } from "@/utils/style";
import type { LocalFont, WebFont } from "./types";
import { Combobox, type MultiComboboxProps, type SingleComboboxProps } from "../ui/combobox";
import { FontDisplay } from "./font-display";
import webFontListJSON from "./webfontlist.json";
type Weight = "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900";
const localFontList = [
{ type: "local", category: "sans-serif", family: "Arial", weights: ["400", "600", "700"] },
{ type: "local", category: "sans-serif", family: "Calibri", weights: ["400", "600", "700"] },
{ type: "local", category: "sans-serif", family: "Helvetica", weights: ["400", "600", "700"] },
{ type: "local", category: "sans-serif", family: "Tahoma", weights: ["400", "600", "700"] },
{ type: "local", category: "sans-serif", family: "Trebuchet MS", weights: ["400", "600", "700"] },
{ type: "local", category: "sans-serif", family: "Verdana", weights: ["400", "600", "700"] },
{ type: "local", category: "serif", family: "Bookman", weights: ["400", "600", "700"] },
{ type: "local", category: "serif", family: "Cambria", weights: ["400", "600", "700"] },
{ type: "local", category: "serif", family: "Garamond", weights: ["400", "600", "700"] },
{ type: "local", category: "serif", family: "Georgia", weights: ["400", "600", "700"] },
{ type: "local", category: "serif", family: "Palatino", weights: ["400", "600", "700"] },
{ type: "local", category: "serif", family: "Times New Roman", weights: ["400", "600", "700"] },
] as LocalFont[];
const webFontList = webFontListJSON as WebFont[];
function buildWebFontMap() {
const webFontMap = new Map<string, WebFont>();
for (const font of webFontList) {
webFontMap.set(font.family, font);
}
return webFontMap;
}
const webFontMap: Map<string, WebFont> = buildWebFontMap();
export function getNextWeights(fontFamily: string): Weight[] | null {
const fontData = webFontMap.get(fontFamily);
const fontData = getFont(fontFamily);
if (!fontData || !Array.isArray(fontData.weights) || fontData.weights.length === 0) return null;
const uniqueWeights = Array.from(new Set(fontData.weights)) as Weight[];
@@ -67,10 +43,17 @@ type FontFamilyComboboxProps = Omit<SingleComboboxProps, "options">;
export function FontFamilyCombobox({ className, ...props }: FontFamilyComboboxProps) {
const options = useMemo(() => {
return [...webFontList, ...localFontList].map((font: LocalFont | WebFont) => ({
return fontList.map((font) => ({
value: font.family,
keywords: [font.family],
label: <FontDisplay name={font.family} type={font.type} url={"preview" in font ? font.preview : undefined} />,
keywords: getFontSearchKeywords(font.family),
label: (
<FontDisplay
family={font.family}
label={getFontDisplayName(font.family)}
type={font.type}
url={"preview" in font ? font.preview : undefined}
/>
),
}));
}, []);
+8 -6
View File
@@ -4,15 +4,16 @@ import { useEffect, useRef, useState } from "react";
import { cn } from "@/utils/style";
interface FontDisplayProps {
name: string;
url?: string;
family: string;
label: string;
type: "local" | "web";
url?: string;
}
const loadedFonts = new Set<string>();
export function FontDisplay({ name, url, type }: FontDisplayProps) {
const previewName = type === "local" ? name : `${name} Preview`;
export function FontDisplay({ family, label, type, url }: FontDisplayProps) {
const previewName = type === "local" ? family : `${family} Preview`;
const containerRef = useRef<HTMLDivElement>(null);
const [isLoaded, setIsLoaded] = useState(() => type === "local" || loadedFonts.has(previewName));
@@ -31,13 +32,14 @@ export function FontDisplay({ name, url, type }: FontDisplayProps) {
}, [isInView, isLoaded, previewName, url]);
return (
<div ref={containerRef} className="inline">
<div ref={containerRef} className="inline-flex items-baseline gap-2">
<span
style={{ fontFamily: isLoaded ? `'${previewName}', sans-serif` : "sans-serif" }}
className={cn(isLoaded ? "opacity-100" : "opacity-50", "transition-opacity duration-200 ease-in")}
>
{name}
{label}
</span>
{label !== family && <span className="text-xs text-muted-foreground">{family}</span>}
</div>
);
}