From 5849a1e85429a2708320d18b7fbc2f7c7b5db128 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sun, 21 Dec 2025 00:06:38 +0100 Subject: [PATCH] feature: Migrate ErrorPopover to shadcn Popover component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migrated ErrorPopover to use shadcn PopoverContent - Removed forwardRef wrapper (handled by shadcn internally) - Replaced custom absolute positioning with PopoverContent component - Used cn() utility for cleaner className management - Maintained all styling with info/danger color variants - Updated AppHeader to use Popover pattern - Replaced manual state management (showErrorPopover/setErrorPopover) - Removed refs and click-outside detection useEffect - Wrapped error button in Popover component with PopoverTrigger - Simplified code by removing 30+ lines of manual popover handling Benefits: - Better keyboard navigation and accessibility (built into Radix UI) - Automatic focus management and ARIA attributes - Cleaner, more maintainable code - Consistent with other shadcn components in the app 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/components/AppHeader.tsx | 126 +++++++++++---------------- src/components/ErrorPopover.tsx | 147 ++++++++++++++++---------------- 2 files changed, 123 insertions(+), 150 deletions(-) diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 86eb809..0defd5c 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -1,9 +1,8 @@ -import { useRef, useEffect } from "react"; import { useShallow } from "zustand/react/shallow"; import { useMachineStore } from "../stores/useMachineStore"; import { useUIStore } from "../stores/useUIStore"; import { WorkflowStepper } from "./WorkflowStepper"; -import { ErrorPopover } from "./ErrorPopover"; +import { ErrorPopoverContent } from "./ErrorPopover"; import { getStateVisualInfo } from "../utils/machineStateHelpers"; import { CheckCircleIcon, @@ -15,6 +14,7 @@ import { } from "@heroicons/react/24/solid"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { Popover, PopoverTrigger } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; export function AppHeader() { @@ -42,17 +42,12 @@ export function AppHeader() { })), ); - const { pyodideError, showErrorPopover, setErrorPopover } = useUIStore( + const { pyodideError } = useUIStore( useShallow((state) => ({ pyodideError: state.pyodideError, - showErrorPopover: state.showErrorPopover, - setErrorPopover: state.setErrorPopover, })), ); - const errorPopoverRef = useRef(null); - const errorButtonRef = useRef(null); - // Get state visual info for header status badge const stateVisual = getStateVisualInfo(machineStatus); const stateIcons = { @@ -65,26 +60,6 @@ export function AppHeader() { }; const StatusIcon = stateIcons[stateVisual.iconName]; - // Close error popover when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - errorPopoverRef.current && - !errorPopoverRef.current.contains(event.target as Node) && - errorButtonRef.current && - !errorButtonRef.current.contains(event.target as Node) - ) { - setErrorPopover(false); - } - }; - - if (showErrorPopover) { - document.addEventListener("mousedown", handleClickOutside); - return () => - document.removeEventListener("mousedown", handleClickOutside); - } - }, [showErrorPopover, setErrorPopover]); - return (
@@ -157,64 +132,63 @@ export function AppHeader() { )} {/* Error indicator - always render to prevent layout shift */} -
- + // Default fallback + return "Error"; + })()} + + + - {/* Error popover */} - {showErrorPopover && (machineErrorMessage || pyodideError) && ( - )} -
+
diff --git a/src/components/ErrorPopover.tsx b/src/components/ErrorPopover.tsx index fd98541..17d11e4 100644 --- a/src/components/ErrorPopover.tsx +++ b/src/components/ErrorPopover.tsx @@ -1,95 +1,94 @@ -import { forwardRef } from "react"; import { ExclamationTriangleIcon, InformationCircleIcon, } from "@heroicons/react/24/solid"; import { getErrorDetails } from "../utils/errorCodeHelpers"; +import { PopoverContent } from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; -interface ErrorPopoverProps { +interface ErrorPopoverContentProps { machineError?: number; isPairingError: boolean; errorMessage?: string | null; pyodideError?: string | null; } -export const ErrorPopover = forwardRef( - ({ machineError, isPairingError, errorMessage, pyodideError }, ref) => { - const errorDetails = getErrorDetails(machineError); - const isPairingErr = isPairingError; - const errorMsg = pyodideError || errorMessage || ""; - const isInfo = isPairingErr || errorDetails?.isInformational; +export function ErrorPopoverContent({ + machineError, + isPairingError, + errorMessage, + pyodideError, +}: ErrorPopoverContentProps) { + const errorDetails = getErrorDetails(machineError); + const isPairingErr = isPairingError; + 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 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 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 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 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 listColor = isInfo + ? "text-info-700 dark:text-info-300" + : "text-danger-700 dark:text-danger-300"; - const Icon = isInfo ? InformationCircleIcon : ExclamationTriangleIcon; - const title = - errorDetails?.title || (isPairingErr ? "Pairing Required" : "Error"); + const Icon = isInfo ? InformationCircleIcon : ExclamationTriangleIcon; + const title = + errorDetails?.title || (isPairingErr ? "Pairing Required" : "Error"); - return ( -
-
-
- -
-

- {title} -

-

- {errorDetails?.description || errorMsg} -

- {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")} -

- )} -
-
+ return ( + +
+ +
+

+ {title} +

+

+ {errorDetails?.description || errorMsg} +

+ {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")} +

+ )}
- ); - }, -); - -ErrorPopover.displayName = "ErrorPopover"; +
+ ); +}