mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
refactor: Extract WorkflowStepper sub-components and utilities
- Move WorkflowStepper into dedicated folder with sub-components - Extract StepCircle, StepLabel, and StepPopover components - Create workflowSteps constant for step definitions - Extract getCurrentStep logic to workflowStepCalculation utility - Extract getGuideContent logic to workflowGuideContent utility - Reduce WorkflowStepper.tsx from 487 to 140 lines (71% reduction) Part of #33: Extract sub-components from large components 🤖 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
ec19426dd1
commit
2de8cd12ff
9 changed files with 623 additions and 487 deletions
|
|
@ -1,487 +0,0 @@
|
||||||
import { useState, useRef } from "react";
|
|
||||||
import { useClickOutside } from "@/hooks";
|
|
||||||
import { useShallow } from "zustand/react/shallow";
|
|
||||||
import { useMachineStore, usePatternUploaded } from "../stores/useMachineStore";
|
|
||||||
import { usePatternStore } from "../stores/usePatternStore";
|
|
||||||
import {
|
|
||||||
CheckCircleIcon,
|
|
||||||
InformationCircleIcon,
|
|
||||||
ExclamationTriangleIcon,
|
|
||||||
} from "@heroicons/react/24/solid";
|
|
||||||
import { MachineStatus } from "../types/machine";
|
|
||||||
|
|
||||||
interface Step {
|
|
||||||
id: number;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const steps: Step[] = [
|
|
||||||
{ id: 1, label: "Connect", description: "Connect to machine" },
|
|
||||||
{ id: 2, label: "Home Machine", description: "Initialize hoop position" },
|
|
||||||
{ id: 3, label: "Load Pattern", description: "Choose PES file" },
|
|
||||||
{ id: 4, label: "Upload", description: "Upload to machine" },
|
|
||||||
{ id: 5, label: "Mask Trace", description: "Trace pattern area" },
|
|
||||||
{ id: 6, label: "Start Sewing", description: "Begin embroidery" },
|
|
||||||
{ id: 7, label: "Monitor", description: "Watch progress" },
|
|
||||||
{ id: 8, label: "Complete", description: "Finish and remove" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Helper function to get guide content for a step
|
|
||||||
function getGuideContent(stepId: number, machineStatus: MachineStatus) {
|
|
||||||
// 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: Home Machine",
|
|
||||||
description:
|
|
||||||
"The hoop needs to be removed and an initial homing procedure must be performed.",
|
|
||||||
items: [
|
|
||||||
"Remove the embroidery hoop from the machine completely",
|
|
||||||
"Press the Accept button on the machine",
|
|
||||||
"Wait for the machine to complete its initialization (homing)",
|
|
||||||
"Once initialization is complete, reattach the hoop",
|
|
||||||
"The machine should now recognize the hoop correctly",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
return {
|
|
||||||
type: "info" as const,
|
|
||||||
title: "Step 3: 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 4:
|
|
||||||
return {
|
|
||||||
type: "info" as const,
|
|
||||||
title: "Step 4: 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 5:
|
|
||||||
// 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 5: 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 6:
|
|
||||||
return {
|
|
||||||
type: "success" as const,
|
|
||||||
title: "Step 6: 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 7:
|
|
||||||
// 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 7: 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 8:
|
|
||||||
return {
|
|
||||||
type: "success" as const,
|
|
||||||
title: "Step 8: 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;
|
|
||||||
|
|
||||||
// Check if machine needs homing (Initial state)
|
|
||||||
if (machineStatus === MachineStatus.Initial) return 2;
|
|
||||||
|
|
||||||
if (!hasPattern) return 3;
|
|
||||||
if (!patternUploaded) return 4;
|
|
||||||
|
|
||||||
// After upload, determine step based on machine status
|
|
||||||
switch (machineStatus) {
|
|
||||||
case MachineStatus.IDLE:
|
|
||||||
case MachineStatus.MASK_TRACE_LOCK_WAIT:
|
|
||||||
case MachineStatus.MASK_TRACING:
|
|
||||||
return 5;
|
|
||||||
|
|
||||||
case MachineStatus.MASK_TRACE_COMPLETE:
|
|
||||||
case MachineStatus.SEWING_WAIT:
|
|
||||||
return 6;
|
|
||||||
|
|
||||||
case MachineStatus.SEWING:
|
|
||||||
case MachineStatus.COLOR_CHANGE_WAIT:
|
|
||||||
case MachineStatus.PAUSE:
|
|
||||||
case MachineStatus.STOP:
|
|
||||||
case MachineStatus.SEWING_INTERRUPTION:
|
|
||||||
return 7;
|
|
||||||
|
|
||||||
case MachineStatus.SEWING_COMPLETE:
|
|
||||||
return 8;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WorkflowStepper() {
|
|
||||||
// Machine store
|
|
||||||
const { machineStatus, isConnected } = useMachineStore(
|
|
||||||
useShallow((state) => ({
|
|
||||||
machineStatus: state.machineStatus,
|
|
||||||
isConnected: state.isConnected,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pattern store
|
|
||||||
const { pesData } = usePatternStore(
|
|
||||||
useShallow((state) => ({
|
|
||||||
pesData: state.pesData,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Derived state: pattern is uploaded if machine has pattern info
|
|
||||||
const patternUploaded = usePatternUploaded();
|
|
||||||
const hasPattern = pesData !== null;
|
|
||||||
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 (exclude step circles)
|
|
||||||
useClickOutside<HTMLDivElement>(popoverRef, () => setShowPopover(false), {
|
|
||||||
enabled: showPopover,
|
|
||||||
excludeRefs: [stepRefs],
|
|
||||||
});
|
|
||||||
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
{/* Progress bar background */}
|
|
||||||
<div
|
|
||||||
className="absolute top-4 lg:top-5 left-0 right-0 h-0.5 lg:h-1 bg-primary-400/20 dark:bg-primary-600/20 rounded-full"
|
|
||||||
style={{ left: "16px", right: "16px" }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Progress bar fill */}
|
|
||||||
<div
|
|
||||||
className="absolute top-4 lg:top-5 left-0 h-0.5 lg:h-1 bg-gradient-to-r from-success-500 to-primary-500 dark:from-success-600 dark:to-primary-600 transition-all duration-500 rounded-full"
|
|
||||||
style={{
|
|
||||||
left: "16px",
|
|
||||||
width: `calc(${((currentStep - 1) / (steps.length - 1)) * 100}% - 16px)`,
|
|
||||||
}}
|
|
||||||
role="progressbar"
|
|
||||||
aria-valuenow={currentStep}
|
|
||||||
aria-valuemin={1}
|
|
||||||
aria-valuemax={steps.length}
|
|
||||||
aria-label={`Step ${currentStep} of ${steps.length}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Steps */}
|
|
||||||
<div className="flex justify-between relative">
|
|
||||||
{steps.map((step) => {
|
|
||||||
const isComplete = step.id < currentStep;
|
|
||||||
const isCurrent = step.id === currentStep;
|
|
||||||
const isUpcoming = step.id > currentStep;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={step.id}
|
|
||||||
className="flex flex-col items-center"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
role="listitem"
|
|
||||||
aria-current={isCurrent ? "step" : undefined}
|
|
||||||
>
|
|
||||||
{/* 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-success-500 dark:bg-success-600 border-success-400 dark:border-success-500 text-white shadow-success-500/30 dark:shadow-success-600/30" : ""}
|
|
||||||
${isCurrent ? "bg-primary-600 dark:bg-primary-700 border-primary-500 dark:border-primary-600 text-white scale-105 lg:scale-110 shadow-primary-600/40 dark:shadow-primary-700/40 ring-2 ring-primary-300 dark:ring-primary-500 ring-offset-2 dark:ring-offset-gray-900" : ""}
|
|
||||||
${isUpcoming ? "bg-primary-700 dark:bg-primary-800 border-primary-500/30 dark:border-primary-600/30 text-primary-200/70 dark:text-primary-300/70" : ""}
|
|
||||||
${showPopover && popoverStep === step.id ? "ring-4 ring-white dark:ring-gray-800" : ""}
|
|
||||||
`}
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
step.id
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Step label */}
|
|
||||||
<div className="mt-1 lg:mt-2 text-center">
|
|
||||||
<div
|
|
||||||
className={`text-xs font-semibold leading-tight ${
|
|
||||||
isCurrent
|
|
||||||
? "text-white"
|
|
||||||
: isComplete
|
|
||||||
? "text-success-200 dark:text-success-300"
|
|
||||||
: "text-primary-300/70 dark:text-primary-400/70"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{step.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Popover */}
|
|
||||||
{showPopover && popoverStep !== null && (
|
|
||||||
<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);
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
54
src/components/WorkflowStepper/StepCircle.tsx
Normal file
54
src/components/WorkflowStepper/StepCircle.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* StepCircle Component
|
||||||
|
*
|
||||||
|
* Renders a circular step indicator with number or checkmark icon
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
|
export interface StepCircleProps {
|
||||||
|
stepId: number;
|
||||||
|
label: string;
|
||||||
|
isComplete: boolean;
|
||||||
|
isCurrent: boolean;
|
||||||
|
isUpcoming: boolean;
|
||||||
|
showPopover: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StepCircle = forwardRef<HTMLDivElement, StepCircleProps>(
|
||||||
|
(
|
||||||
|
{ stepId, label, isComplete, isCurrent, isUpcoming, showPopover, onClick },
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
onClick={onClick}
|
||||||
|
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
|
||||||
|
${stepId <= (isCurrent ? stepId : isComplete ? stepId : stepId - 1) ? "cursor-pointer hover:scale-110" : "cursor-not-allowed"}
|
||||||
|
${isComplete ? "bg-success-500 dark:bg-success-600 border-success-400 dark:border-success-500 text-white shadow-success-500/30 dark:shadow-success-600/30" : ""}
|
||||||
|
${isCurrent ? "bg-primary-600 dark:bg-primary-700 border-primary-500 dark:border-primary-600 text-white scale-105 lg:scale-110 shadow-primary-600/40 dark:shadow-primary-700/40 ring-2 ring-primary-300 dark:ring-primary-500 ring-offset-2 dark:ring-offset-gray-900" : ""}
|
||||||
|
${isUpcoming ? "bg-primary-700 dark:bg-primary-800 border-primary-500/30 dark:border-primary-600/30 text-primary-200/70 dark:text-primary-300/70" : ""}
|
||||||
|
${showPopover ? "ring-4 ring-white dark:ring-gray-800" : ""}
|
||||||
|
`}
|
||||||
|
aria-label={`${label}: ${isComplete ? "completed" : isCurrent ? "current" : "upcoming"}. Click for details.`}
|
||||||
|
role="button"
|
||||||
|
tabIndex={isComplete || isCurrent ? 0 : -1}
|
||||||
|
>
|
||||||
|
{isComplete ? (
|
||||||
|
<CheckCircleIcon
|
||||||
|
className="w-5 h-5 lg:w-6 lg:h-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
stepId
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
StepCircle.displayName = "StepCircle";
|
||||||
29
src/components/WorkflowStepper/StepLabel.tsx
Normal file
29
src/components/WorkflowStepper/StepLabel.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* StepLabel Component
|
||||||
|
*
|
||||||
|
* Renders the text label below each step circle
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface StepLabelProps {
|
||||||
|
label: string;
|
||||||
|
isCurrent: boolean;
|
||||||
|
isComplete: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StepLabel({ label, isCurrent, isComplete }: StepLabelProps) {
|
||||||
|
return (
|
||||||
|
<div className="mt-1 lg:mt-2 text-center">
|
||||||
|
<div
|
||||||
|
className={`text-xs font-semibold leading-tight ${
|
||||||
|
isCurrent
|
||||||
|
? "text-white"
|
||||||
|
: isComplete
|
||||||
|
? "text-success-200 dark:text-success-300"
|
||||||
|
: "text-primary-300/70 dark:text-primary-400/70"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
123
src/components/WorkflowStepper/StepPopover.tsx
Normal file
123
src/components/WorkflowStepper/StepPopover.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
/**
|
||||||
|
* StepPopover Component
|
||||||
|
*
|
||||||
|
* Renders the guidance popover with dynamic content based on step and machine status
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
import {
|
||||||
|
InformationCircleIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import { MachineStatus } from "../../types/machine";
|
||||||
|
import { getGuideContent } from "../../utils/workflowGuideContent";
|
||||||
|
|
||||||
|
export interface StepPopoverProps {
|
||||||
|
stepId: number;
|
||||||
|
machineStatus: MachineStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StepPopover = forwardRef<HTMLDivElement, StepPopoverProps>(
|
||||||
|
({ stepId, machineStatus }, ref) => {
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
StepPopover.displayName = "StepPopover";
|
||||||
141
src/components/WorkflowStepper/WorkflowStepper.tsx
Normal file
141
src/components/WorkflowStepper/WorkflowStepper.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
/**
|
||||||
|
* WorkflowStepper Component
|
||||||
|
*
|
||||||
|
* Displays the 8-step embroidery workflow with progress tracking and contextual guidance
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useRef } from "react";
|
||||||
|
import { useClickOutside } from "@/hooks";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import { useMachineStore, usePatternUploaded } from "../../stores/useMachineStore";
|
||||||
|
import { usePatternStore } from "../../stores/usePatternStore";
|
||||||
|
import { WORKFLOW_STEPS } from "../../constants/workflowSteps";
|
||||||
|
import { getCurrentStep } from "../../utils/workflowStepCalculation";
|
||||||
|
import { StepCircle } from "./StepCircle";
|
||||||
|
import { StepLabel } from "./StepLabel";
|
||||||
|
import { StepPopover } from "./StepPopover";
|
||||||
|
|
||||||
|
export function WorkflowStepper() {
|
||||||
|
// Machine store
|
||||||
|
const { machineStatus, isConnected } = useMachineStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
machineStatus: state.machineStatus,
|
||||||
|
isConnected: state.isConnected,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pattern store
|
||||||
|
const { pesData } = usePatternStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
pesData: state.pesData,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Derived state: pattern is uploaded if machine has pattern info
|
||||||
|
const patternUploaded = usePatternUploaded();
|
||||||
|
const hasPattern = pesData !== null;
|
||||||
|
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 (exclude step circles)
|
||||||
|
useClickOutside<HTMLDivElement>(popoverRef, () => setShowPopover(false), {
|
||||||
|
enabled: showPopover,
|
||||||
|
excludeRefs: [stepRefs],
|
||||||
|
});
|
||||||
|
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{/* Progress bar background */}
|
||||||
|
<div
|
||||||
|
className="absolute top-4 lg:top-5 left-0 right-0 h-0.5 lg:h-1 bg-primary-400/20 dark:bg-primary-600/20 rounded-full"
|
||||||
|
style={{ left: "16px", right: "16px" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Progress bar fill */}
|
||||||
|
<div
|
||||||
|
className="absolute top-4 lg:top-5 left-0 h-0.5 lg:h-1 bg-gradient-to-r from-success-500 to-primary-500 dark:from-success-600 dark:to-primary-600 transition-all duration-500 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: "16px",
|
||||||
|
width: `calc(${((currentStep - 1) / (WORKFLOW_STEPS.length - 1)) * 100}% - 16px)`,
|
||||||
|
}}
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={currentStep}
|
||||||
|
aria-valuemin={1}
|
||||||
|
aria-valuemax={WORKFLOW_STEPS.length}
|
||||||
|
aria-label={`Step ${currentStep} of ${WORKFLOW_STEPS.length}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Steps */}
|
||||||
|
<div className="flex justify-between relative">
|
||||||
|
{WORKFLOW_STEPS.map((step) => {
|
||||||
|
const isComplete = step.id < currentStep;
|
||||||
|
const isCurrent = step.id === currentStep;
|
||||||
|
const isUpcoming = step.id > currentStep;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={step.id}
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
role="listitem"
|
||||||
|
aria-current={isCurrent ? "step" : undefined}
|
||||||
|
>
|
||||||
|
<StepCircle
|
||||||
|
ref={(el: HTMLDivElement | null) => {
|
||||||
|
stepRefs.current[step.id] = el;
|
||||||
|
}}
|
||||||
|
stepId={step.id}
|
||||||
|
label={step.label}
|
||||||
|
isComplete={isComplete}
|
||||||
|
isCurrent={isCurrent}
|
||||||
|
isUpcoming={isUpcoming}
|
||||||
|
showPopover={showPopover && popoverStep === step.id}
|
||||||
|
onClick={() => handleStepClick(step.id)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StepLabel
|
||||||
|
label={step.label}
|
||||||
|
isCurrent={isCurrent}
|
||||||
|
isComplete={isComplete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Popover */}
|
||||||
|
{showPopover && popoverStep !== null && (
|
||||||
|
<StepPopover
|
||||||
|
ref={popoverRef}
|
||||||
|
stepId={popoverStep}
|
||||||
|
machineStatus={machineStatus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/components/WorkflowStepper/index.ts
Normal file
5
src/components/WorkflowStepper/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/**
|
||||||
|
* WorkflowStepper component barrel export
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { WorkflowStepper } from "./WorkflowStepper";
|
||||||
20
src/constants/workflowSteps.ts
Normal file
20
src/constants/workflowSteps.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Workflow step definitions for the embroidery process
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface WorkflowStep {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WORKFLOW_STEPS: readonly WorkflowStep[] = [
|
||||||
|
{ id: 1, label: "Connect", description: "Connect to machine" },
|
||||||
|
{ id: 2, label: "Home Machine", description: "Initialize hoop position" },
|
||||||
|
{ id: 3, label: "Load Pattern", description: "Choose PES file" },
|
||||||
|
{ id: 4, label: "Upload", description: "Upload to machine" },
|
||||||
|
{ id: 5, label: "Mask Trace", description: "Trace pattern area" },
|
||||||
|
{ id: 6, label: "Start Sewing", description: "Begin embroidery" },
|
||||||
|
{ id: 7, label: "Monitor", description: "Watch progress" },
|
||||||
|
{ id: 8, label: "Complete", description: "Finish and remove" },
|
||||||
|
] as const;
|
||||||
195
src/utils/workflowGuideContent.ts
Normal file
195
src/utils/workflowGuideContent.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
/**
|
||||||
|
* Workflow step guide content
|
||||||
|
*
|
||||||
|
* Provides contextual guidance for each workflow step based on machine state
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MachineStatus } from "../types/machine";
|
||||||
|
|
||||||
|
export interface GuideContent {
|
||||||
|
type: "info" | "warning" | "success" | "error" | "progress";
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
items: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get guide content for a specific workflow step
|
||||||
|
*
|
||||||
|
* @param stepId - The workflow step ID (1-8)
|
||||||
|
* @param machineStatus - Current machine status for dynamic content
|
||||||
|
* @returns Guide content with type, title, description, and items
|
||||||
|
*/
|
||||||
|
export function getGuideContent(
|
||||||
|
stepId: number,
|
||||||
|
machineStatus: MachineStatus,
|
||||||
|
): GuideContent | null {
|
||||||
|
switch (stepId) {
|
||||||
|
case 1:
|
||||||
|
return {
|
||||||
|
type: "info",
|
||||||
|
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",
|
||||||
|
title: "Step 2: Home Machine",
|
||||||
|
description:
|
||||||
|
"The hoop needs to be removed and an initial homing procedure must be performed.",
|
||||||
|
items: [
|
||||||
|
"Remove the embroidery hoop from the machine completely",
|
||||||
|
"Press the Accept button on the machine",
|
||||||
|
"Wait for the machine to complete its initialization (homing)",
|
||||||
|
"Once initialization is complete, reattach the hoop",
|
||||||
|
"The machine should now recognize the hoop correctly",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return {
|
||||||
|
type: "info",
|
||||||
|
title: "Step 3: 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 4:
|
||||||
|
return {
|
||||||
|
type: "info",
|
||||||
|
title: "Step 4: 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 5:
|
||||||
|
// Check machine status for substates
|
||||||
|
if (machineStatus === MachineStatus.MASK_TRACE_LOCK_WAIT) {
|
||||||
|
return {
|
||||||
|
type: "warning",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
title: "Step 5: 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 6:
|
||||||
|
return {
|
||||||
|
type: "success",
|
||||||
|
title: "Step 6: 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 7:
|
||||||
|
// Check for substates
|
||||||
|
if (machineStatus === MachineStatus.COLOR_CHANGE_WAIT) {
|
||||||
|
return {
|
||||||
|
type: "warning",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
title: "Step 7: 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 8:
|
||||||
|
return {
|
||||||
|
type: "success",
|
||||||
|
title: "Step 8: 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/utils/workflowStepCalculation.ts
Normal file
56
src/utils/workflowStepCalculation.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* Workflow step calculation utilities
|
||||||
|
*
|
||||||
|
* Determines the current workflow step based on machine state and pattern status
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MachineStatus } from "../types/machine";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the current workflow step based on machine state
|
||||||
|
*
|
||||||
|
* @param machineStatus - Current machine status
|
||||||
|
* @param isConnected - Whether machine is connected
|
||||||
|
* @param hasPattern - Whether a pattern is loaded
|
||||||
|
* @param patternUploaded - Whether pattern has been uploaded to machine
|
||||||
|
* @returns Current step number (1-8)
|
||||||
|
*/
|
||||||
|
export function getCurrentStep(
|
||||||
|
machineStatus: MachineStatus,
|
||||||
|
isConnected: boolean,
|
||||||
|
hasPattern: boolean,
|
||||||
|
patternUploaded: boolean,
|
||||||
|
): number {
|
||||||
|
if (!isConnected) return 1;
|
||||||
|
|
||||||
|
// Check if machine needs homing (Initial state)
|
||||||
|
if (machineStatus === MachineStatus.Initial) return 2;
|
||||||
|
|
||||||
|
if (!hasPattern) return 3;
|
||||||
|
if (!patternUploaded) return 4;
|
||||||
|
|
||||||
|
// After upload, determine step based on machine status
|
||||||
|
switch (machineStatus) {
|
||||||
|
case MachineStatus.IDLE:
|
||||||
|
case MachineStatus.MASK_TRACE_LOCK_WAIT:
|
||||||
|
case MachineStatus.MASK_TRACING:
|
||||||
|
return 5;
|
||||||
|
|
||||||
|
case MachineStatus.MASK_TRACE_COMPLETE:
|
||||||
|
case MachineStatus.SEWING_WAIT:
|
||||||
|
return 6;
|
||||||
|
|
||||||
|
case MachineStatus.SEWING:
|
||||||
|
case MachineStatus.COLOR_CHANGE_WAIT:
|
||||||
|
case MachineStatus.PAUSE:
|
||||||
|
case MachineStatus.STOP:
|
||||||
|
case MachineStatus.SEWING_INTERRUPTION:
|
||||||
|
return 7;
|
||||||
|
|
||||||
|
case MachineStatus.SEWING_COMPLETE:
|
||||||
|
return 8;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue