All files / client/src/components/ui toolbar-icon-button.tsx

100% Statements 18/18
96% Branches 24/25
100% Functions 3/3
100% Lines 14/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98                                        9x                       184x 124x 5x                 184x 170x 169x                                     184x 184x 184x 184x 184x 184x                 184x                                      
import * as React from "react";
import type { ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { getStatusHoverTextClass, getStatusTextClass } from "@/lib/status-semantics";
 
interface ToolbarIconButtonProps
  extends Omit<React.ComponentPropsWithoutRef<typeof Button>, "children" | "size" | "variant"> {
  readonly icon: ReactNode;
  readonly label: string;
  readonly destructive?: boolean;
  readonly pressed?: boolean;
  readonly status?: "default" | "success" | "muted" | "bright" | "error";
  readonly animated?: boolean;
  readonly animation?: "generating" | "waiting" | "evaluating" | "error";
  readonly iconSize?: "default" | "lg";
}
 
type ToolbarIconStatus = NonNullable<ToolbarIconButtonProps["status"]>;
 
const STATUS_CLASS_NAMES: Record<ToolbarIconStatus, string | undefined> = {
  success: `${getStatusTextClass("success")} ${getStatusHoverTextClass("success")}`,
  muted: "text-muted-foreground hover:text-muted-foreground",
  bright: "text-foreground hover:text-foreground",
  error: `${getStatusTextClass("error")} ${getStatusHoverTextClass("error")}`,
  default: undefined,
};
 
function getToolbarIconStatusClassName(
  status: ToolbarIconButtonProps["status"],
  pressed: boolean | undefined,
) {
  if (status !== undefined) return STATUS_CLASS_NAMES[status];
  if (pressed === undefined) return undefined;
  return pressed
    ? "text-status-success hover:text-status-success"
    : "text-muted-foreground hover:text-muted-foreground";
}
 
function getToolbarIconAnimationClassName(
  animation: ToolbarIconButtonProps["animation"],
  animated: boolean,
) {
  if (animation !== undefined) return `toolbar-icon-button-animation-${animation}`;
  if (animated) return "toolbar-icon-button-animated";
  return undefined;
}
 
/** Shared icon-only action used by compact panel toolbars. */
export function ToolbarIconButton({
  icon,
  label,
  destructive = false,
  pressed,
  status,
  animated = false,
  animation,
  iconSize = "default",
  className,
  title,
  "aria-label": ariaLabel,
  "aria-pressed": ariaPressed,
  ...props
}: Readonly<ToolbarIconButtonProps>) {
  const resolvedAriaPressed = ariaPressed ?? pressed;
  const statusClassName = getToolbarIconStatusClassName(status, pressed);
  const animationClassName = getToolbarIconAnimationClassName(animation, animated);
  let renderedIcon = icon;
  Eif (React.isValidElement(icon)) {
    renderedIcon = React.cloneElement(icon, {
        className: cn(
          destructive && "text-red-500",
          animationClassName,
          (icon.props as { className?: string }).className,
        ),
      });
  }
 
  return (
    <Button
      {...props}
      variant="ghost"
      size="icon"
      className={cn(
        "hover:border-transparent",
        statusClassName,
        iconSize === "lg" ? "[&_svg]:!h-5 [&_svg]:!w-5" : undefined,
        className,
      )}
      aria-label={ariaLabel ?? label}
      title={title ?? label}
      aria-pressed={resolvedAriaPressed}
    >
      {renderedIcon}
    </Button>
  );
}