diff --git a/src/App.tsx b/src/App.tsx index 3c5524a..c9bb4cc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,13 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { useShallow } from 'zustand/react/shallow'; import { useMachineStore } from './stores/useMachineStore'; import { usePatternStore } from './stores/usePatternStore'; import { useUIStore } from './stores/useUIStore'; -import { FileUpload } from './components/FileUpload'; +import { AppHeader } from './components/AppHeader'; +import { LeftSidebar } from './components/LeftSidebar'; import { PatternCanvas } from './components/PatternCanvas'; -import { ProgressMonitor } from './components/ProgressMonitor'; -import { WorkflowStepper } from './components/WorkflowStepper'; -import { PatternSummaryCard } from './components/PatternSummaryCard'; +import { PatternPreviewPlaceholder } from './components/PatternPreviewPlaceholder'; import { BluetoothDevicePicker } from './components/BluetoothDevicePicker'; -import { getErrorDetails } from './utils/errorCodeHelpers'; -import { getStateVisualInfo } from './utils/machineStateHelpers'; -import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon, InformationCircleIcon } from '@heroicons/react/24/solid'; -import { isBluetoothSupported } from './utils/bluetoothSupport'; import './App.css'; function App() { @@ -20,442 +15,69 @@ function App() { useEffect(() => { document.title = `Respira v${__APP_VERSION__}`; }, []); - // Machine store + + // Machine store - for auto-loading cached pattern const { - isConnected, - machineInfo, - machineStatus, - machineStatusName, - machineError, - patternInfo, - error: machineErrorMessage, - isPairingError, - isCommunicating: isPolling, - resumeFileName, resumedPattern, - connect, - disconnect, + resumeFileName, } = useMachineStore( useShallow((state) => ({ - isConnected: state.isConnected, - machineInfo: state.machineInfo, - machineStatus: state.machineStatus, - machineStatusName: state.machineStatusName, - machineError: state.machineError, - patternInfo: state.patternInfo, - error: state.error, - isPairingError: state.isPairingError, - isCommunicating: state.isCommunicating, - resumeFileName: state.resumeFileName, resumedPattern: state.resumedPattern, - connect: state.connect, - disconnect: state.disconnect, + resumeFileName: state.resumeFileName, })) ); - // Pattern store + // Pattern store - for auto-loading cached pattern const { pesData, - patternUploaded, setPattern, setPatternOffset, - setPatternUploaded, } = usePatternStore( useShallow((state) => ({ pesData: state.pesData, - patternUploaded: state.patternUploaded, setPattern: state.setPattern, setPatternOffset: state.setPatternOffset, - setPatternUploaded: state.setPatternUploaded, })) ); - // UI store + // UI store - for Pyodide initialization const { - pyodideError, - showErrorPopover, initializePyodide, - setErrorPopover, } = useUIStore( useShallow((state) => ({ - pyodideError: state.pyodideError, - showErrorPopover: state.showErrorPopover, initializePyodide: state.initializePyodide, - setErrorPopover: state.setErrorPopover, })) ); - const errorPopoverRef = useRef(null); - const errorButtonRef = useRef(null); - // Initialize Pyodide in background on mount (non-blocking thanks to worker) useEffect(() => { initializePyodide(); }, [initializePyodide]); - // 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]); - // Auto-load cached pattern when available - if (resumedPattern && !pesData) { - console.log('[App] Loading resumed pattern:', resumeFileName, 'Offset:', resumedPattern.patternOffset); - setPattern(resumedPattern.pesData, resumeFileName || ''); - // Restore the cached pattern offset - if (resumedPattern.patternOffset) { - setPatternOffset(resumedPattern.patternOffset.x, resumedPattern.patternOffset.y); + useEffect(() => { + if (resumedPattern && !pesData) { + console.log('[App] Loading resumed pattern:', resumeFileName, 'Offset:', resumedPattern.patternOffset); + setPattern(resumedPattern.pesData, resumeFileName || ''); + // Restore the cached pattern offset + if (resumedPattern.patternOffset) { + setPatternOffset(resumedPattern.patternOffset.x, resumedPattern.patternOffset.y); + } } - } - - - // Track pattern uploaded state based on machine status - if (!isConnected) { - if (patternUploaded) { - setPatternUploaded(false); - } - } else { - // Pattern is uploaded if machine has pattern info - const shouldBeUploaded = patternInfo !== null; - if (patternUploaded !== shouldBeUploaded) { - setPatternUploaded(shouldBeUploaded); - } - } - - // Get state visual info for header status badge - const stateVisual = getStateVisualInfo(machineStatus); - const stateIcons = { - ready: CheckCircleIcon, - active: BoltIcon, - waiting: PauseCircleIcon, - complete: CheckCircleIcon, - interrupted: PauseCircleIcon, - error: ExclamationTriangleIcon, - }; - const StatusIcon = stateIcons[stateVisual.iconName]; + }, [resumedPattern, resumeFileName, pesData, setPattern, setPatternOffset]); return (
-
-
- {/* Machine Connection Status - Responsive width column */} -
-
-
-
-
-

Respira

- {isConnected && machineInfo?.serialNumber && ( - - • {machineInfo.serialNumber} - - )} - {isPolling && ( - - )} -
-
- {isConnected ? ( - <> - - - - {machineStatusName} - - - ) : ( -

Not Connected

- )} - - {/* Error indicator - always render to prevent layout shift */} -
- - - {/* Error popover */} - {showErrorPopover && (machineErrorMessage || pyodideError) && ( -
- {(() => { - const errorDetails = getErrorDetails(machineError); - const isPairingErr = isPairingError; - const errorMsg = pyodideError || machineErrorMessage || ''; - const isInfo = isPairingErr || 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 || (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')} -

- )} -
-
-
- ); - })()} -
- )} -
-
-
-
- - {/* Workflow Stepper - Flexible width column */} -
- -
-
-
+
{/* Left Column - Controls */} -
- {/* Connect Button or Browser Hint - Show when disconnected */} - {!isConnected && ( - <> - {isBluetoothSupported() ? ( -
-
-
- - - -
-
-

Get Started

-

Connect to your embroidery machine

-
-
- -
- ) : ( -
-
- -
-

Browser Not Supported

-

- Your browser doesn't support Web Bluetooth, which is required to connect to your embroidery machine. -

-
-

Please try one of these options:

-
    -
  • Use a supported browser (Chrome, Edge, or Opera)
  • -
  • - Download the Desktop app from{' '} - - GitHub Releases - -
  • -
-
-
-
-
- )} - - )} - - {/* Pattern File - Show during upload stage (before pattern is uploaded) */} - {isConnected && !patternUploaded && ( - - )} - - {/* Compact Pattern Summary - Show after upload (during sewing stages) */} - {isConnected && patternUploaded && pesData && ( - - )} - - {/* Progress Monitor - Show when pattern is uploaded */} - {isConnected && patternUploaded && ( -
- -
- )} -
+ {/* Right Column - Pattern Preview */}
- {pesData ? ( - - ) : ( -
-

Pattern Preview

-
- {/* Decorative background pattern */} -
-
-
-
-
- -
-
- - - -
- - - -
-
-

No Pattern Loaded

-

- Connect to your machine and choose a PES embroidery file to see your design preview -

-
-
-
- Drag to Position -
-
-
- Zoom & Pan -
-
-
- Real-time Preview -
-
-
-
-
- )} + {pesData ? : }
diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx new file mode 100644 index 0000000..d11ab61 --- /dev/null +++ b/src/components/AppHeader.tsx @@ -0,0 +1,207 @@ +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 { getStateVisualInfo } from '../utils/machineStateHelpers'; +import { + CheckCircleIcon, + BoltIcon, + PauseCircleIcon, + ExclamationTriangleIcon, + ArrowPathIcon, + XMarkIcon, +} from '@heroicons/react/24/solid'; + +export function AppHeader() { + const { + isConnected, + machineInfo, + machineStatus, + machineStatusName, + machineError, + error: machineErrorMessage, + isPairingError, + isCommunicating: isPolling, + disconnect, + } = useMachineStore( + useShallow((state) => ({ + isConnected: state.isConnected, + machineInfo: state.machineInfo, + machineStatus: state.machineStatus, + machineStatusName: state.machineStatusName, + machineError: state.machineError, + error: state.error, + isPairingError: state.isPairingError, + isCommunicating: state.isCommunicating, + disconnect: state.disconnect, + })) + ); + + const { + pyodideError, + showErrorPopover, + setErrorPopover, + } = 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 = { + ready: CheckCircleIcon, + active: BoltIcon, + waiting: PauseCircleIcon, + complete: CheckCircleIcon, + interrupted: PauseCircleIcon, + error: ExclamationTriangleIcon, + }; + 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 ( +
+
+ {/* Machine Connection Status - Responsive width column */} +
+
+
+
+
+

Respira

+ {isConnected && machineInfo?.serialNumber && ( + + • {machineInfo.serialNumber} + + )} + {isPolling && ( + + )} +
+
+ {isConnected ? ( + <> + + + + {machineStatusName} + + + ) : ( +

Not Connected

+ )} + + {/* Error indicator - always render to prevent layout shift */} +
+ + + {/* Error popover */} + {showErrorPopover && (machineErrorMessage || pyodideError) && ( + + )} +
+
+
+
+ + {/* Workflow Stepper - Flexible width column */} +
+ +
+
+
+ ); +} diff --git a/src/components/ConnectionPrompt.tsx b/src/components/ConnectionPrompt.tsx new file mode 100644 index 0000000..118113f --- /dev/null +++ b/src/components/ConnectionPrompt.tsx @@ -0,0 +1,67 @@ +import { useShallow } from 'zustand/react/shallow'; +import { useMachineStore } from '../stores/useMachineStore'; +import { isBluetoothSupported } from '../utils/bluetoothSupport'; +import { ExclamationTriangleIcon } from '@heroicons/react/24/solid'; + +export function ConnectionPrompt() { + const { connect } = useMachineStore( + useShallow((state) => ({ + connect: state.connect, + })) + ); + + if (isBluetoothSupported()) { + return ( +
+
+
+ + + +
+
+

Get Started

+

Connect to your embroidery machine

+
+
+ +
+ ); + } + + return ( +
+
+ +
+

Browser Not Supported

+

+ Your browser doesn't support Web Bluetooth, which is required to connect to your embroidery machine. +

+
+

Please try one of these options:

+
    +
  • Use a supported browser (Chrome, Edge, or Opera)
  • +
  • + Download the Desktop app from{' '} + + GitHub Releases + +
  • +
+
+
+
+
+ ); +} diff --git a/src/components/ErrorPopover.tsx b/src/components/ErrorPopover.tsx new file mode 100644 index 0000000..6f09705 --- /dev/null +++ b/src/components/ErrorPopover.tsx @@ -0,0 +1,84 @@ +import { forwardRef } from 'react'; +import { ExclamationTriangleIcon, InformationCircleIcon } from '@heroicons/react/24/solid'; +import { getErrorDetails } from '../utils/errorCodeHelpers'; + +interface ErrorPopoverProps { + 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; + + 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 || (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')} +

+ )} +
+
+
+
+ ); + } +); + +ErrorPopover.displayName = 'ErrorPopover'; diff --git a/src/components/LeftSidebar.tsx b/src/components/LeftSidebar.tsx new file mode 100644 index 0000000..15f93ac --- /dev/null +++ b/src/components/LeftSidebar.tsx @@ -0,0 +1,42 @@ +import { useShallow } from 'zustand/react/shallow'; +import { useMachineStore } from '../stores/useMachineStore'; +import { usePatternStore } from '../stores/usePatternStore'; +import { ConnectionPrompt } from './ConnectionPrompt'; +import { FileUpload } from './FileUpload'; +import { PatternSummaryCard } from './PatternSummaryCard'; +import { ProgressMonitor } from './ProgressMonitor'; + +export function LeftSidebar() { + const { isConnected } = useMachineStore( + useShallow((state) => ({ + isConnected: state.isConnected, + })) + ); + + const { pesData, patternUploaded } = usePatternStore( + useShallow((state) => ({ + pesData: state.pesData, + patternUploaded: state.patternUploaded, + })) + ); + + return ( +
+ {/* Connect Button or Browser Hint - Show when disconnected */} + {!isConnected && } + + {/* Pattern File - Show during upload stage (before pattern is uploaded) */} + {isConnected && !patternUploaded && } + + {/* Compact Pattern Summary - Show after upload (during sewing stages) */} + {isConnected && patternUploaded && pesData && } + + {/* Progress Monitor - Show when pattern is uploaded */} + {isConnected && patternUploaded && ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/PatternPreviewPlaceholder.tsx b/src/components/PatternPreviewPlaceholder.tsx new file mode 100644 index 0000000..2cebbd4 --- /dev/null +++ b/src/components/PatternPreviewPlaceholder.tsx @@ -0,0 +1,46 @@ +export function PatternPreviewPlaceholder() { + return ( +
+

Pattern Preview

+
+ {/* Decorative background pattern */} +
+
+
+
+
+ +
+
+ + + +
+ + + +
+
+

No Pattern Loaded

+

+ Connect to your machine and choose a PES embroidery file to see your design preview +

+
+
+
+ Drag to Position +
+
+
+ Zoom & Pan +
+
+
+ Real-time Preview +
+
+
+
+
+ ); +}