返回 presentation-ai
FontCombobox.tsx
根目录 / src / components / ui / font-picker / components / FontCombobox.tsx
1 import { Button } from "@/components/ui/button";
2 import {
3 Command,
4 CommandEmpty,
5 CommandGroup,
6 CommandInput,
7 CommandItem,
8 CommandList,
9 } from "@/components/ui/command";
10 import {
11 Popover,
12 PopoverContent,
13 PopoverTrigger,
14 } from "@/components/ui/popover";
15 import { ScrollArea } from "@/components/ui/scroll-area";
16 import { Skeleton } from "@/components/ui/skeleton";
17 import { cn } from "@/lib/utils";
18 import { ChevronDown, Loader2 } from "lucide-react";
19 import dynamic from "next/dynamic";
20 import { useEffect } from "react";
21 import { useInView } from "react-intersection-observer";
22 import { type Font } from "../types";
23 import { usePaginatedFonts } from "../utils/usePaginatedFonts";
24 import { FontItem } from "./FontItem";
25
26 const FontPreviews = dynamic(() => import("./FontPreviews"), {
27 loading: () => <Skeleton className="h-8 w-full" />,
28 ssr: false,
29 });
30
31 export function FontCombobox({
32 fonts,
33 currentFont,
34 onFontSelect,
35 noMatches,
36 searchValue,
37 onSearchChange,
38 open,
39 onOpenChange,
40 className,
41 }: {
42 fonts: Font[];
43 currentFont: Font;
44 onFontSelect: (font: Font) => void;
45 noMatches: string;
46 searchValue: string;
47 onSearchChange: (value: string) => void;
48 open: boolean;
49 onOpenChange: (open: boolean) => void;
50 className?: string;
51 }) {
52 const { visibleFonts, hasMore, isLoadingMore, loadMore } = usePaginatedFonts(
53 fonts,
54 searchValue,
55 );
56
57 const { ref: loadMoreRef, inView } = useInView({
58 threshold: 0.1,
59 triggerOnce: false,
60 });
61
62 useEffect(() => {
63 if (inView && hasMore && !isLoadingMore) {
64 loadMore();
65 }
66 }, [inView, hasMore, isLoadingMore, loadMore]);
67
68 return (
69 <Popover open={open} onOpenChange={onOpenChange} modal={true}>
70 <PopoverTrigger asChild>
71 <Button
72 variant="outline"
73 role="combobox"
74 aria-expanded={open}
75 className={cn(
76 "h-8 w-full justify-between p-0 py-2 font-normal",
77 className,
78 )}
79 data-plate-focus="true"
80 >
81 <div className="flex min-w-0 flex-1 items-center">
82 <FontPreviews>
83 <FontItem
84 key={currentFont.sane}
85 font={currentFont}
86 fontIndex={fonts.findIndex(
87 (font) => font.sane === currentFont.sane,
88 )}
89 isCurrent={false}
90 />
91 </FontPreviews>
92 </div>
93 <ChevronDown className="mx-2 h-4 w-4 shrink-0 opacity-50" />
94 </Button>
95 </PopoverTrigger>
96 <PopoverContent
97 onMouseDown={(e) => e.stopPropagation()}
98 className="ignore-click-outside/toolbar max-h-none w-full overflow-visible p-0"
99 align="start"
100 >
101 <Command shouldFilter={false}>
102 <CommandInput
103 placeholder="Search fonts..."
104 value={searchValue}
105 onValueChange={onSearchChange}
106 />
107 <CommandList className="max-h-none overflow-visible">
108 <CommandEmpty>{noMatches}</CommandEmpty>
109 <CommandGroup>
110 <ScrollArea className="h-72">
111 <FontPreviews>
112 {visibleFonts.map((font, index) => (
113 <CommandItem
114 data-plate-focus="true"
115 key={font.sane}
116 value={font.name}
117 onSelect={() => {
118 onFontSelect(font);
119 onOpenChange(false);
120 }}
121 >
122 <FontItem
123 key={font.sane}
124 font={font}
125 fontIndex={index}
126 isCurrent={currentFont.name === font.name}
127 />
128 </CommandItem>
129 ))}
130 {hasMore && (
131 <div ref={loadMoreRef} className="p-2">
132 {isLoadingMore ? (
133 <div className="flex items-center justify-center py-2">
134 <Loader2 className="mr-2 h-3 w-3 animate-spin" />
135 <span className="text-xs text-muted-foreground">
136 Loading more fonts...
137 </span>
138 </div>
139 ) : (
140 <div className="py-2 text-center text-xs text-muted-foreground">
141 Scroll for more fonts...
142 </div>
143 )}
144 </div>
145 )}
146 </FontPreviews>
147 </ScrollArea>
148 </CommandGroup>
149 </CommandList>
150 </Command>
151 </PopoverContent>
152 </Popover>
153 );
154 }
155
155 lines Plain Text