All files / src/components/notifications Callout.tsx

100% Statements 38/38
50% Branches 1/2
50% Functions 1/2
100% Lines 38/38

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 632x 2x   2x 2x                       2x 2x 2x 2x 2x 2x             2x 3x 3x 3x 3x 3x 3x   3x 3x 3x 3x 3x 3x   3x 3x 3x 3x 3x 3x 3x 3x 3x 3x   3x 3x   3x   3x   3x  
import { useState, type ReactNode, type JSX } from "react";
import { AlertTriangle, Check, Info, X } from "lucide-react";
import { type CalloutBuiltinProps, type CalloutVariant } from "@grackle-ai/common";
import styles from "./Callout.module.scss";
import { ICON_LG, ICON_MD } from "../../utils/iconSize.js";
 
// `variant` now lives in the built-in's zod schema (@grackle-ai/common);
// re-export the union so the package barrel keeps exposing CalloutVariant.
export type { CalloutVariant };
 
interface CalloutProps extends CalloutBuiltinProps {
  children: ReactNode;
  /** Optional extra class name for layout overrides. */
  className?: string;
}
 
const VARIANT_ICONS: Record<CalloutVariant, ReactNode> = {
  success: <Check size={ICON_LG} />,
  error: <X size={ICON_LG} />,
  warning: <AlertTriangle size={ICON_LG} />,
  info: <Info size={ICON_LG} />,
};
 
/**
 * Inline contextual alert for persistent, location-specific information.
 * Use for things like blocked dependencies, validation errors, or
 * status messages that belong within a specific panel rather than a toast.
 */
export function Callout({
  variant = "info",
  children,
  dismissible = false,
  className,
}: CalloutProps): JSX.Element {
  const [dismissed, setDismissed] = useState(false);
 
  return (
    <>
      {!dismissed && (
        <div
          className={[styles.callout, styles[variant], className].filter(Boolean).join(" ")}
          role={variant === "error" || variant === "warning" ? "alert" : "status"}
        >
          <span className={styles.icon} aria-hidden="true">
            {VARIANT_ICONS[variant]}
          </span>
          <span className={styles.content}>{children}</span>
          {dismissible && (
            <button
              type="button"
              className={styles.close}
              onClick={() => setDismissed(true)}
              aria-label="Dismiss"
            >
              <X size={ICON_MD} aria-hidden="true" />
            </button>
          )}
        </div>
      )}
    </>
  );
}