mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
Move error display to header with clickable popover
- Replace content-pushing error banners with header status indicator - Add categorized error labels (Python Error, Connection Error, etc.) - Show detailed error info with solutions in 600px popover on click - Fix layout shift by always rendering error button (invisible when no error) - Clear machineError state on disconnect - Fix TypeScript error in WorkflowStepper ref callback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bf3e397ddb
commit
efc712995b
4 changed files with 467 additions and 419 deletions
207
src/App.tsx
207
src/App.tsx
|
|
@ -1,17 +1,16 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useBrotherMachine } from './hooks/useBrotherMachine';
|
||||
import { FileUpload } from './components/FileUpload';
|
||||
import { PatternCanvas } from './components/PatternCanvas';
|
||||
import { ProgressMonitor } from './components/ProgressMonitor';
|
||||
import { WorkflowStepper } from './components/WorkflowStepper';
|
||||
import { NextStepGuide } from './components/NextStepGuide';
|
||||
import { PatternSummaryCard } from './components/PatternSummaryCard';
|
||||
import { BluetoothDevicePicker } from './components/BluetoothDevicePicker';
|
||||
import type { PesPatternData } from './utils/pystitchConverter';
|
||||
import { pyodideLoader } from './utils/pyodideLoader';
|
||||
import { hasError } from './utils/errorCodeHelpers';
|
||||
import { hasError, getErrorDetails } from './utils/errorCodeHelpers';
|
||||
import { canDeletePattern, getStateVisualInfo } from './utils/machineStateHelpers';
|
||||
import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/solid';
|
||||
import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon, InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
|
|
@ -22,6 +21,9 @@ function App() {
|
|||
const [patternOffset, setPatternOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const [patternUploaded, setPatternUploaded] = useState(false);
|
||||
const [currentFileName, setCurrentFileName] = useState<string>(''); // Track current pattern filename
|
||||
const [showErrorPopover, setShowErrorPopover] = useState(false);
|
||||
const errorPopoverRef = useRef<HTMLDivElement>(null);
|
||||
const errorButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Initialize Pyodide on mount
|
||||
useEffect(() => {
|
||||
|
|
@ -37,6 +39,25 @@ function App() {
|
|||
});
|
||||
}, []);
|
||||
|
||||
// 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)
|
||||
) {
|
||||
setShowErrorPopover(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (showErrorPopover) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [showErrorPopover]);
|
||||
|
||||
// Auto-load cached pattern when available
|
||||
const resumedPattern = machine.resumedPattern;
|
||||
const resumeFileName = machine.resumeFileName;
|
||||
|
|
@ -142,7 +163,7 @@ function App() {
|
|||
<ArrowPathIcon className="w-3.5 h-3.5 text-blue-200 animate-spin" title="Auto-refreshing status" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="flex items-center gap-2 mt-1 min-h-[32px]">
|
||||
{machine.isConnected ? (
|
||||
<>
|
||||
<button
|
||||
|
|
@ -162,6 +183,122 @@ function App() {
|
|||
) : (
|
||||
<p className="text-xs text-blue-200">Not Connected</p>
|
||||
)}
|
||||
|
||||
{/* Error indicator - always render to prevent layout shift */}
|
||||
<div className="relative">
|
||||
<button
|
||||
ref={errorButtonRef}
|
||||
onClick={() => setShowErrorPopover(!showErrorPopover)}
|
||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 sm:py-1 rounded text-sm font-medium bg-red-500/90 hover:bg-red-600 text-white border border-red-400 transition-all flex-shrink-0 ${
|
||||
(machine.error || pyodideError)
|
||||
? 'cursor-pointer animate-pulse hover:animate-none'
|
||||
: 'invisible pointer-events-none'
|
||||
}`}
|
||||
title="Click to view error details"
|
||||
aria-label="View error details"
|
||||
disabled={!(machine.error || pyodideError)}
|
||||
>
|
||||
<ExclamationTriangleIcon className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span>
|
||||
{(() => {
|
||||
if (pyodideError) return 'Python Error';
|
||||
if (machine.isPairingError) return 'Pairing Required';
|
||||
|
||||
const errorMsg = machine.error || '';
|
||||
|
||||
// Categorize by error message content
|
||||
if (errorMsg.toLowerCase().includes('bluetooth') || errorMsg.toLowerCase().includes('connection')) {
|
||||
return 'Connection Error';
|
||||
}
|
||||
if (errorMsg.toLowerCase().includes('upload')) {
|
||||
return 'Upload Error';
|
||||
}
|
||||
if (errorMsg.toLowerCase().includes('pattern')) {
|
||||
return 'Pattern Error';
|
||||
}
|
||||
if (machine.machineError !== undefined) {
|
||||
return `Machine Error`;
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 'Error';
|
||||
})()}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Error popover */}
|
||||
{showErrorPopover && (machine.error || pyodideError) && (
|
||||
<div
|
||||
ref={errorPopoverRef}
|
||||
className="absolute top-full mt-2 left-0 w-[600px] z-50 animate-fadeIn"
|
||||
role="dialog"
|
||||
aria-label="Error details"
|
||||
>
|
||||
{(() => {
|
||||
const errorDetails = getErrorDetails(machine.machineError);
|
||||
const isPairingError = machine.isPairingError;
|
||||
const errorMsg = pyodideError || machine.error || '';
|
||||
const isInfo = isPairingError || errorDetails?.isInformational;
|
||||
|
||||
const bgColor = isInfo
|
||||
? 'bg-blue-50 dark:bg-blue-900/95 border-blue-600 dark:border-blue-500'
|
||||
: 'bg-red-50 dark:bg-red-900/95 border-red-600 dark:border-red-500';
|
||||
|
||||
const iconColor = isInfo
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-red-600 dark:text-red-400';
|
||||
|
||||
const textColor = isInfo
|
||||
? 'text-blue-900 dark:text-blue-200'
|
||||
: 'text-red-900 dark:text-red-200';
|
||||
|
||||
const descColor = isInfo
|
||||
? 'text-blue-800 dark:text-blue-300'
|
||||
: 'text-red-800 dark:text-red-300';
|
||||
|
||||
const listColor = isInfo
|
||||
? 'text-blue-700 dark:text-blue-300'
|
||||
: 'text-red-700 dark:text-red-300';
|
||||
|
||||
const Icon = isInfo ? InformationCircleIcon : ExclamationTriangleIcon;
|
||||
const title = errorDetails?.title || (isPairingError ? 'Pairing Required' : 'Error');
|
||||
|
||||
return (
|
||||
<div className={`${bgColor} border-l-4 p-4 rounded-lg shadow-xl backdrop-blur-sm`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<Icon className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} />
|
||||
<div className="flex-1">
|
||||
<h3 className={`text-base font-semibold ${textColor} mb-2`}>
|
||||
{title}
|
||||
</h3>
|
||||
<p className={`text-sm ${descColor} mb-3`}>
|
||||
{errorDetails?.description || errorMsg}
|
||||
</p>
|
||||
{errorDetails?.solutions && errorDetails.solutions.length > 0 && (
|
||||
<>
|
||||
<h4 className={`text-sm font-semibold ${textColor} mb-2`}>
|
||||
{isInfo ? 'Steps:' : 'How to Fix:'}
|
||||
</h4>
|
||||
<ol className={`list-decimal list-inside text-sm ${listColor} space-y-1.5`}>
|
||||
{errorDetails.solutions.map((solution, index) => (
|
||||
<li key={index} className="pl-2">{solution}</li>
|
||||
))}
|
||||
</ol>
|
||||
</>
|
||||
)}
|
||||
{machine.machineError !== undefined && !errorDetails?.isInformational && (
|
||||
<p className={`text-xs ${descColor} mt-3 font-mono`}>
|
||||
Error Code: 0x{machine.machineError.toString(16).toUpperCase().padStart(2, '0')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -173,58 +310,15 @@ function App() {
|
|||
isConnected={machine.isConnected}
|
||||
hasPattern={pesData !== null}
|
||||
patternUploaded={patternUploaded}
|
||||
hasError={hasError(machine.machineError)}
|
||||
errorMessage={machine.error || undefined}
|
||||
errorCode={machine.machineError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 p-4 sm:p-5 lg:p-6 w-full overflow-y-auto lg:overflow-hidden flex flex-col">
|
||||
{/* Global errors */}
|
||||
{machine.error && (
|
||||
<div className={`px-6 py-4 rounded-lg border-l-4 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn ${
|
||||
machine.isPairingError
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-200 border-blue-600 dark:border-blue-500'
|
||||
: 'bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-200 border-red-600 dark:border-red-500'
|
||||
}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<svg className={`w-5 h-5 flex-shrink-0 ${machine.isPairingError ? 'text-blue-600 dark:text-blue-400' : 'text-red-600 dark:text-red-400'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
{machine.isPairingError ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
)}
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold mb-1">{machine.isPairingError ? 'Pairing Required' : 'Error'}</div>
|
||||
<div className="text-sm">{machine.error}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{pyodideError && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-200 px-6 py-4 rounded-lg border-l-4 border-red-600 dark:border-red-500 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<strong className="font-semibold">Python Error:</strong> {pyodideError}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!pyodideReady && !pyodideError && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-200 px-6 py-4 rounded-lg border-l-4 border-blue-600 dark:border-blue-500 mb-6 shadow-md animate-fadeIn">
|
||||
<div className="flex items-center gap-3">
|
||||
<svg className="w-5 h-5 animate-spin text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span className="font-medium">Initializing Python environment...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
|
||||
{/* Left Column - Controls */}
|
||||
<div className="flex flex-col gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
|
||||
|
|
@ -362,17 +456,6 @@ function App() {
|
|||
{/* Bluetooth Device Picker (Electron only) */}
|
||||
<BluetoothDevicePicker />
|
||||
</div>
|
||||
|
||||
{/* Next Step Guide - Fixed floating overlay */}
|
||||
<NextStepGuide
|
||||
machineStatus={machine.machineStatus}
|
||||
isConnected={machine.isConnected}
|
||||
hasPattern={pesData !== null}
|
||||
patternUploaded={patternUploaded}
|
||||
hasError={hasError(machine.machineError)}
|
||||
errorMessage={machine.error || undefined}
|
||||
errorCode={machine.machineError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,354 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { InformationCircleIcon, ExclamationTriangleIcon, XMarkIcon } from '@heroicons/react/24/solid';
|
||||
import { MachineStatus } from '../types/machine';
|
||||
import { getErrorDetails } from '../utils/errorCodeHelpers';
|
||||
|
||||
interface NextStepGuideProps {
|
||||
machineStatus: MachineStatus;
|
||||
isConnected: boolean;
|
||||
hasPattern: boolean;
|
||||
patternUploaded: boolean;
|
||||
hasError: boolean;
|
||||
errorMessage?: string;
|
||||
errorCode?: number;
|
||||
}
|
||||
|
||||
export function NextStepGuide({
|
||||
machineStatus,
|
||||
isConnected,
|
||||
hasPattern,
|
||||
patternUploaded,
|
||||
hasError,
|
||||
errorMessage,
|
||||
errorCode
|
||||
}: NextStepGuideProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
// Expand when state changes
|
||||
useEffect(() => {
|
||||
setIsExpanded(true);
|
||||
}, [machineStatus, isConnected, hasPattern, patternUploaded, hasError]);
|
||||
|
||||
// Render guide content based on state
|
||||
const renderContent = () => {
|
||||
// Don't show if there's an error - show detailed error guidance instead
|
||||
if (hasError) {
|
||||
const errorDetails = getErrorDetails(errorCode);
|
||||
|
||||
// Check if this is informational (like initialization steps) vs a real error
|
||||
if (errorDetails?.isInformational) {
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">
|
||||
{errorDetails.title}
|
||||
</h3>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-300 mb-3">
|
||||
{errorDetails.description}
|
||||
</p>
|
||||
{errorDetails.solutions && errorDetails.solutions.length > 0 && (
|
||||
<>
|
||||
<h4 className="text-sm font-semibold text-blue-900 dark:text-blue-200 mb-2">Steps:</h4>
|
||||
<ol className="list-decimal list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1.5">
|
||||
{errorDetails.solutions.map((solution, index) => (
|
||||
<li key={index} className="pl-2">{solution}</li>
|
||||
))}
|
||||
</ol>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Regular error display for actual errors
|
||||
return (
|
||||
<div className="bg-red-50 dark:bg-red-900/95 border-l-4 border-red-600 dark:border-red-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<ExclamationTriangleIcon className="w-6 h-6 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-red-900 dark:text-red-200 mb-2">
|
||||
{errorDetails?.title || 'Error Occurred'}
|
||||
</h3>
|
||||
<p className="text-sm text-red-800 dark:text-red-300 mb-3">
|
||||
{errorDetails?.description || errorMessage || 'An error occurred. Please check the machine and try again.'}
|
||||
</p>
|
||||
{errorDetails?.solutions && errorDetails.solutions.length > 0 && (
|
||||
<>
|
||||
<h4 className="text-sm font-semibold text-red-900 dark:text-red-200 mb-2">How to Fix:</h4>
|
||||
<ol className="list-decimal list-inside text-sm text-red-700 dark:text-red-300 space-y-1.5">
|
||||
{errorDetails.solutions.map((solution, index) => (
|
||||
<li key={index} className="pl-2">{solution}</li>
|
||||
))}
|
||||
</ol>
|
||||
</>
|
||||
)}
|
||||
{errorCode !== undefined && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400 mt-3 font-mono">
|
||||
Error Code: 0x{errorCode.toString(16).toUpperCase().padStart(2, '0')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Determine what to show based on current state
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 1: Connect to Machine</h3>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-300 mb-3">To get started, connect to your Brother embroidery machine via Bluetooth.</p>
|
||||
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
||||
<li>Make sure your machine is powered on</li>
|
||||
<li>Enable Bluetooth on your machine</li>
|
||||
<li>Click the "Connect to Machine" button below</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasPattern) {
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 2: Load Your Pattern</h3>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-300 mb-3">Choose a PES embroidery file from your computer to preview and upload.</p>
|
||||
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
||||
<li>Click "Choose PES File" in the Pattern File section</li>
|
||||
<li>Select your embroidery design (.pes file)</li>
|
||||
<li>Review the pattern preview on the right</li>
|
||||
<li>You can drag the pattern to adjust its position</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!patternUploaded) {
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 3: Upload Pattern to Machine</h3>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-300 mb-3">Send your pattern to the embroidery machine to prepare for sewing.</p>
|
||||
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
||||
<li>Review the pattern preview to ensure it's positioned correctly</li>
|
||||
<li>Check the pattern size matches your hoop</li>
|
||||
<li>Click "Upload to Machine" when ready</li>
|
||||
<li>Wait for the upload to complete (this may take a minute)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Pattern is uploaded, guide based on machine status
|
||||
switch (machineStatus) {
|
||||
case MachineStatus.IDLE:
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 4: Start Mask Trace</h3>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-300 mb-3">The mask trace helps the machine understand the pattern boundaries.</p>
|
||||
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
||||
<li>Click "Start Mask Trace" button in the Sewing Progress section</li>
|
||||
<li>The machine will trace the pattern outline</li>
|
||||
<li>This ensures the hoop is positioned correctly</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MachineStatus.MASK_TRACE_LOCK_WAIT:
|
||||
return (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/95 border-l-4 border-yellow-600 dark:border-yellow-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Machine Action Required</h3>
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-300 mb-3">The machine is ready to trace the pattern outline.</p>
|
||||
<ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
|
||||
<li><strong>Press the button on your machine</strong> to confirm and start the mask trace</li>
|
||||
<li>Ensure the hoop is properly attached</li>
|
||||
<li>Make sure the needle area is clear</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MachineStatus.MASK_TRACING:
|
||||
return (
|
||||
<div className="bg-cyan-50 dark:bg-cyan-900/95 border-l-4 border-cyan-600 dark:border-cyan-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-cyan-600 dark:text-cyan-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-cyan-900 dark:text-cyan-200 mb-2">Mask Trace In Progress</h3>
|
||||
<p className="text-sm text-cyan-800 dark:text-cyan-300 mb-3">The machine is tracing the pattern boundary. Please wait...</p>
|
||||
<ul className="list-disc list-inside text-sm text-cyan-700 dark:text-cyan-300 space-y-1">
|
||||
<li>Watch the machine trace the outline</li>
|
||||
<li>Verify the pattern fits within your hoop</li>
|
||||
<li>Do not interrupt the machine</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MachineStatus.MASK_TRACE_COMPLETE:
|
||||
case MachineStatus.SEWING_WAIT:
|
||||
return (
|
||||
<div className="bg-green-50 dark:bg-green-900/95 border-l-4 border-green-600 dark:border-green-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-green-900 dark:text-green-200 mb-2">Step 5: Ready to Sew!</h3>
|
||||
<p className="text-sm text-green-800 dark:text-green-300 mb-3">The machine is ready to begin embroidering your pattern.</p>
|
||||
<ul className="list-disc list-inside text-sm text-green-700 dark:text-green-300 space-y-1">
|
||||
<li>Verify your thread colors are correct</li>
|
||||
<li>Ensure the fabric is properly hooped</li>
|
||||
<li>Click "Start Sewing" when ready</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MachineStatus.SEWING:
|
||||
return (
|
||||
<div className="bg-cyan-50 dark:bg-cyan-900/95 border-l-4 border-cyan-600 dark:border-cyan-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-cyan-600 dark:text-cyan-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-cyan-900 dark:text-cyan-200 mb-2">Step 6: Sewing In Progress</h3>
|
||||
<p className="text-sm text-cyan-800 dark:text-cyan-300 mb-3">Your embroidery is being stitched. Monitor the progress below.</p>
|
||||
<ul className="list-disc list-inside text-sm text-cyan-700 dark:text-cyan-300 space-y-1">
|
||||
<li>Watch the progress bar and current stitch count</li>
|
||||
<li>The machine will pause when a color change is needed</li>
|
||||
<li>Do not leave the machine unattended</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MachineStatus.COLOR_CHANGE_WAIT:
|
||||
return (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/95 border-l-4 border-yellow-600 dark:border-yellow-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Thread Change Required</h3>
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-300 mb-3">The machine needs a different thread color to continue.</p>
|
||||
<ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
|
||||
<li>Check the color blocks section to see which thread is needed</li>
|
||||
<li>Change to the correct thread color</li>
|
||||
<li><strong>Press the button on your machine</strong> to resume sewing</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MachineStatus.PAUSE:
|
||||
case MachineStatus.STOP:
|
||||
case MachineStatus.SEWING_INTERRUPTION:
|
||||
return (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/95 border-l-4 border-yellow-600 dark:border-yellow-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Sewing Paused</h3>
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-300 mb-3">The embroidery has been paused or interrupted.</p>
|
||||
<ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
|
||||
<li>Check if everything is okay with the machine</li>
|
||||
<li>Click "Resume Sewing" when ready to continue</li>
|
||||
<li>The machine will pick up where it left off</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case MachineStatus.SEWING_COMPLETE:
|
||||
return (
|
||||
<div className="bg-green-50 dark:bg-green-900/95 border-l-4 border-green-600 dark:border-green-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<InformationCircleIcon className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-green-900 dark:text-green-200 mb-2">Step 7: Embroidery Complete!</h3>
|
||||
<p className="text-sm text-green-800 dark:text-green-300 mb-3">Your embroidery is finished. Great work!</p>
|
||||
<ul className="list-disc list-inside text-sm text-green-700 dark:text-green-300 space-y-1">
|
||||
<li>Remove the hoop from the machine</li>
|
||||
<li>Press the Accept button on the machine</li>
|
||||
<li>Carefully remove your finished embroidery</li>
|
||||
<li>Trim any jump stitches or loose threads</li>
|
||||
<li>Click "Delete Pattern" to start a new project</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Render floating overlay
|
||||
return (
|
||||
<>
|
||||
{/* Collapsed state - small info button in bottom-right */}
|
||||
{!isExpanded && (
|
||||
<div className="fixed bottom-4 right-4 z-50 animate-fadeIn">
|
||||
<button
|
||||
onClick={() => setIsExpanded(true)}
|
||||
className="w-12 h-12 bg-blue-600 dark:bg-blue-700 hover:bg-blue-700 dark:hover:bg-blue-600 text-white rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110 animate-pulse"
|
||||
aria-label="Show next step guide"
|
||||
title="Show next step guide"
|
||||
>
|
||||
<InformationCircleIcon className="w-7 h-7" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expanded state - floating overlay */}
|
||||
{isExpanded && (
|
||||
<div className="fixed bottom-4 right-4 z-50 w-[90vw] max-w-[400px] max-h-[40vh] overflow-y-auto animate-slideInRight">
|
||||
<div className="relative rounded-lg overflow-hidden shadow-xl">
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={() => setIsExpanded(false)}
|
||||
className="absolute top-2 right-2 z-10 w-6 h-6 bg-white/90 dark:bg-gray-800/90 hover:bg-white dark:hover:bg-gray-700 rounded-full flex items-center justify-center transition-colors shadow-md"
|
||||
aria-label="Minimize guide"
|
||||
title="Minimize guide"
|
||||
>
|
||||
<XMarkIcon className="w-4 h-4 text-gray-700 dark:text-gray-300" />
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
import { CheckCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { CheckCircleIcon, InformationCircleIcon, ExclamationTriangleIcon } from '@heroicons/react/24/solid';
|
||||
import { MachineStatus } from '../types/machine';
|
||||
import { getErrorDetails } from '../utils/errorCodeHelpers';
|
||||
|
||||
interface WorkflowStepperProps {
|
||||
machineStatus: MachineStatus;
|
||||
isConnected: boolean;
|
||||
hasPattern: boolean;
|
||||
patternUploaded: boolean;
|
||||
hasError?: boolean;
|
||||
errorMessage?: string;
|
||||
errorCode?: number;
|
||||
}
|
||||
|
||||
interface Step {
|
||||
|
|
@ -24,6 +29,183 @@ const steps: Step[] = [
|
|||
{ id: 7, label: 'Complete', description: 'Finish and remove' },
|
||||
];
|
||||
|
||||
// Helper function to get guide content for a step
|
||||
function getGuideContent(
|
||||
stepId: number,
|
||||
machineStatus: MachineStatus,
|
||||
hasError: boolean,
|
||||
errorCode?: number,
|
||||
errorMessage?: string
|
||||
) {
|
||||
// Check for errors first
|
||||
if (hasError) {
|
||||
const errorDetails = getErrorDetails(errorCode);
|
||||
|
||||
if (errorDetails?.isInformational) {
|
||||
return {
|
||||
type: 'info' as const,
|
||||
title: errorDetails.title,
|
||||
description: errorDetails.description,
|
||||
items: errorDetails.solutions || []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'error' as const,
|
||||
title: errorDetails?.title || 'Error Occurred',
|
||||
description: errorDetails?.description || errorMessage || 'An error occurred. Please check the machine and try again.',
|
||||
items: errorDetails?.solutions || [],
|
||||
errorCode
|
||||
};
|
||||
}
|
||||
|
||||
// Return content based on step
|
||||
switch (stepId) {
|
||||
case 1:
|
||||
return {
|
||||
type: 'info' as const,
|
||||
title: 'Step 1: Connect to Machine',
|
||||
description: 'To get started, connect to your Brother embroidery machine via Bluetooth.',
|
||||
items: [
|
||||
'Make sure your machine is powered on',
|
||||
'Enable Bluetooth on your machine',
|
||||
'Click the "Connect to Machine" button below'
|
||||
]
|
||||
};
|
||||
|
||||
case 2:
|
||||
return {
|
||||
type: 'info' as const,
|
||||
title: 'Step 2: Load Your Pattern',
|
||||
description: 'Choose a PES embroidery file from your computer to preview and upload.',
|
||||
items: [
|
||||
'Click "Choose PES File" in the Pattern File section',
|
||||
'Select your embroidery design (.pes file)',
|
||||
'Review the pattern preview on the right',
|
||||
'You can drag the pattern to adjust its position'
|
||||
]
|
||||
};
|
||||
|
||||
case 3:
|
||||
return {
|
||||
type: 'info' as const,
|
||||
title: 'Step 3: Upload Pattern to Machine',
|
||||
description: 'Send your pattern to the embroidery machine to prepare for sewing.',
|
||||
items: [
|
||||
'Review the pattern preview to ensure it\'s positioned correctly',
|
||||
'Check the pattern size matches your hoop',
|
||||
'Click "Upload to Machine" when ready',
|
||||
'Wait for the upload to complete (this may take a minute)'
|
||||
]
|
||||
};
|
||||
|
||||
case 4:
|
||||
// Check machine status for substates
|
||||
if (machineStatus === MachineStatus.MASK_TRACE_LOCK_WAIT) {
|
||||
return {
|
||||
type: 'warning' as const,
|
||||
title: 'Machine Action Required',
|
||||
description: 'The machine is ready to trace the pattern outline.',
|
||||
items: [
|
||||
'Press the button on your machine to confirm and start the mask trace',
|
||||
'Ensure the hoop is properly attached',
|
||||
'Make sure the needle area is clear'
|
||||
]
|
||||
};
|
||||
}
|
||||
if (machineStatus === MachineStatus.MASK_TRACING) {
|
||||
return {
|
||||
type: 'progress' as const,
|
||||
title: 'Mask Trace In Progress',
|
||||
description: 'The machine is tracing the pattern boundary. Please wait...',
|
||||
items: [
|
||||
'Watch the machine trace the outline',
|
||||
'Verify the pattern fits within your hoop',
|
||||
'Do not interrupt the machine'
|
||||
]
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'info' as const,
|
||||
title: 'Step 4: Start Mask Trace',
|
||||
description: 'The mask trace helps the machine understand the pattern boundaries.',
|
||||
items: [
|
||||
'Click "Start Mask Trace" button in the Sewing Progress section',
|
||||
'The machine will trace the pattern outline',
|
||||
'This ensures the hoop is positioned correctly'
|
||||
]
|
||||
};
|
||||
|
||||
case 5:
|
||||
return {
|
||||
type: 'success' as const,
|
||||
title: 'Step 5: Ready to Sew!',
|
||||
description: 'The machine is ready to begin embroidering your pattern.',
|
||||
items: [
|
||||
'Verify your thread colors are correct',
|
||||
'Ensure the fabric is properly hooped',
|
||||
'Click "Start Sewing" when ready'
|
||||
]
|
||||
};
|
||||
|
||||
case 6:
|
||||
// Check for substates
|
||||
if (machineStatus === MachineStatus.COLOR_CHANGE_WAIT) {
|
||||
return {
|
||||
type: 'warning' as const,
|
||||
title: 'Thread Change Required',
|
||||
description: 'The machine needs a different thread color to continue.',
|
||||
items: [
|
||||
'Check the color blocks section to see which thread is needed',
|
||||
'Change to the correct thread color',
|
||||
'Press the button on your machine to resume sewing'
|
||||
]
|
||||
};
|
||||
}
|
||||
if (machineStatus === MachineStatus.PAUSE ||
|
||||
machineStatus === MachineStatus.STOP ||
|
||||
machineStatus === MachineStatus.SEWING_INTERRUPTION) {
|
||||
return {
|
||||
type: 'warning' as const,
|
||||
title: 'Sewing Paused',
|
||||
description: 'The embroidery has been paused or interrupted.',
|
||||
items: [
|
||||
'Check if everything is okay with the machine',
|
||||
'Click "Resume Sewing" when ready to continue',
|
||||
'The machine will pick up where it left off'
|
||||
]
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'progress' as const,
|
||||
title: 'Step 6: Sewing In Progress',
|
||||
description: 'Your embroidery is being stitched. Monitor the progress below.',
|
||||
items: [
|
||||
'Watch the progress bar and current stitch count',
|
||||
'The machine will pause when a color change is needed',
|
||||
'Do not leave the machine unattended'
|
||||
]
|
||||
};
|
||||
|
||||
case 7:
|
||||
return {
|
||||
type: 'success' as const,
|
||||
title: 'Step 7: Embroidery Complete!',
|
||||
description: 'Your embroidery is finished. Great work!',
|
||||
items: [
|
||||
'Remove the hoop from the machine',
|
||||
'Press the Accept button on the machine',
|
||||
'Carefully remove your finished embroidery',
|
||||
'Trim any jump stitches or loose threads',
|
||||
'Click "Delete Pattern" to start a new project'
|
||||
]
|
||||
};
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentStep(machineStatus: MachineStatus, isConnected: boolean, hasPattern: boolean, patternUploaded: boolean): number {
|
||||
if (!isConnected) return 1;
|
||||
if (!hasPattern) return 2;
|
||||
|
|
@ -55,8 +237,53 @@ function getCurrentStep(machineStatus: MachineStatus, isConnected: boolean, hasP
|
|||
}
|
||||
}
|
||||
|
||||
export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patternUploaded }: WorkflowStepperProps) {
|
||||
export function WorkflowStepper({
|
||||
machineStatus,
|
||||
isConnected,
|
||||
hasPattern,
|
||||
patternUploaded,
|
||||
hasError = false,
|
||||
errorMessage,
|
||||
errorCode
|
||||
}: WorkflowStepperProps) {
|
||||
const currentStep = getCurrentStep(machineStatus, isConnected, hasPattern, patternUploaded);
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const [popoverStep, setPopoverStep] = useState<number | null>(null);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const stepRefs = useRef<{ [key: number]: HTMLDivElement | null }>({});
|
||||
|
||||
// Close popover when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
|
||||
// Check if click was on a step circle
|
||||
const clickedStep = Object.values(stepRefs.current).find(ref =>
|
||||
ref?.contains(event.target as Node)
|
||||
);
|
||||
if (!clickedStep) {
|
||||
setShowPopover(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (showPopover) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [showPopover]);
|
||||
|
||||
const handleStepClick = (stepId: number) => {
|
||||
// Only allow clicking on current step or earlier completed steps
|
||||
if (stepId <= currentStep) {
|
||||
if (showPopover && popoverStep === stepId) {
|
||||
setShowPopover(false);
|
||||
setPopoverStep(null);
|
||||
} else {
|
||||
setPopoverStep(stepId);
|
||||
setShowPopover(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative max-w-5xl mx-auto mt-2 lg:mt-4" role="navigation" aria-label="Workflow progress">
|
||||
|
|
@ -94,13 +321,19 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
|
|||
>
|
||||
{/* Step circle */}
|
||||
<div
|
||||
ref={(el) => { stepRefs.current[step.id] = el; }}
|
||||
onClick={() => handleStepClick(step.id)}
|
||||
className={`
|
||||
w-8 h-8 lg:w-10 lg:h-10 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-300 border-2 shadow-md
|
||||
${step.id <= currentStep ? 'cursor-pointer hover:scale-110' : 'cursor-not-allowed'}
|
||||
${isComplete ? 'bg-green-500 dark:bg-green-600 border-green-400 dark:border-green-500 text-white shadow-green-500/30 dark:shadow-green-600/30' : ''}
|
||||
${isCurrent ? 'bg-blue-600 dark:bg-blue-700 border-blue-500 dark:border-blue-600 text-white scale-105 lg:scale-110 shadow-blue-600/40 dark:shadow-blue-700/40 ring-2 ring-blue-300 dark:ring-blue-500 ring-offset-2 dark:ring-offset-gray-900' : ''}
|
||||
${isUpcoming ? 'bg-blue-700 dark:bg-blue-800 border-blue-500/30 dark:border-blue-600/30 text-blue-200/70 dark:text-blue-300/70' : ''}
|
||||
${showPopover && popoverStep === step.id ? 'ring-4 ring-white dark:ring-gray-800' : ''}
|
||||
`}
|
||||
aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}`}
|
||||
aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}. Click for details.`}
|
||||
role="button"
|
||||
tabIndex={step.id <= currentStep ? 0 : -1}
|
||||
>
|
||||
{isComplete ? (
|
||||
<CheckCircleIcon className="w-5 h-5 lg:w-6 lg:h-6" aria-hidden="true" />
|
||||
|
|
@ -121,6 +354,91 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Popover */}
|
||||
{showPopover && popoverStep !== null && stepRefs.current[popoverStep] && (
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className="absolute top-full mt-4 left-1/2 transform -translate-x-1/2 w-full max-w-xl z-50 animate-fadeIn"
|
||||
role="dialog"
|
||||
aria-label="Step guidance"
|
||||
>
|
||||
{(() => {
|
||||
const content = getGuideContent(popoverStep, machineStatus, hasError, errorCode, errorMessage);
|
||||
if (!content) return null;
|
||||
|
||||
const colorClasses = {
|
||||
info: 'bg-blue-50 dark:bg-blue-900/95 border-blue-600 dark:border-blue-500',
|
||||
success: 'bg-green-50 dark:bg-green-900/95 border-green-600 dark:border-green-500',
|
||||
warning: 'bg-yellow-50 dark:bg-yellow-900/95 border-yellow-600 dark:border-yellow-500',
|
||||
error: 'bg-red-50 dark:bg-red-900/95 border-red-600 dark:border-red-500',
|
||||
progress: 'bg-cyan-50 dark:bg-cyan-900/95 border-cyan-600 dark:border-cyan-500'
|
||||
};
|
||||
|
||||
const iconColorClasses = {
|
||||
info: 'text-blue-600 dark:text-blue-400',
|
||||
success: 'text-green-600 dark:text-green-400',
|
||||
warning: 'text-yellow-600 dark:text-yellow-400',
|
||||
error: 'text-red-600 dark:text-red-400',
|
||||
progress: 'text-cyan-600 dark:text-cyan-400'
|
||||
};
|
||||
|
||||
const textColorClasses = {
|
||||
info: 'text-blue-900 dark:text-blue-200',
|
||||
success: 'text-green-900 dark:text-green-200',
|
||||
warning: 'text-yellow-900 dark:text-yellow-200',
|
||||
error: 'text-red-900 dark:text-red-200',
|
||||
progress: 'text-cyan-900 dark:text-cyan-200'
|
||||
};
|
||||
|
||||
const descColorClasses = {
|
||||
info: 'text-blue-800 dark:text-blue-300',
|
||||
success: 'text-green-800 dark:text-green-300',
|
||||
warning: 'text-yellow-800 dark:text-yellow-300',
|
||||
error: 'text-red-800 dark:text-red-300',
|
||||
progress: 'text-cyan-800 dark:text-cyan-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 === 'error' ? ExclamationTriangleIcon : InformationCircleIcon;
|
||||
|
||||
return (
|
||||
<div className={`${colorClasses[content.type]} border-l-4 p-4 rounded-lg shadow-xl backdrop-blur-sm`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<Icon className={`w-6 h-6 ${iconColorClasses[content.type]} flex-shrink-0 mt-0.5`} />
|
||||
<div className="flex-1">
|
||||
<h3 className={`text-base font-semibold ${textColorClasses[content.type]} mb-2`}>
|
||||
{content.title}
|
||||
</h3>
|
||||
<p className={`text-sm ${descColorClasses[content.type]} mb-3`}>
|
||||
{content.description}
|
||||
</p>
|
||||
{content.items && content.items.length > 0 && (
|
||||
<ul className={`list-disc list-inside text-sm ${listColorClasses[content.type]} space-y-1`}>
|
||||
{content.items.map((item, index) => (
|
||||
<li key={index} className="pl-2" dangerouslySetInnerHTML={{ __html: item.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') }} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{content.type === 'error' && content.errorCode !== undefined && (
|
||||
<p className={`text-xs ${descColorClasses[content.type]} mt-3 font-mono`}>
|
||||
Error Code: 0x{content.errorCode.toString(16).toUpperCase().padStart(2, '0')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ export function useBrotherMachine() {
|
|||
setPatternInfo(null);
|
||||
setSewingProgress(null);
|
||||
setError(null);
|
||||
setMachineError(SewingMachineError.None);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to disconnect");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue