mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-16 16:19:47 +10:00
feat: add Chinese font options (#2905)
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user