diff --git a/src/components/ErrorPopover.tsx b/src/components/ErrorPopover.tsx index 17d11e4..282f26f 100644 --- a/src/components/ErrorPopover.tsx +++ b/src/components/ErrorPopover.tsx @@ -1,10 +1,13 @@ -import { - ExclamationTriangleIcon, - InformationCircleIcon, -} from "@heroicons/react/24/solid"; import { getErrorDetails } from "../utils/errorCodeHelpers"; import { PopoverContent } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; +import { + InfoCard, + InfoCardTitle, + InfoCardDescription, + InfoCardList, + InfoCardListItem, +} from "@/components/ui/info-card"; interface ErrorPopoverContentProps { machineError?: number; @@ -24,71 +27,50 @@ export function ErrorPopoverContent({ const errorMsg = pyodideError || errorMessage || ""; const isInfo = isPairingErr || errorDetails?.isInformational; - const bgColor = isInfo - ? "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500" - : "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500"; - - const iconColor = isInfo - ? "text-info-600 dark:text-info-400" - : "text-danger-600 dark:text-danger-400"; - - const textColor = isInfo - ? "text-info-900 dark:text-info-200" - : "text-danger-900 dark:text-danger-200"; - - const descColor = isInfo - ? "text-info-800 dark:text-info-300" - : "text-danger-800 dark:text-danger-300"; - - const listColor = isInfo - ? "text-info-700 dark:text-info-300" - : "text-danger-700 dark:text-danger-300"; - - const Icon = isInfo ? InformationCircleIcon : ExclamationTriangleIcon; + const variant = isInfo ? "info" : "error"; const title = errorDetails?.title || (isPairingErr ? "Pairing Required" : "Error"); return ( - -
- -
-

- {title} -

-

- {errorDetails?.description || errorMsg} + + + {title} + + {errorDetails?.description || errorMsg} + + {errorDetails?.solutions && errorDetails.solutions.length > 0 && ( + <> +

+ {isInfo ? "Steps:" : "How to Fix:"} +

+ + {errorDetails.solutions.map((solution, index) => ( + {solution} + ))} + + + )} + {machineError !== undefined && !errorDetails?.isInformational && ( +

+ Error Code: 0x + {machineError.toString(16).toUpperCase().padStart(2, "0")}

- {errorDetails?.solutions && errorDetails.solutions.length > 0 && ( - <> -

- {isInfo ? "Steps:" : "How to Fix:"} -

-
    - {errorDetails.solutions.map((solution, index) => ( -
  1. - {solution} -
  2. - ))} -
- - )} - {machineError !== undefined && !errorDetails?.isInformational && ( -

- Error Code: 0x - {machineError.toString(16).toUpperCase().padStart(2, "0")} -

- )} -
-
+ )} +
); } diff --git a/src/components/WorkflowStepper/StepPopover.tsx b/src/components/WorkflowStepper/StepPopover.tsx index d625fae..741f075 100644 --- a/src/components/WorkflowStepper/StepPopover.tsx +++ b/src/components/WorkflowStepper/StepPopover.tsx @@ -5,12 +5,15 @@ */ import { forwardRef } from "react"; -import { - InformationCircleIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/solid"; import { MachineStatus } from "../../types/machine"; import { getGuideContent } from "../../utils/workflowGuideContent"; +import { + InfoCard, + InfoCardTitle, + InfoCardDescription, + InfoCardList, + InfoCardListItem, +} from "@/components/ui/info-card"; export interface StepPopoverProps { stepId: number; @@ -22,54 +25,16 @@ export const StepPopover = forwardRef( const content = getGuideContent(stepId, machineStatus); if (!content) return null; - const colorClasses = { - info: "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500", - success: - "bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500", - warning: - "bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500", - error: - "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500", - progress: - "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500", - }; + // Map content type to InfoCard variant + const variantMap = { + info: "info", + success: "success", + warning: "warning", + error: "error", + progress: "info", + } as const; - const iconColorClasses = { - info: "text-info-600 dark:text-info-400", - success: "text-success-600 dark:text-success-400", - warning: "text-warning-600 dark:text-warning-400", - error: "text-danger-600 dark:text-danger-400", - progress: "text-info-600 dark:text-info-400", - }; - - const textColorClasses = { - info: "text-info-900 dark:text-info-200", - success: "text-success-900 dark:text-success-200", - warning: "text-warning-900 dark:text-warning-200", - error: "text-danger-900 dark:text-danger-200", - progress: "text-info-900 dark:text-info-200", - }; - - const descColorClasses = { - info: "text-info-800 dark:text-info-300", - success: "text-success-800 dark:text-success-300", - warning: "text-warning-800 dark:text-warning-300", - error: "text-danger-800 dark:text-danger-300", - progress: "text-info-800 dark:text-info-300", - }; - - const listColorClasses = { - info: "text-blue-700 dark:text-blue-300", - success: "text-green-700 dark:text-green-300", - warning: "text-yellow-700 dark:text-yellow-300", - error: "text-red-700 dark:text-red-300", - progress: "text-cyan-700 dark:text-cyan-300", - }; - - const Icon = - content.type === "warning" - ? ExclamationTriangleIcon - : InformationCircleIcon; + const variant = variantMap[content.type]; return (
( role="dialog" aria-label="Step guidance" > -
-
- -
-

- {content.title} -

-

- {content.description} -

- {content.items && content.items.length > 0 && ( -
    - {content.items.map((item, index) => { - // Parse **text** markdown syntax into React elements safely - const parts = item.split(/(\*\*.*?\*\*)/); - return ( -
  • - {parts.map((part, i) => { - if (part.startsWith("**") && part.endsWith("**")) { - return {part.slice(2, -2)}; - } - return part; - })} -
  • - ); - })} -
- )} -
-
-
+ + {content.title} + + {content.description} + + {content.items && content.items.length > 0 && ( + + {content.items.map((item, index) => { + // Parse **text** markdown syntax into React elements safely + const parts = item.split(/(\*\*.*?\*\*)/); + return ( + + {parts.map((part, i) => { + if (part.startsWith("**") && part.endsWith("**")) { + return {part.slice(2, -2)}; + } + return part; + })} + + ); + })} + + )} +
); }, diff --git a/src/components/ui/info-card.test.tsx b/src/components/ui/info-card.test.tsx index 637dcff..94ef41b 100644 --- a/src/components/ui/info-card.test.tsx +++ b/src/components/ui/info-card.test.tsx @@ -31,7 +31,8 @@ describe("InfoCard", () => { ); expect(screen.getByText(`${variant} Card`)).toBeTruthy(); - expect(container.firstChild?.classList.contains("border-l-4")).toBe(true); + const firstChild = container.firstChild as HTMLElement | null; + expect(firstChild?.classList.contains("border-l-4")).toBe(true); }); }); @@ -77,9 +78,7 @@ describe("InfoCard", () => { describe("InfoCardDescription", () => { it("should render description text", () => { - render( - This is a description, - ); + render(This is a description); expect(screen.getByText("This is a description")).toBeTruthy(); }); @@ -150,9 +149,7 @@ describe("InfoCard", () => { ); expect(screen.getByText("Success!")).toBeTruthy(); - expect( - screen.getByText("Operation completed successfully"), - ).toBeTruthy(); + expect(screen.getByText("Operation completed successfully")).toBeTruthy(); expect(screen.getByText("First step completed")).toBeTruthy(); expect(screen.getByText("Second step completed")).toBeTruthy(); }); diff --git a/src/components/ui/info-card.tsx b/src/components/ui/info-card.tsx index a2b7bee..3d33856 100644 --- a/src/components/ui/info-card.tsx +++ b/src/components/ui/info-card.tsx @@ -8,25 +8,22 @@ import { import { cn } from "@/lib/utils"; -const infoCardVariants = cva( - "border-l-4 p-4 rounded-lg backdrop-blur-sm", - { - variants: { - variant: { - info: "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500", - warning: - "bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500", - error: - "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500", - success: - "bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500", - }, - }, - defaultVariants: { - variant: "info", +const infoCardVariants = cva("border-l-4 p-4 rounded-lg backdrop-blur-sm", { + variants: { + variant: { + info: "bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500", + warning: + "bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500", + error: + "bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500", + success: + "bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500", }, }, -); + defaultVariants: { + variant: "info", + }, +}); const iconColorVariants = cva("w-6 h-6 flex-shrink-0 mt-0.5", { variants: { @@ -85,7 +82,8 @@ const listColorVariants = cva("text-sm", { }); interface InfoCardProps - extends React.HTMLAttributes, + extends + React.HTMLAttributes, VariantProps { icon?: React.ComponentType>; showDefaultIcon?: boolean; @@ -145,7 +143,8 @@ function InfoCardDescription({ } interface InfoCardListProps - extends React.HTMLAttributes, + extends + React.HTMLAttributes, VariantProps { ordered?: boolean; }