"use client"; import { useState, useMemo, useRef, useEffect, useCallback, type CSSProperties } from "react"; import { List, type RowComponentProps } from "react-window"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { loadFullFont } from "@/fonts/google-fonts"; import { SYSTEM_FONTS } from "@/fonts/system-fonts"; import type { FontAtlas, FontAtlasEntry } from "@/fonts/types"; import { useFontAtlas } from "@/fonts/use-font-atlas"; import { cn } from "@/utils/ui"; import { ChevronDown, Search } from "lucide-react"; import { HugeiconsIcon } from "@hugeicons/react"; import { TextIcon } from "@hugeicons/core-free-icons"; const FONT_TABS = [ { key: "all", label: "All fonts" }, { key: "my-fonts", label: "My fonts" }, { key: "favorites", label: "Favorites" }, ] as const; type FontTab = (typeof FONT_TABS)[number]["key"]; const ROW_HEIGHT = 40; const PREVIEW_SCALE = 0.8; const LIST_WIDTH = 288; const MAX_LIST_HEIGHT = 288; const OVERSCAN = 15; interface FontPickerProps { defaultValue?: string; onValueChange?: (value: string) => void; className?: string; } export function FontPicker({ defaultValue, onValueChange, className, }: FontPickerProps) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const [activeTab, setActiveTab] = useState("all"); const searchInputRef = useRef(null); const { atlas, status, fontNames, retry: handleRetry } = useFontAtlas({ open }); const filteredFonts = useMemo(() => { if (!search) return fontNames; const query = search.toLowerCase(); return fontNames.filter((name) => name.toLowerCase().includes(query)); }, [fontNames, search]); const listHeight = Math.min( MAX_LIST_HEIGHT, filteredFonts.length * ROW_HEIGHT, ); const handleSelect = useCallback( async ({ family }: { family: string }) => { if (!SYSTEM_FONTS.has(family)) { try { await loadFullFont({ family }); } catch { // ignore load failure, font will fall back to system default } } onValueChange?.(family); setOpen(false); }, [onValueChange], ); useEffect(() => { if (!open) { setSearch(""); setActiveTab("all"); } }, [open]); const activeTabLabel = FONT_TABS.find((t) => t.key === activeTab)?.label.toLowerCase() ?? ""; return (
{defaultValue ?? "Select a font"}
{ event.preventDefault(); searchInputRef.current?.focus(); }} onCloseAutoFocus={(event) => { event.preventDefault(); event.stopPropagation(); }} >
setSearch(event.target.value)} size="xs" className="w-full pl-5 bg-transparent border-none! shadow-none!" />
{FONT_TABS.map((tab) => ( ))}
{status === "loading" && (
Loading fonts...
)} {status === "error" && (

Failed to load font previews.

)} {status === "idle" && fontNames.length > 0 && filteredFonts.length === 0 && (
No fonts found.
)} {status === "idle" && atlas && filteredFonts.length > 0 && ( )}
); } function FontSpritePreview({ entry }: { entry: FontAtlasEntry }) { return (
); } type FontRowProps = { atlas: FontAtlas; filteredFonts: string[]; selectedFont: string | undefined; onFontSelect: (params: { family: string }) => void; }; function FontRow({ index, style, atlas, filteredFonts, selectedFont, onFontSelect, }: RowComponentProps) { const fontName = filteredFonts[index]; const entry = atlas.fonts[fontName]; const isSelected = fontName === selectedFont; const isSystemFont = SYSTEM_FONTS.has(fontName); return ( ); }