返回 presentation-ai
toolbar.tsx
根目录 / src / components / plate / ui / toolbar.tsx
1 "use client";
2
3 import * as ToolbarPrimitive from "@radix-ui/react-toolbar";
4 import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 import { cva, type VariantProps } from "class-variance-authority";
6 import { ChevronDown } from "lucide-react";
7 import * as React from "react";
8
9 import {
10 DropdownMenuLabel,
11 DropdownMenuRadioGroup,
12 DropdownMenuSeparator,
13 } from "@/components/ui/dropdown-menu";
14 import { Separator } from "@/components/ui/separator";
15 import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
16 import { cn } from "@/lib/utils";
17
18 export function Toolbar({
19 className,
20 ...props
21 }: React.ComponentProps<typeof ToolbarPrimitive.Root>) {
22 return (
23 <ToolbarPrimitive.Root
24 className={cn("relative flex items-center select-none", className)}
25 {...props}
26 />
27 );
28 }
29
30 export function ToolbarToggleGroup({
31 className,
32 ...props
33 }: React.ComponentProps<typeof ToolbarPrimitive.ToolbarToggleGroup>) {
34 return (
35 <ToolbarPrimitive.ToolbarToggleGroup
36 className={cn("flex items-center", className)}
37 {...props}
38 />
39 );
40 }
41
42 export function ToolbarLink({
43 className,
44 ...props
45 }: React.ComponentProps<typeof ToolbarPrimitive.Link>) {
46 return (
47 <ToolbarPrimitive.Link
48 className={cn("font-medium underline underline-offset-4", className)}
49 {...props}
50 />
51 );
52 }
53
54 export function ToolbarSeparator({
55 className,
56 ...props
57 }: React.ComponentProps<typeof ToolbarPrimitive.Separator>) {
58 return (
59 <ToolbarPrimitive.Separator
60 className={cn("mx-2 my-1 w-px shrink-0 bg-border", className)}
61 {...props}
62 />
63 );
64 }
65
66 // From toggleVariants
67 const toolbarButtonVariants = cva(
68 "inline-flex cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[background-color,color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-checked:bg-accent aria-checked:text-accent-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
69 {
70 defaultVariants: {
71 size: "default",
72 variant: "default",
73 },
74 variants: {
75 size: {
76 default: "h-9 min-w-9 px-2",
77 lg: "h-10 min-w-10 px-2.5",
78 sm: "h-8 min-w-8 px-1.5",
79 },
80 variant: {
81 default: "bg-transparent",
82 outline:
83 "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
84 },
85 },
86 },
87 );
88
89 const dropdownArrowVariants = cva(
90 cn(
91 "inline-flex items-center justify-center rounded-r-md text-sm font-medium text-foreground transition-colors disabled:pointer-events-none disabled:opacity-50",
92 ),
93 {
94 defaultVariants: {
95 size: "sm",
96 variant: "default",
97 },
98 variants: {
99 size: {
100 default: "h-9 w-6",
101 lg: "h-10 w-8",
102 sm: "h-8 w-4",
103 },
104 variant: {
105 default:
106 "bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
107 outline:
108 "border border-l-0 border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
109 },
110 },
111 },
112 );
113
114 type ToolbarButtonProps = {
115 isDropdown?: boolean;
116 pressed?: boolean;
117 } & Omit<
118 React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
119 "asChild" | "value"
120 > &
121 VariantProps<typeof toolbarButtonVariants>;
122
123 export const ToolbarButton = withTooltip(function ToolbarButton({
124 children,
125 className,
126 isDropdown,
127 pressed,
128 size = "sm",
129 variant,
130 ...props
131 }: ToolbarButtonProps) {
132 return typeof pressed === "boolean" ? (
133 <ToolbarToggleGroup disabled={props.disabled} value="single" type="single">
134 <ToolbarToggleItem
135 className={cn(
136 toolbarButtonVariants({
137 size,
138 variant,
139 }),
140 isDropdown && "justify-between gap-1 pr-1",
141 className,
142 )}
143 value={pressed ? "single" : ""}
144 {...props}
145 >
146 {isDropdown ? (
147 <>
148 <div className="flex flex-1 items-center gap-2 whitespace-nowrap">
149 {children}
150 </div>
151 <div>
152 <ChevronDown
153 className="size-3.5 text-muted-foreground"
154 data-icon
155 />
156 </div>
157 </>
158 ) : (
159 children
160 )}
161 </ToolbarToggleItem>
162 </ToolbarToggleGroup>
163 ) : (
164 <ToolbarPrimitive.Button
165 className={cn(
166 toolbarButtonVariants({
167 size,
168 variant,
169 }),
170 isDropdown && "pr-1",
171 className,
172 )}
173 {...props}
174 >
175 {children}
176 </ToolbarPrimitive.Button>
177 );
178 });
179
180 export function ToolbarSplitButton({
181 className,
182 ...props
183 }: React.ComponentPropsWithoutRef<typeof ToolbarButton>) {
184 return (
185 <ToolbarButton
186 className={cn("group flex gap-0 px-0 hover:bg-transparent", className)}
187 {...props}
188 />
189 );
190 }
191
192 type ToolbarSplitButtonPrimaryProps = Omit<
193 React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
194 "value"
195 > &
196 VariantProps<typeof toolbarButtonVariants>;
197
198 export function ToolbarSplitButtonPrimary({
199 children,
200 className,
201 size = "sm",
202 variant,
203 ...props
204 }: ToolbarSplitButtonPrimaryProps) {
205 return (
206 <span
207 className={cn(
208 toolbarButtonVariants({
209 size,
210 variant,
211 }),
212 "rounded-r-none",
213 "group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground",
214 className,
215 )}
216 {...props}
217 >
218 {children}
219 </span>
220 );
221 }
222
223 export function ToolbarSplitButtonSecondary({
224 className,
225 size,
226 variant,
227 ...props
228 }: React.ComponentPropsWithoutRef<"span"> &
229 VariantProps<typeof dropdownArrowVariants>) {
230 return (
231 <span
232 className={cn(
233 dropdownArrowVariants({
234 size,
235 variant,
236 }),
237 "group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground",
238 className,
239 )}
240 onClick={(e) => e.stopPropagation()}
241 role="button"
242 {...props}
243 >
244 <ChevronDown className="size-3.5 text-muted-foreground" data-icon />
245 </span>
246 );
247 }
248
249 export function ToolbarToggleItem({
250 className,
251 size = "sm",
252 variant,
253 ...props
254 }: React.ComponentProps<typeof ToolbarPrimitive.ToggleItem> &
255 VariantProps<typeof toolbarButtonVariants>) {
256 return (
257 <ToolbarPrimitive.ToggleItem
258 className={cn(toolbarButtonVariants({ size, variant }), className)}
259 {...props}
260 />
261 );
262 }
263
264 export function ToolbarGroup({
265 children,
266 className,
267 }: React.ComponentProps<"div">) {
268 return (
269 <div
270 className={cn(
271 "group/toolbar-group",
272 "relative hidden has-[button]:flex",
273 className,
274 )}
275 >
276 <div className="flex items-center">{children}</div>
277
278 <div className="mx-1.5 py-0.5 group-last/toolbar-group:hidden!">
279 <Separator orientation="vertical" />
280 </div>
281 </div>
282 );
283 }
284
285 type TooltipProps<T extends React.ElementType> = {
286 tooltip?: React.ReactNode;
287 tooltipContentProps?: Omit<
288 React.ComponentPropsWithoutRef<typeof TooltipContent>,
289 "children"
290 >;
291 tooltipProps?: Omit<
292 React.ComponentPropsWithoutRef<typeof Tooltip>,
293 "children"
294 >;
295 tooltipTriggerProps?: React.ComponentPropsWithoutRef<typeof TooltipTrigger>;
296 } & React.ComponentProps<T>;
297
298 function withTooltip<T extends React.ElementType>(Component: T) {
299 return function ExtendComponent({
300 tooltip,
301 tooltipContentProps,
302 tooltipProps,
303 tooltipTriggerProps,
304 ...props
305 }: TooltipProps<T>) {
306 const [mounted, setMounted] = React.useState(false);
307
308 React.useEffect(() => {
309 setMounted(true);
310 }, []);
311
312 const component = <Component {...(props as React.ComponentProps<T>)} />;
313
314 if (tooltip && mounted) {
315 return (
316 <Tooltip {...tooltipProps}>
317 <TooltipTrigger asChild {...tooltipTriggerProps}>
318 {component}
319 </TooltipTrigger>
320
321 <TooltipContent {...tooltipContentProps}>{tooltip}</TooltipContent>
322 </Tooltip>
323 );
324 }
325
326 return component;
327 };
328 }
329
330 function TooltipContent({
331 children,
332 className,
333 // CHANGE
334 sideOffset = 4,
335 ...props
336 }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
337 return (
338 <TooltipPrimitive.Portal>
339 <TooltipPrimitive.Content
340 className={cn(
341 "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md bg-primary px-3 py-1.5 text-xs text-balance text-primary-foreground",
342 className,
343 )}
344 data-slot="tooltip-content"
345 sideOffset={sideOffset}
346 {...props}
347 >
348 {children}
349 {/* CHANGE */}
350 {/* <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-primary fill-primary" /> */}
351 </TooltipPrimitive.Content>
352 </TooltipPrimitive.Portal>
353 );
354 }
355
356 export function ToolbarMenuGroup({
357 children,
358 className,
359 label,
360 ...props
361 }: React.ComponentProps<typeof DropdownMenuRadioGroup> & { label?: string }) {
362 return (
363 <>
364 <DropdownMenuSeparator
365 className={cn(
366 "hidden",
367 "mb-0 shrink-0 peer-has-[[role=menuitem]]/menu-group:block peer-has-[[role=menuitemradio]]/menu-group:block peer-has-[[role=option]]/menu-group:block",
368 )}
369 />
370
371 <DropdownMenuRadioGroup
372 {...props}
373 className={cn(
374 "hidden",
375 "peer/menu-group group/menu-group my-1.5 has-[[role=menuitem]]:block has-[[role=menuitemradio]]:block has-[[role=option]]:block",
376 className,
377 )}
378 >
379 {label && (
380 <DropdownMenuLabel className="text-xs font-semibold text-muted-foreground select-none">
381 {label}
382 </DropdownMenuLabel>
383 )}
384 {children}
385 </DropdownMenuRadioGroup>
386 </>
387 );
388 }
389
389 lines Plain Text