返回 presentation-ai
ThemeModalContent.tsx
根目录 / src / components / notebook / presentation / components / theme / ThemeModalContent.tsx
1 "use client";
2
3 import { X } from "lucide-react";
4
5 import { ThemeCardSkeleton } from "@/components/notebook/presentation/components/theme/ThemeCardSkeleton";
6 import { ThemeCard } from "@/components/presentation/edit-panel/sections/theme/ThemeCard";
7 import { Button } from "@/components/ui/button";
8 import { ScrollArea } from "@/components/ui/scroll-area";
9 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
10 import {
11 themes as builtInThemes,
12 type ThemeProperties,
13 } from "@/lib/presentation/themes";
14
15 const THEME_SKELETON_KEYS = [
16 "theme-skeleton-1",
17 "theme-skeleton-2",
18 "theme-skeleton-3",
19 ] as const;
20
21 interface CustomTheme {
22 id: string;
23 name: string;
24 description?: string;
25 themeData: ThemeProperties;
26 isPublic: boolean;
27 logoUrl?: string;
28 userId: string;
29 user?: {
30 name: string;
31 };
32 isFavorite?: boolean;
33 likeCount?: number;
34 isLiked?: boolean;
35 }
36
37 interface ThemeModalContentProps {
38 activeTab: string;
39 onTabChange: (tab: string) => void;
40 selectedThemeId: string | null;
41 onPreviewTheme: (id: string, theme: ThemeProperties) => void;
42 userThemes: CustomTheme[];
43 isLoadingUserThemes: boolean;
44 onApplyTheme: () => void;
45 onClose: () => void;
46 }
47
48 /**
49 * Left panel theme selection content with tabs and theme grids
50 */
51 export function ThemeModalContent({
52 activeTab,
53 onTabChange,
54 selectedThemeId,
55
56 onPreviewTheme,
57 userThemes,
58 isLoadingUserThemes,
59 onApplyTheme,
60 onClose,
61 }: ThemeModalContentProps) {
62 const hasUserThemes = userThemes.length > 0;
63
64 return (
65 <div className="flex min-h-0 w-full flex-1 flex-col overflow-hidden border-border bg-background lg:basis-[40%] lg:border-r">
66 <div className="flex shrink-0 items-center justify-between border-b border-border px-2">
67 <h2 className="p-4 text-lg font-semibold">Themes</h2>
68 <div className="flex items-center gap-2">
69 <Button
70 size="icon"
71 variant="ghost"
72 onClick={onClose}
73 aria-label="Close theme modal"
74 >
75 <X className="size-4" />
76 </Button>
77 </div>
78 </div>
79
80 <Tabs
81 defaultValue="allweone-themes"
82 value={activeTab}
83 onValueChange={onTabChange}
84 className="flex min-h-0 flex-1 flex-col overflow-hidden"
85 >
86 <div className="shrink-0 px-4 pt-2">
87 <TabsList className="h-auto w-full justify-start gap-6 rounded-none border-b border-border bg-transparent p-0">
88 <TabsTrigger
89 value="allweone-themes"
90 className="rounded-none border-b-2 border-transparent px-0 py-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
91 >
92 ALLWEONE
93 </TabsTrigger>
94 <TabsTrigger
95 value="explore"
96 className="rounded-none border-b-2 border-transparent px-0 py-2 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
97 >
98 My themes
99 </TabsTrigger>
100 </TabsList>
101 </div>
102
103 <ScrollArea className="min-h-0 flex-1 overflow-hidden">
104 <TabsContent value="allweone-themes" className="m-0 p-4 pb-6">
105 <div className="grid grid-cols-1 gap-3 min-[420px]:grid-cols-2 xl:grid-cols-2">
106 {Object.entries(builtInThemes).map(([key, theme]) => (
107 <div key={key} className="h-44">
108 <ThemeCard
109 themeId={key}
110 theme={theme}
111 isSelected={selectedThemeId === key}
112 onSelect={() => onPreviewTheme(key, theme)}
113 showEllipsis={false}
114 showFavoriteButton={false}
115 showInfo={true}
116 />
117 </div>
118 ))}
119 </div>
120 </TabsContent>
121
122 <TabsContent value="explore" className="m-0 p-4 pb-6">
123 <div className="space-y-6">
124 <div className="space-y-3">
125 <h3 className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
126 My Themes
127 </h3>
128 {isLoadingUserThemes ? (
129 <div className="grid grid-cols-1 gap-3 min-[420px]:grid-cols-2 xl:grid-cols-3">
130 {THEME_SKELETON_KEYS.map((key) => (
131 <ThemeCardSkeleton key={`user-${key}`} />
132 ))}
133 </div>
134 ) : hasUserThemes ? (
135 <div className="grid grid-cols-1 gap-3 min-[420px]:grid-cols-2 xl:grid-cols-3">
136 {userThemes.map((item) => (
137 <div key={item.id} className="h-44">
138 <ThemeCard
139 themeId={item.id}
140 theme={item.themeData}
141 isSelected={selectedThemeId === item.id}
142 isFavorite={item.isFavorite}
143 likeCount={item.likeCount}
144 isLiked={item.isLiked}
145 isOwner={true}
146 isPublic={item.isPublic}
147 onSelect={() =>
148 onPreviewTheme(item.id, item.themeData)
149 }
150 showInfo={true}
151 />
152 </div>
153 ))}
154 </div>
155 ) : (
156 <div className="py-4 text-sm text-muted-foreground italic">
157 You haven&apos;t created any themes yet.
158 </div>
159 )}
160 </div>
161 </div>
162 </TabsContent>
163 </ScrollArea>
164 </Tabs>
165
166 {/* Apply Button */}
167 <div className="sticky bottom-0 z-10 shrink-0 border-t border-border bg-background/95 p-4 pb-[calc(1rem+env(safe-area-inset-bottom))] backdrop-blur supports-backdrop-filter:bg-background/80">
168 <Button
169 onClick={onApplyTheme}
170 className="w-full"
171 disabled={!selectedThemeId}
172 >
173 Apply Theme
174 </Button>
175 </div>
176 </div>
177 );
178 }
179
179 lines Plain Text