返回 presentation-ai
PaletteDropdown.tsx
根目录 / src / components / presentation / slides / slide-editor / PaletteDropdown.tsx
1 "use client";
2
3 import {
4 AlignCenter,
5 ArrowDown,
6 ArrowUp,
7 FoldVertical,
8 ImageIcon,
9 Maximize2,
10 Palette,
11 } from "lucide-react";
12
13 import { PRESENTATION_PORTAL_CONTENT_CLASS } from "@/components/presentation/overlay-layers";
14 import { Button } from "@/components/ui/button";
15 import ColorPicker from "@/components/ui/color-picker";
16 import {
17 DropdownMenu,
18 DropdownMenuContent,
19 DropdownMenuTrigger,
20 } from "@/components/ui/dropdown-menu";
21 import { themes } from "@/lib/presentation/themes";
22 import { capitalizeFirstLetter, cn } from "@/lib/utils";
23 import { useSlideEditorContext } from "./SlideEditorContext";
24 import { layoutMap } from "./types";
25
26 export function PaletteDropdown() {
27 const {
28 selectedLayout,
29 currentSlide,
30 currentTheme,
31 currentThemeData,
32 currentAlignment,
33 currentWidth,
34 hasRootImage,
35 updateSlide,
36 handleLayoutChange,
37 handleImageEdit,
38 setSelectedLayout,
39 } = useSlideEditorContext();
40
41 return (
42 <DropdownMenu>
43 <DropdownMenuTrigger asChild>
44 <Button
45 variant="ghost"
46 size="icon"
47 className="size-8! gap-1 rounded-full border border-white/20 bg-background/50 shadow backdrop-blur-md transition-all hover:bg-background/80"
48 >
49 <Palette className="size-4 text-muted-foreground" />
50 </Button>
51 </DropdownMenuTrigger>
52 <DropdownMenuContent
53 align="start"
54 className={cn(PRESENTATION_PORTAL_CONTENT_CLASS, "w-90 p-4")}
55 >
56 {/* Layout options */}
57 <div className="mb-4 flex gap-2 border-b border-border pb-4">
58 {[0, 1, 2, 3, 4].map((idx) => (
59 <button
60 type="button"
61 key={layoutMap[idx]}
62 title={capitalizeFirstLetter(layoutMap[idx]!)}
63 onClick={() => {
64 if (!hasRootImage) {
65 setSelectedLayout(idx);
66 updateSlide({
67 layoutType: layoutMap[idx],
68 rootImage: { query: "", url: "" },
69 });
70 } else {
71 handleLayoutChange(idx);
72 }
73 }}
74 className={cn(
75 "flex items-center justify-center rounded-lg px-3 py-2 transition-all",
76 selectedLayout === idx
77 ? "border-2 border-primary bg-primary/20"
78 : "border border-border bg-secondary hover:border-muted-foreground",
79 )}
80 >
81 <div
82 className={cn(
83 "grid h-5 w-8 rounded-sm",
84 selectedLayout === idx
85 ? "border-2 border-primary-foreground bg-primary-foreground"
86 : "border border-muted-foreground/50 bg-transparent",
87 )}
88 >
89 {idx === 1 && (
90 <div
91 className={cn(
92 "h-2 w-full rounded-none",
93 selectedLayout === idx
94 ? "bg-muted"
95 : "bg-muted-foreground/50",
96 )}
97 />
98 )}
99 {idx === 2 && (
100 <div className="flex size-full gap-0.5 p-0.5">
101 <div
102 className={cn(
103 "flex-1 rounded-sm",
104 selectedLayout === idx
105 ? "bg-primary"
106 : "bg-muted-foreground/50",
107 )}
108 />
109 <div
110 className={cn(
111 "flex-1 rounded-sm",
112 selectedLayout === idx
113 ? "bg-primary-foreground"
114 : "bg-transparent",
115 )}
116 />
117 </div>
118 )}
119 {idx === 3 && (
120 <div className="flex size-full gap-0.5 p-0.5">
121 <div
122 className={cn(
123 "flex-1 rounded-sm",
124 selectedLayout === idx
125 ? "bg-primary-foreground"
126 : "bg-transparent",
127 )}
128 />
129 <div
130 className={cn(
131 "flex-1 rounded-sm",
132 selectedLayout === idx
133 ? "bg-muted"
134 : "bg-muted-foreground/50",
135 )}
136 />
137 </div>
138 )}
139 {idx === 4 && (
140 <div
141 className={cn(
142 "size-full rounded-sm",
143 selectedLayout === idx
144 ? "bg-muted"
145 : "bg-muted-foreground/50",
146 )}
147 />
148 )}
149 </div>
150 </button>
151 ))}
152 </div>
153
154 {/* Card color */}
155 <div className="mb-4 border-b border-border pb-4">
156 <div className="flex items-center justify-between">
157 <div className="flex items-center gap-3">
158 <Palette className="size-4 text-muted-foreground" />
159 <span className="text-sm font-medium">Card color</span>
160 </div>
161 <ColorPicker
162 value={
163 currentSlide?.bgColor ??
164 themes[currentTheme]?.colors?.background ??
165 currentThemeData?.colors?.background
166 }
167 onChange={(color) => updateSlide({ bgColor: color })}
168 />
169 </div>
170 </div>
171
172 {/* Content alignment */}
173 <div className="mb-4 border-b border-border pb-4">
174 <div className="flex items-center justify-between">
175 <div className="flex items-center gap-3">
176 <AlignCenter className="size-4 text-muted-foreground" />
177 <span className="text-sm font-medium">Content alignment</span>
178 </div>
179 <div className="flex gap-1">
180 {[
181 { value: "start" as const, icon: ArrowUp },
182 { value: "center" as const, icon: FoldVertical },
183 { value: "end" as const, icon: ArrowDown },
184 ].map((item) => (
185 <button
186 aria-label="palette dropdown control"
187 type="button"
188 key={item.value}
189 onClick={() => updateSlide({ alignment: item.value })}
190 className={cn(
191 "rounded p-2 transition-all",
192 currentAlignment === item.value
193 ? "border border-primary bg-primary/20"
194 : "border border-border hover:border-muted-foreground",
195 )}
196 >
197 <item.icon className="size-4" />
198 </button>
199 ))}
200 </div>
201 </div>
202 </div>
203
204 {/* Card width */}
205 <div className="mb-4 border-b border-border pb-4">
206 <div className="flex items-center justify-between">
207 <div className="flex items-center gap-3">
208 <Maximize2 className="size-4 text-muted-foreground" />
209 <span className="text-sm font-medium">Card width</span>
210 </div>
211 <div className="flex gap-1">
212 {(["M", "L"] as const).map((size) => (
213 <button
214 type="button"
215 key={size}
216 onClick={() => updateSlide({ width: size })}
217 className={cn(
218 "rounded px-3 py-1 text-sm font-medium transition",
219 currentWidth === size
220 ? "border border-primary bg-primary/20 text-primary"
221 : "border border-border text-muted-foreground hover:border-muted-foreground",
222 )}
223 >
224 {size}
225 </button>
226 ))}
227 </div>
228 </div>
229 </div>
230
231 {/* Image */}
232 <div className="flex items-center justify-between">
233 <div className="flex items-center gap-3">
234 <ImageIcon className="size-4 text-muted-foreground" />
235 <span className="text-sm font-medium">Image</span>
236 </div>
237 <Button
238 variant="outline"
239 size="sm"
240 className="border-primary text-primary hover:bg-primary/10"
241 onClick={handleImageEdit}
242 >
243 {hasRootImage ? "Edit" : "+ Add"}
244 </Button>
245 </div>
246 </DropdownMenuContent>
247 </DropdownMenu>
248 );
249 }
250
250 lines Plain Text