| 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 |