diff --git a/src/components/ErrorPopover.tsx b/src/components/ErrorPopover.tsx index 17d11e4..fa2ab14 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 "./InfoCard"; 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/InfoCard.tsx b/src/components/InfoCard.tsx new file mode 100644 index 0000000..40c33d0 --- /dev/null +++ b/src/components/InfoCard.tsx @@ -0,0 +1,191 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { + InformationCircleIcon, + ExclamationTriangleIcon, + CheckCircleIcon, +} from "@heroicons/react/24/solid"; + +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 iconColorVariants = cva("w-6 h-6 flex-shrink-0 mt-0.5", { + variants: { + variant: { + info: "text-info-600 dark:text-info-400", + warning: "text-warning-600 dark:text-warning-400", + error: "text-danger-600 dark:text-danger-400", + success: "text-success-600 dark:text-success-400", + }, + }, + defaultVariants: { + variant: "info", + }, +}); + +const titleColorVariants = cva("text-base font-semibold mb-2", { + variants: { + variant: { + info: "text-info-900 dark:text-info-200", + warning: "text-warning-900 dark:text-warning-200", + error: "text-danger-900 dark:text-danger-200", + success: "text-success-900 dark:text-success-200", + }, + }, + defaultVariants: { + variant: "info", + }, +}); + +const descriptionColorVariants = cva("text-sm mb-3", { + variants: { + variant: { + info: "text-info-800 dark:text-info-300", + warning: "text-warning-800 dark:text-warning-300", + error: "text-danger-800 dark:text-danger-300", + success: "text-success-800 dark:text-success-300", + }, + }, + defaultVariants: { + variant: "info", + }, +}); + +const listColorVariants = cva("text-sm", { + variants: { + variant: { + info: "text-info-700 dark:text-info-300", + warning: "text-warning-700 dark:text-warning-300", + error: "text-danger-700 dark:text-danger-300", + success: "text-success-700 dark:text-success-300", + }, + }, + defaultVariants: { + variant: "info", + }, +}); + +interface InfoCardProps + extends + React.HTMLAttributes, + VariantProps { + icon?: React.ComponentType>; + showDefaultIcon?: boolean; +} + +function InfoCard({ + className, + variant = "info", + icon: CustomIcon, + showDefaultIcon = true, + children, + ...props +}: InfoCardProps) { + // Default icons based on variant + const defaultIcons = { + info: InformationCircleIcon, + warning: ExclamationTriangleIcon, + error: ExclamationTriangleIcon, + success: CheckCircleIcon, + } as const; + + const Icon = + CustomIcon || (showDefaultIcon && variant ? defaultIcons[variant] : null); + + return ( +
+
+ {Icon && } +
{children}
+
+
+ ); +} + +function InfoCardTitle({ + className, + variant = "info", + ...props +}: React.HTMLAttributes & + VariantProps) { + return ( +

+ ); +} + +function InfoCardDescription({ + className, + variant = "info", + ...props +}: React.HTMLAttributes & + VariantProps) { + return ( +

+ ); +} + +interface InfoCardListProps + extends + React.HTMLAttributes, + VariantProps { + ordered?: boolean; +} + +function InfoCardList({ + className, + variant = "info", + ordered = false, + children, + ...props +}: InfoCardListProps) { + const ListComponent = ordered ? "ol" : "ul"; + const listClass = ordered ? "list-decimal" : "list-disc"; + + return ( + + {children} + + ); +} + +function InfoCardListItem({ + className, + ...props +}: React.HTMLAttributes) { + return

  • ; +} + +export { + InfoCard, + InfoCardTitle, + InfoCardDescription, + InfoCardList, + InfoCardListItem, +}; diff --git a/src/components/WorkflowStepper/StepPopover.tsx b/src/components/WorkflowStepper/StepPopover.tsx index d625fae..43273e9 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 "../InfoCard"; 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; + })} + + ); + })} + + )} +
    ); },