返回 presentation-ai
FontPicker.tsx
根目录 / src / components / ui / font-picker / components / FontPicker.tsx
1 /** biome-ignore-all lint/suspicious/noExplicitAny: This use requires any */
2 import { useCallback, useEffect, useMemo, useState } from "react";
3
4 // The font info is now imported statically at the top of the file.
5 import fontInfos from "../font-preview/fontInfo.json";
6
7 import { cn } from "@/lib/utils";
8 import {
9 type Font,
10 type FontPickerProps,
11 defaultFont,
12 toString,
13 } from "../types";
14 import { checkLoaded } from "../utils/fontChecker";
15 import { sanify } from "../utils/sanify";
16 import { getFourVariants, loadFontFromObject } from "../utils/utils";
17 import { FontCombobox } from "./FontCombobox";
18 import { FontList } from "./FontList";
19
20 export default function FontPicker({
21 defaultValue = "Open Sans",
22 noMatches = "No matches",
23 autoLoad = true,
24 loaderOnly = false,
25 loadAllVariants = false,
26 loadFonts = "",
27 googleFonts = "all",
28 fontCategories = "all",
29 localFonts = [],
30 mode = "combo",
31 fontVariants,
32 value,
33 fontsLoaded,
34 fontsLoadedTimeout,
35 className,
36 selectClassName,
37 ...rest
38 }: FontPickerProps) {
39 const [open, setOpen] = useState(false);
40 const [searchValue, setSearchValue] = useState("");
41
42 // All state and effects for loading have been removed (fontInfos, isLoadingFonts, error).
43
44 const allGoogleFonts: Font[] = useMemo(() => {
45 // The data from fontInfos is available immediately.
46 return (fontInfos as any[]).map((info: Omit<Font, "cased">) => ({
47 ...info,
48 cased: info.name.toLowerCase(),
49 }));
50 }, []); // The dependency on fontInfos is removed as it's now a constant import.
51
52 const fonts = useMemo(() => {
53 let activeFonts: Font[];
54
55 if (googleFonts === "all") {
56 activeFonts = [...allGoogleFonts];
57 } else if (typeof googleFonts === "string") {
58 const fontNames = googleFonts.trim().toLowerCase().split(",");
59 activeFonts = allGoogleFonts.filter((font) =>
60 fontNames.includes(font.cased),
61 );
62 } else if (typeof googleFonts === "function") {
63 activeFonts = allGoogleFonts.filter(googleFonts);
64 } else {
65 const fontNames =
66 googleFonts?.map((v) =>
67 typeof v === "string" ? v.toLowerCase() : v.cased,
68 ) ?? [];
69 activeFonts = allGoogleFonts.filter((font) =>
70 fontNames.includes(font.cased),
71 );
72 }
73
74 const processedLocalFonts = localFonts.map((font) => ({
75 ...font,
76 cased: font.name.toLowerCase(),
77 sane: sanify(font.name),
78 variants: font.variants.map((v) => toString(v)),
79 isLocal: true,
80 }));
81
82 activeFonts = [...activeFonts, ...processedLocalFonts];
83
84 if (fontCategories === "all") {
85 return activeFonts;
86 }
87
88 const categories = Array.isArray(fontCategories)
89 ? fontCategories.map((c) => c.toLowerCase())
90 : fontCategories.trim().toLowerCase().split(",");
91
92 return activeFonts.filter((font) => categories.includes(font.category));
93 }, [googleFonts, allGoogleFonts, localFonts, fontCategories]);
94
95 const getFontByName = useCallback(
96 (name: string) => fonts.find((font) => font.name.trim() === name.trim()),
97 [fonts],
98 );
99
100 const saneDefaultValue = useMemo(() => {
101 if (!fonts || fonts.length === 0) {
102 return defaultValue;
103 }
104 const search = defaultValue.toLowerCase().trim();
105 return fonts.some((font) => font.cased === search)
106 ? defaultValue
107 : fonts[0]?.name;
108 }, [fonts, defaultValue]);
109
110 const [currentFont, setCurrentFont] = useState<Font>(
111 () => getFontByName(saneDefaultValue!) || defaultFont,
112 );
113
114 useEffect(() => {
115 setCurrentFont(getFontByName(saneDefaultValue!) || defaultFont);
116 }, [saneDefaultValue, getFontByName]);
117
118 const handleFontSelect = useCallback(
119 (font: Font) => {
120 if (autoLoad) {
121 loadFontFromObject(font, loadAllVariants, getFourVariants);
122 }
123 fontVariants?.({ fontName: font.name, variants: font.variants });
124 value?.(font.name);
125 },
126 [autoLoad, loadAllVariants, fontVariants, value],
127 );
128
129 useEffect(() => {
130 if (!fontsLoaded) return;
131
132 const fontsToLoad: string[] = [];
133 if (typeof loadFonts === "string" && loadFonts) {
134 fontsToLoad.push(loadFonts);
135 } else if (Array.isArray(loadFonts)) {
136 loadFonts.forEach((font) => {
137 const fontName = typeof font === "string" ? font : font?.fontName;
138 if (fontName) fontsToLoad.push(fontName);
139 });
140 }
141
142 console.log("fontsToLoad", fontsToLoad);
143 const fontsToCheck = [...new Set([currentFont.name, ...fontsToLoad])];
144
145 const checkFonts = async () => {
146 try {
147 const results = await Promise.all(
148 fontsToCheck.map((font) =>
149 checkLoaded({ fontFamily: font, timeout: fontsLoadedTimeout }),
150 ),
151 );
152 fontsLoaded?.(!results.some((res) => !res));
153 } catch (e) {
154 console.error("Error checking if font families loaded", e);
155 fontsLoaded?.(false);
156 }
157 };
158
159 checkFonts();
160 }, [loadFonts, currentFont, fontsLoaded, fontsLoadedTimeout]);
161
162 // When acting as a loader-only component, proactively load all fonts passed in props.
163 useEffect(() => {
164 if (!loaderOnly) return;
165
166 const fontsToLoad: string[] = [];
167 if (typeof loadFonts === "string" && loadFonts) {
168 fontsToLoad.push(loadFonts);
169 } else if (Array.isArray(loadFonts)) {
170 loadFonts.forEach((font) => {
171 const fontName = typeof font === "string" ? font : font?.fontName;
172 if (fontName) fontsToLoad.push(fontName);
173 });
174 }
175
176 // De-duplicate and load each font family via Google Fonts
177 [...new Set(fontsToLoad)].forEach((fontName) => {
178 const font = getFontByName(fontName);
179 if (font) {
180 loadFontFromObject(font, loadAllVariants, getFourVariants);
181 }
182 });
183 }, [loaderOnly, loadFonts, getFontByName, loadAllVariants]);
184
185 if (loaderOnly) {
186 return null;
187 }
188
189 // Error display is removed as a static import will fail at build time, not run time.
190
191 return (
192 <div className={cn("w-full", className)} {...rest}>
193 {mode === "combo" ? (
194 <FontCombobox
195 fonts={fonts}
196 currentFont={currentFont}
197 onFontSelect={handleFontSelect}
198 noMatches={noMatches}
199 searchValue={searchValue}
200 onSearchChange={setSearchValue}
201 open={open}
202 onOpenChange={setOpen}
203 className={selectClassName}
204 />
205 ) : (
206 <FontList
207 fonts={fonts}
208 currentFont={currentFont}
209 onFontSelect={handleFontSelect}
210 searchValue={searchValue}
211 onSearchChange={setSearchValue}
212 isLoadingFonts={false}
213 />
214 )}
215 </div>
216 );
217 }
218
218 lines Plain Text