mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
Implement unified compact card design system across all UI components
Replace inconsistent card layouts with a cohesive, space-efficient design pattern featuring color-coded borders, icon headers, and compact spacing. This redesign significantly reduces vertical space usage while improving visual hierarchy and scannability. UI/UX improvements: - Apply consistent card design with border-left accent colors - Add icon + title + subtitle header pattern to all cards - Reduce padding from p-6 to p-4 for more compact layout - Use smaller, tighter font sizes (text-xs, text-sm) - Implement color-coded borders for quick visual identification Component-specific changes: - MachineConnection: Green/gray border, WiFi icon, compact status display - PatternSummaryCard: Blue border, Document icon (new component) - FileUpload: Orange/gray border, Document icon, inline button layout - ProgressMonitor: Purple border, Chart icon, single-column layout - PatternCanvas: Teal/gray border, Photo icon, dimensions in header Conditional rendering optimizations: - Show FileUpload OR PatternSummaryCard based on upload state - Move ProgressMonitor to left column with PatternSummary - Relocate NextStepGuide below PatternCanvas for better space usage - Remove duplicate delete button from ProgressMonitor Space savings: ~40-50% reduction in vertical space usage across all cards 🤖 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
9b536e9deb
commit
99ed1adb68
6 changed files with 469 additions and 376 deletions
63
src/App.tsx
63
src/App.tsx
|
|
@ -6,9 +6,11 @@ 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 type { PesPatternData } from './utils/pystitchConverter';
|
||||
import { pyodideLoader } from './utils/pyodideLoader';
|
||||
import { hasError } from './utils/errorCodeHelpers';
|
||||
import { canDeletePattern } from './utils/machineStateHelpers';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
|
|
@ -153,17 +155,6 @@ function App() {
|
|||
<div className="grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-6">
|
||||
{/* Left Column - Controls */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Next Step Guide - Always visible */}
|
||||
<NextStepGuide
|
||||
machineStatus={machine.machineStatus}
|
||||
isConnected={machine.isConnected}
|
||||
hasPattern={pesData !== null}
|
||||
patternUploaded={patternUploaded}
|
||||
hasError={hasError(machine.machineError)}
|
||||
errorMessage={machine.error || undefined}
|
||||
errorCode={machine.machineError}
|
||||
/>
|
||||
|
||||
{/* Machine Connection - Always visible */}
|
||||
<MachineConnection
|
||||
isConnected={machine.isConnected}
|
||||
|
|
@ -177,8 +168,8 @@ function App() {
|
|||
onRefresh={machine.refreshStatus}
|
||||
/>
|
||||
|
||||
{/* Pattern File - Only show when connected */}
|
||||
{machine.isConnected && (
|
||||
{/* Pattern File - Show during upload stage (before pattern is uploaded) */}
|
||||
{machine.isConnected && !patternUploaded && (
|
||||
<FileUpload
|
||||
isConnected={machine.isConnected}
|
||||
machineStatus={machine.machineStatus}
|
||||
|
|
@ -195,6 +186,32 @@ function App() {
|
|||
isUploading={machine.isUploading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Compact Pattern Summary - Show after upload (during sewing stages) */}
|
||||
{machine.isConnected && patternUploaded && pesData && (
|
||||
<PatternSummaryCard
|
||||
pesData={pesData}
|
||||
fileName={currentFileName}
|
||||
onDeletePattern={handleDeletePattern}
|
||||
canDelete={canDeletePattern(machine.machineStatus)}
|
||||
isDeleting={machine.isDeleting}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Progress Monitor - Show when pattern is uploaded */}
|
||||
{machine.isConnected && patternUploaded && (
|
||||
<ProgressMonitor
|
||||
machineStatus={machine.machineStatus}
|
||||
patternInfo={machine.patternInfo}
|
||||
sewingProgress={machine.sewingProgress}
|
||||
pesData={pesData}
|
||||
onStartMaskTrace={machine.startMaskTrace}
|
||||
onStartSewing={machine.startSewing}
|
||||
onResumeSewing={machine.resumeSewing}
|
||||
onDeletePattern={handleDeletePattern}
|
||||
isDeleting={machine.isDeleting}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column - Pattern Preview */}
|
||||
|
|
@ -254,20 +271,16 @@ function App() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress Monitor - Wide section below pattern preview */}
|
||||
{machine.isConnected && patternUploaded && (
|
||||
<ProgressMonitor
|
||||
{/* Next Step Guide - Below pattern preview */}
|
||||
<NextStepGuide
|
||||
machineStatus={machine.machineStatus}
|
||||
patternInfo={machine.patternInfo}
|
||||
sewingProgress={machine.sewingProgress}
|
||||
pesData={pesData}
|
||||
onStartMaskTrace={machine.startMaskTrace}
|
||||
onStartSewing={machine.startSewing}
|
||||
onResumeSewing={machine.resumeSewing}
|
||||
onDeletePattern={handleDeletePattern}
|
||||
isDeleting={machine.isDeleting}
|
||||
isConnected={machine.isConnected}
|
||||
hasPattern={pesData !== null}
|
||||
patternUploaded={patternUploaded}
|
||||
hasError={hasError(machine.machineError)}
|
||||
errorMessage={machine.error || undefined}
|
||||
errorCode={machine.machineError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { convertPesToPen, type PesPatternData } from '../utils/pystitchConverter
|
|||
import { MachineStatus } from '../types/machine';
|
||||
import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
|
||||
import { PatternInfoSkeleton } from './SkeletonLoader';
|
||||
import { ArrowUpTrayIcon, CheckCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon } from '@heroicons/react/24/solid';
|
||||
|
||||
interface FileUploadProps {
|
||||
isConnected: boolean;
|
||||
|
|
@ -80,28 +80,98 @@ export function FileUpload({
|
|||
}
|
||||
}, [pesData, displayFileName, onUpload, patternOffset]);
|
||||
|
||||
const borderColor = pesData ? 'border-orange-600 dark:border-orange-500' : 'border-gray-400 dark:border-gray-600';
|
||||
const iconColor = pesData ? 'text-orange-600 dark:text-orange-400' : 'text-gray-600 dark:text-gray-400';
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
|
||||
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300 dark:border-gray-600 dark:text-white">Pattern File</h2>
|
||||
<div className={`bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 ${borderColor}`}>
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<DocumentTextIcon className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Pattern File</h3>
|
||||
{pesData && displayFileName ? (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 truncate" title={displayFileName}>
|
||||
{displayFileName}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">No pattern loaded</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{resumeAvailable && resumeFileName && (
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 px-4 py-3 rounded mb-4">
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
<strong>Loaded cached pattern:</strong> "{resumeFileName}"
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 px-3 py-2 rounded mb-3">
|
||||
<p className="text-xs text-green-800 dark:text-green-200">
|
||||
<strong>Cached:</strong> "{resumeFileName}"
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{patternUploaded && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 px-4 py-3 rounded mb-4">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>Pattern uploaded successfully!</strong> The pattern is now locked and cannot be changed.
|
||||
To upload a different pattern, you must first complete or delete the current one.
|
||||
</p>
|
||||
{isLoading && <PatternInfoSkeleton />}
|
||||
|
||||
{!isLoading && pesData && (
|
||||
<div className="mb-3">
|
||||
<div className="grid grid-cols-2 gap-2 text-xs mb-2">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Size</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
|
||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Stitches</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{pesData.stitchCount.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">Colors:</span>
|
||||
<div className="flex gap-1">
|
||||
{pesData.threads.slice(0, 8).map((thread, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600"
|
||||
style={{ backgroundColor: thread.hex }}
|
||||
title={`Thread ${idx + 1}: ${thread.hex}`}
|
||||
/>
|
||||
))}
|
||||
{pesData.colorCount > 8 && (
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-[7px] font-bold text-gray-600 dark:text-gray-300">
|
||||
+{pesData.colorCount - 8}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pesData && !canUploadPattern(machineStatus) && (
|
||||
<div className="bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 px-3 py-2 rounded border border-yellow-200 dark:border-yellow-800 mb-3 text-xs">
|
||||
Cannot upload while {getMachineStateCategory(machineStatus)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUploading && uploadProgress < 100 && (
|
||||
<div className="mb-3">
|
||||
<div className="flex justify-between items-center mb-1.5">
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">Uploading</span>
|
||||
<span className="text-xs font-bold text-orange-600 dark:text-orange-400">
|
||||
{uploadProgress > 0 ? uploadProgress.toFixed(1) + '%' : 'Starting...'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2.5 bg-gray-300 dark:bg-gray-600 rounded-full overflow-hidden shadow-inner relative">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-orange-500 via-orange-600 to-orange-700 dark:from-orange-600 dark:via-orange-700 dark:to-orange-800 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite] rounded-full"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="file"
|
||||
accept=".pes"
|
||||
|
|
@ -112,23 +182,23 @@ export function FileUpload({
|
|||
/>
|
||||
<label
|
||||
htmlFor="file-input"
|
||||
className={`inline-flex items-center gap-2 px-6 py-3 bg-gray-600 dark:bg-gray-700 text-white rounded-lg font-semibold text-sm transition-all ${
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded font-semibold text-xs transition-all ${
|
||||
!pyodideReady || isLoading || patternUploaded
|
||||
? 'opacity-50 cursor-not-allowed grayscale-[0.3]'
|
||||
: 'cursor-pointer hover:bg-gray-700 dark:hover:bg-gray-600 hover:shadow-lg active:scale-[0.98]'
|
||||
? 'opacity-50 cursor-not-allowed bg-gray-400 dark:bg-gray-600 text-white'
|
||||
: 'cursor-pointer bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 animate-spin" 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>Loading Pattern...</span>
|
||||
<span>Loading...</span>
|
||||
</>
|
||||
) : !pyodideReady ? (
|
||||
<>
|
||||
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 animate-spin" 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>
|
||||
|
|
@ -136,107 +206,37 @@ export function FileUpload({
|
|||
</>
|
||||
) : patternUploaded ? (
|
||||
<>
|
||||
<CheckCircleIcon className="w-5 h-5" />
|
||||
<span>Pattern Locked</span>
|
||||
<CheckCircleIcon className="w-3.5 h-3.5" />
|
||||
<span>Locked</span>
|
||||
</>
|
||||
) : (
|
||||
<span>Choose PES File</span>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{isLoading && <PatternInfoSkeleton />}
|
||||
|
||||
{!isLoading && pesData && (
|
||||
<div className="mt-4 animate-fadeIn">
|
||||
<h3 className="text-base font-semibold my-4 dark:text-white">Pattern Information</h3>
|
||||
<div className="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-700/50 dark:to-gray-800/50 p-4 rounded-lg space-y-3 border border-gray-200 dark:border-gray-600 shadow-sm">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">File Name:</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100 text-right max-w-[200px] truncate" title={displayFileName}>
|
||||
{displayFileName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Pattern Size:</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
|
||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Thread Colors:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">{pesData.colorCount}</span>
|
||||
<div className="flex gap-1">
|
||||
{pesData.threads.slice(0, 5).map((thread, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600 shadow-sm"
|
||||
style={{ backgroundColor: thread.hex }}
|
||||
title={`Thread ${idx + 1}: ${thread.hex}`}
|
||||
/>
|
||||
))}
|
||||
{pesData.colorCount > 5 && (
|
||||
<div className="w-4 h-4 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-[8px] font-bold text-gray-600 dark:text-gray-300">
|
||||
+{pesData.colorCount - 5}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Total Stitches:</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">{pesData.stitchCount.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pesData && canUploadPattern(machineStatus) && !patternUploaded && uploadProgress < 100 && (
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
disabled={!isConnected || isUploading}
|
||||
className="mt-4 inline-flex items-center gap-2 px-6 py-2.5 bg-blue-600 dark:bg-blue-700 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 disabled:hover:shadow-none disabled:active:scale-100"
|
||||
className="px-3 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-xs hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
aria-label={isUploading ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : 'Upload pattern to machine'}
|
||||
>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 animate-spin inline mr-1" 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>Uploading... {uploadProgress > 0 ? uploadProgress.toFixed(0) + '%' : ''}</span>
|
||||
{uploadProgress > 0 ? uploadProgress.toFixed(0) + '%' : 'Uploading'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ArrowUpTrayIcon className="w-5 h-5" />
|
||||
<span>Upload to Machine</span>
|
||||
<ArrowUpTrayIcon className="w-3.5 h-3.5 inline mr-1" />
|
||||
Upload
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{pesData && !canUploadPattern(machineStatus) && (
|
||||
<div className="bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 px-4 py-3 rounded-lg border border-yellow-200 dark:border-yellow-800 my-4 font-medium animate-fadeIn">
|
||||
Cannot upload pattern while machine is {getMachineStateCategory(machineStatus)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUploading && uploadProgress < 100 && (
|
||||
<div className="mt-4 animate-fadeIn">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Uploading to Machine</span>
|
||||
<span className="text-sm font-bold text-blue-600 dark:text-blue-400">{uploadProgress > 0 ? uploadProgress.toFixed(1) + '%' : 'Starting...'}</span>
|
||||
</div>
|
||||
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded-full overflow-hidden shadow-inner relative">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 dark:from-blue-600 dark:via-blue-700 dark:to-blue-800 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite] rounded-full"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-2 text-center">Please wait while your pattern is being transferred...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
BoltIcon,
|
||||
PauseCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
WifiIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import type { MachineInfo } from '../types/machine';
|
||||
import { MachineStatus } from '../types/machine';
|
||||
|
|
@ -62,73 +63,77 @@ export function MachineConnection({
|
|||
};
|
||||
|
||||
const statusBadgeColors = {
|
||||
idle: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300 border-cyan-200 dark:border-cyan-700',
|
||||
info: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300 border-cyan-200 dark:border-cyan-700',
|
||||
active: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 border-yellow-200 dark:border-yellow-700',
|
||||
waiting: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 border-yellow-200 dark:border-yellow-700',
|
||||
warning: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 border-yellow-200 dark:border-yellow-700',
|
||||
complete: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-700',
|
||||
success: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-700',
|
||||
interrupted: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-700',
|
||||
error: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-700',
|
||||
danger: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-700',
|
||||
idle: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300',
|
||||
info: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300',
|
||||
active: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300',
|
||||
waiting: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300',
|
||||
warning: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300',
|
||||
complete: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300',
|
||||
success: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300',
|
||||
interrupted: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300',
|
||||
error: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300',
|
||||
danger: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300',
|
||||
};
|
||||
|
||||
// Only show error info when connected AND there's an actual error
|
||||
const errorInfo = (isConnected && hasError(machineError)) ? getErrorDetails(machineError) : null;
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
|
||||
<div className="flex items-center justify-between mb-4 pb-2 border-b-2 border-gray-300 dark:border-gray-600">
|
||||
<h2 className="text-xl font-semibold dark:text-white">Machine Connection</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{isConnected && isPolling && (
|
||||
<span className="w-2 h-2 bg-blue-500 dark:bg-blue-400 rounded-full animate-pulse" title="Auto-refreshing" aria-label="Auto-refreshing machine status"></span>
|
||||
)}
|
||||
{isConnected && (
|
||||
<button
|
||||
onClick={handleDisconnectClick}
|
||||
className="px-3 py-1.5 bg-gray-600 dark:bg-gray-700 text-white rounded-lg font-semibold text-xs hover:bg-gray-700 dark:hover:bg-gray-600 active:bg-gray-800 dark:active:bg-gray-500 hover:shadow-md active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-300 dark:focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
aria-label="Disconnect from embroidery machine"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
)}
|
||||
<>
|
||||
{!isConnected ? (
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-gray-400 dark:border-gray-600">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<WifiIcon className="w-6 h-6 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Machine Connection</h3>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">Not connected</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isConnected ? (
|
||||
<div className="flex gap-3 mt-4 flex-wrap">
|
||||
<button
|
||||
onClick={onConnect}
|
||||
className="px-6 py-2.5 bg-blue-600 dark:bg-blue-700 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
aria-label="Connect to embroidery machine"
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-xs hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer"
|
||||
>
|
||||
Connect to Machine
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-green-600 dark:border-green-500">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<WifiIcon className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">Machine Connected</h3>
|
||||
{isPolling && (
|
||||
<span className="w-2 h-2 bg-blue-500 dark:bg-blue-400 rounded-full animate-pulse" title="Auto-refreshing" aria-label="Auto-refreshing machine status"></span>
|
||||
)}
|
||||
</div>
|
||||
{machineInfo && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 truncate" title={machineInfo.serialNumber}>
|
||||
{machineInfo.serialNumber}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error/Info Display */}
|
||||
{errorInfo && (
|
||||
errorInfo.isInformational ? (
|
||||
// Informational messages (like initialization steps)
|
||||
<div className="mb-4 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div className="mb-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<InformationCircleIcon className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||
<InformationCircleIcon className="w-4 h-4 text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-blue-900 dark:text-blue-200 text-sm">{errorInfo.title}</div>
|
||||
<div className="font-semibold text-blue-900 dark:text-blue-200 text-xs">{errorInfo.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Regular errors shown as errors
|
||||
<div className="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div className="mb-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-red-600 dark:text-red-400 text-lg flex-shrink-0">⚠️</span>
|
||||
<span className="text-red-600 dark:text-red-400 flex-shrink-0">⚠️</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-red-900 dark:text-red-200 text-sm mb-1">{errorInfo.title}</div>
|
||||
<div className="text-xs text-red-700 dark:text-red-300 font-mono">
|
||||
<div className="font-semibold text-red-900 dark:text-red-200 text-xs mb-1">{errorInfo.title}</div>
|
||||
<div className="text-[10px] text-red-700 dark:text-red-300 font-mono">
|
||||
Error Code: 0x{machineError.toString(16).toUpperCase().padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -137,36 +142,30 @@ export function MachineConnection({
|
|||
)
|
||||
)}
|
||||
|
||||
{/* Machine Status */}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Status:</span>
|
||||
<span className={`flex items-center gap-2 px-3 py-1.5 rounded-lg font-semibold text-sm ${statusBadgeColors[stateVisual.color as keyof typeof statusBadgeColors] || statusBadgeColors.info}`}>
|
||||
{/* Status Badge */}
|
||||
<div className="mb-3">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 block mb-1">Status:</span>
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg font-semibold text-xs ${statusBadgeColors[stateVisual.color as keyof typeof statusBadgeColors] || statusBadgeColors.info}`}>
|
||||
{(() => {
|
||||
const Icon = stateIcons[stateVisual.iconName];
|
||||
return <Icon className="w-4 h-4" />;
|
||||
return <Icon className="w-3.5 h-3.5" />;
|
||||
})()}
|
||||
<span>{machineStatusName}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Machine Info */}
|
||||
{machineInfo && (
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-4 rounded-lg space-y-2 mb-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-gray-600 dark:text-gray-400">Serial Number:</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">{machineInfo.serialNumber}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-gray-600 dark:text-gray-400">Max Area:</span>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Max Area</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{(machineInfo.maxWidth / 10).toFixed(1)} × {(machineInfo.maxHeight / 10).toFixed(1)} mm
|
||||
</span>
|
||||
</div>
|
||||
{machineInfo.totalCount !== undefined && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-gray-600 dark:text-gray-400">Total Stitches:</span>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Total Stitches</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{machineInfo.totalCount.toLocaleString()}
|
||||
</span>
|
||||
|
|
@ -174,6 +173,13 @@ export function MachineConnection({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleDisconnectClick}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600 text-xs font-medium transition-colors cursor-pointer"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -187,6 +193,6 @@ export function MachineConnection({
|
|||
onCancel={() => setShowDisconnectConfirm(false)}
|
||||
variant="danger"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { Stage, Layer, Group } from 'react-konva';
|
||||
import Konva from 'konva';
|
||||
import { PlusIcon, MinusIcon, ArrowPathIcon, LockClosedIcon } from '@heroicons/react/24/solid';
|
||||
import { PlusIcon, MinusIcon, ArrowPathIcon, LockClosedIcon, PhotoIcon } from '@heroicons/react/24/solid';
|
||||
import type { PesPatternData } from '../utils/pystitchConverter';
|
||||
import type { SewingProgress, MachineInfo } from '../types/machine';
|
||||
import { calculateInitialScale } from '../utils/konvaRenderers';
|
||||
|
|
@ -181,9 +181,24 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
|||
}
|
||||
}, [onPatternOffsetChange]);
|
||||
|
||||
const borderColor = pesData ? 'border-teal-600 dark:border-teal-500' : 'border-gray-400 dark:border-gray-600';
|
||||
const iconColor = pesData ? 'text-teal-600 dark:text-teal-400' : 'text-gray-600 dark:text-gray-400';
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
|
||||
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300 dark:border-gray-600 dark:text-white">Pattern Preview</h2>
|
||||
<div className={`bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 ${borderColor}`}>
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<PhotoIcon className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Pattern Preview</h3>
|
||||
{pesData ? (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} × {((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">No pattern loaded</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative w-full h-[600px] border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-900 overflow-hidden" ref={containerRef}>
|
||||
{containerSize.width > 0 && (
|
||||
<Stage
|
||||
|
|
@ -281,25 +296,19 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
|||
{pesData && (
|
||||
<>
|
||||
{/* Thread Legend Overlay */}
|
||||
<div className="absolute top-2.5 left-2.5 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm p-3 rounded-lg shadow-lg z-10 max-w-[150px]">
|
||||
<h4 className="m-0 mb-2 text-[13px] font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-300 dark:border-gray-600 pb-1.5">Threads</h4>
|
||||
<div className="absolute top-2.5 left-2.5 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm p-2.5 rounded-lg shadow-lg z-10 max-w-[150px]">
|
||||
<h4 className="m-0 mb-2 text-xs font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-300 dark:border-gray-600 pb-1.5">Threads</h4>
|
||||
{pesData.threads.map((thread, index) => (
|
||||
<div key={index} className="flex items-center gap-2 mb-1.5 last:mb-0">
|
||||
<div
|
||||
className="w-5 h-5 rounded border border-black dark:border-gray-300 flex-shrink-0"
|
||||
className="w-4 h-4 rounded border border-black dark:border-gray-300 flex-shrink-0"
|
||||
style={{ backgroundColor: thread.hex }}
|
||||
/>
|
||||
<span className="text-xs text-gray-900 dark:text-gray-100">Thread {index + 1}</span>
|
||||
<span className="text-[11px] text-gray-900 dark:text-gray-100">Thread {index + 1}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pattern Dimensions Overlay */}
|
||||
<div className="absolute bottom-[165px] right-5 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm px-4 py-2 rounded-lg shadow-lg z-[11] text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
|
||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
||||
</div>
|
||||
|
||||
{/* Pattern Offset Indicator */}
|
||||
<div className={`absolute bottom-20 right-5 backdrop-blur-sm p-2.5 px-3.5 rounded-lg shadow-lg z-[11] min-w-[180px] transition-colors ${
|
||||
patternUploaded ? 'bg-amber-50/95 dark:bg-amber-900/80 border-2 border-amber-300 dark:border-amber-600' : 'bg-white/95 dark:bg-gray-800/95'
|
||||
|
|
|
|||
90
src/components/PatternSummaryCard.tsx
Normal file
90
src/components/PatternSummaryCard.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { DocumentTextIcon, TrashIcon } from '@heroicons/react/24/solid';
|
||||
import type { PesPatternData } from '../utils/pystitchConverter';
|
||||
|
||||
interface PatternSummaryCardProps {
|
||||
pesData: PesPatternData;
|
||||
fileName: string;
|
||||
onDeletePattern: () => void;
|
||||
canDelete: boolean;
|
||||
isDeleting: boolean;
|
||||
}
|
||||
|
||||
export function PatternSummaryCard({
|
||||
pesData,
|
||||
fileName,
|
||||
onDeletePattern,
|
||||
canDelete,
|
||||
isDeleting
|
||||
}: PatternSummaryCardProps) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-blue-600 dark:border-blue-500">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<DocumentTextIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Active Pattern</h3>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 truncate" title={fileName}>
|
||||
{fileName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Size</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
|
||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Stitches</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{pesData.stitchCount.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">Colors:</span>
|
||||
<div className="flex gap-1">
|
||||
{pesData.threads.slice(0, 8).map((thread, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600"
|
||||
style={{ backgroundColor: thread.hex }}
|
||||
title={`Thread ${idx + 1}: ${thread.hex}`}
|
||||
/>
|
||||
))}
|
||||
{pesData.colorCount > 8 && (
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-[7px] font-bold text-gray-600 dark:text-gray-300">
|
||||
+{pesData.colorCount - 8}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canDelete && (
|
||||
<button
|
||||
onClick={onDeletePattern}
|
||||
disabled={isDeleting}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 rounded border border-red-300 dark:border-red-700 hover:bg-red-100 dark:hover:bg-red-900/30 text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<svg className="w-3 h-3 animate-spin" 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>
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TrashIcon className="w-3 h-3" />
|
||||
Delete Pattern
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,7 +6,8 @@ import {
|
|||
CheckBadgeIcon,
|
||||
ClockIcon,
|
||||
PauseCircleIcon,
|
||||
ExclamationCircleIcon
|
||||
ExclamationCircleIcon,
|
||||
ChartBarIcon
|
||||
} from '@heroicons/react/24/solid';
|
||||
import type { PatternInfo, SewingProgress } from '../types/machine';
|
||||
import { MachineStatus } from '../types/machine';
|
||||
|
|
@ -14,7 +15,6 @@ import type { PesPatternData } from '../utils/pystitchConverter';
|
|||
import {
|
||||
canStartSewing,
|
||||
canStartMaskTrace,
|
||||
canDeletePattern,
|
||||
canResumeSewing,
|
||||
getStateVisualInfo
|
||||
} from '../utils/machineStateHelpers';
|
||||
|
|
@ -39,8 +39,6 @@ export function ProgressMonitor({
|
|||
onStartMaskTrace,
|
||||
onStartSewing,
|
||||
onResumeSewing,
|
||||
onDeletePattern,
|
||||
isDeleting = false,
|
||||
}: ProgressMonitorProps) {
|
||||
// State indicators
|
||||
const isMaskTraceComplete = machineStatus === MachineStatus.MASK_TRACE_COMPLETE;
|
||||
|
|
@ -93,65 +91,68 @@ export function ProgressMonitor({
|
|||
);
|
||||
|
||||
const stateIndicatorColors = {
|
||||
idle: 'bg-blue-50 dark:bg-blue-900/20 border-l-blue-600',
|
||||
info: 'bg-blue-50 dark:bg-blue-900/20 border-l-blue-600',
|
||||
active: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
|
||||
waiting: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
|
||||
warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
|
||||
complete: 'bg-green-50 dark:bg-green-900/20 border-l-green-600',
|
||||
success: 'bg-green-50 dark:bg-green-900/20 border-l-green-600',
|
||||
interrupted: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
|
||||
error: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
|
||||
danger: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
|
||||
idle: 'bg-blue-50 dark:bg-blue-900/20 border-blue-600',
|
||||
info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-600',
|
||||
active: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500',
|
||||
waiting: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500',
|
||||
warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500',
|
||||
complete: 'bg-green-50 dark:bg-green-900/20 border-green-600',
|
||||
success: 'bg-green-50 dark:bg-green-900/20 border-green-600',
|
||||
interrupted: 'bg-red-50 dark:bg-red-900/20 border-red-600',
|
||||
error: 'bg-red-50 dark:bg-red-900/20 border-red-600',
|
||||
danger: 'bg-red-50 dark:bg-red-900/20 border-red-600',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 animate-fadeIn">
|
||||
<h2 className="text-lg font-semibold mb-3 pb-2 border-b border-gray-300 dark:border-gray-600 dark:text-white">Sewing Progress</h2>
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-purple-600 dark:border-purple-500">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<ChartBarIcon className="w-6 h-6 text-purple-600 dark:text-purple-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Sewing Progress</h3>
|
||||
{sewingProgress && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{progressPercent.toFixed(1)}% complete
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Left Column - Pattern Info & Progress */}
|
||||
<div>
|
||||
{/* Pattern Info */}
|
||||
{patternInfo && (
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-3 rounded-lg mb-3">
|
||||
<div className="grid grid-cols-3 gap-3 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400 block text-xs">Total Stitches</span>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs mb-3">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Total Stitches</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">{patternInfo.totalStitches.toLocaleString()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400 block text-xs">Est. Time</span>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Est. Time</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{Math.floor(patternInfo.totalTime / 60)}:{String(patternInfo.totalTime % 60).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400 block text-xs">Speed</span>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Speed</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">{patternInfo.speed} spm</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress Bar */}
|
||||
{sewingProgress && (
|
||||
<div className="mb-3">
|
||||
<div className="flex justify-between items-center mb-1.5">
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">Progress</span>
|
||||
<span className="text-xl font-bold text-blue-600 dark:text-blue-400">{progressPercent.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded-md overflow-hidden shadow-inner relative mb-2">
|
||||
<div className="h-full bg-gradient-to-r from-blue-600 to-blue-700 dark:from-blue-600 dark:to-blue-800 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite]" style={{ width: `${progressPercent}%` }} />
|
||||
<div className="h-full bg-gradient-to-r from-purple-600 to-purple-700 dark:from-purple-600 dark:to-purple-800 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite]" style={{ width: `${progressPercent}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded-lg grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400 block text-xs">Current Stitch</span>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Current Stitch</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{sewingProgress.currentStitch.toLocaleString()} / {patternInfo?.totalStitches.toLocaleString() || 0}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400 block text-xs">Time Elapsed</span>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">Time Elapsed</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{Math.floor(sewingProgress.currentTime / 60)}:{String(sewingProgress.currentTime % 60).padStart(2, '0')}
|
||||
</span>
|
||||
|
|
@ -163,92 +164,31 @@ export function ProgressMonitor({
|
|||
{/* State Visual Indicator */}
|
||||
{patternInfo && (() => {
|
||||
const iconMap = {
|
||||
ready: <ClockIcon className="w-6 h-6 text-blue-600 dark:text-blue-400" />,
|
||||
active: <PlayIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />,
|
||||
waiting: <PauseCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />,
|
||||
complete: <CheckBadgeIcon className="w-6 h-6 text-green-600 dark:text-green-400" />,
|
||||
interrupted: <PauseCircleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />,
|
||||
error: <ExclamationCircleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />
|
||||
ready: <ClockIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />,
|
||||
active: <PlayIcon className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />,
|
||||
waiting: <PauseCircleIcon className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />,
|
||||
complete: <CheckBadgeIcon className="w-5 h-5 text-green-600 dark:text-green-400" />,
|
||||
interrupted: <PauseCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400" />,
|
||||
error: <ExclamationCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400" />
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-3 p-3 rounded-lg mb-3 border-l-4 ${stateIndicatorColors[stateVisual.color as keyof typeof stateIndicatorColors] || stateIndicatorColors.info}`}>
|
||||
<div className={`flex items-center gap-3 p-2.5 rounded-lg mb-3 border-l-4 ${stateIndicatorColors[stateVisual.color as keyof typeof stateIndicatorColors] || stateIndicatorColors.info}`}>
|
||||
<div className="flex-shrink-0">
|
||||
{iconMap[stateVisual.iconName]}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-sm dark:text-gray-100">{stateVisual.label}</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">{stateVisual.description}</div>
|
||||
<div className="font-semibold text-xs dark:text-gray-100">{stateVisual.label}</div>
|
||||
<div className="text-[10px] text-gray-600 dark:text-gray-400">{stateVisual.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{/* Resume has highest priority when available */}
|
||||
{canResumeSewing(machineStatus) && (
|
||||
<button
|
||||
onClick={onResumeSewing}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-blue-600 dark:bg-blue-700 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
aria-label="Resume sewing the current pattern"
|
||||
>
|
||||
<PlayIcon className="w-4 h-4" />
|
||||
Resume Sewing
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Start Sewing - primary action */}
|
||||
{canStartSewing(machineStatus) && !canResumeSewing(machineStatus) && (
|
||||
<button
|
||||
onClick={onStartSewing}
|
||||
className="px-4 py-2.5 bg-blue-600 dark:bg-blue-700 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
aria-label="Start sewing the pattern"
|
||||
>
|
||||
Start Sewing
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Start Mask Trace - secondary action */}
|
||||
{canStartMaskTrace(machineStatus) && (
|
||||
<button
|
||||
onClick={onStartMaskTrace}
|
||||
className="px-4 py-2.5 bg-gray-600 dark:bg-gray-700 text-white rounded-lg font-semibold text-sm hover:bg-gray-700 dark:hover:bg-gray-600 active:bg-gray-800 dark:active:bg-gray-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-300 dark:focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
aria-label={isMaskTraceComplete ? 'Start mask trace again' : 'Start mask trace'}
|
||||
>
|
||||
{isMaskTraceComplete ? 'Trace Again' : 'Start Mask Trace'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Delete - destructive action, always last */}
|
||||
{patternInfo && canDeletePattern(machineStatus) && (
|
||||
<button
|
||||
onClick={onDeletePattern}
|
||||
disabled={isDeleting}
|
||||
className="px-4 py-2.5 bg-red-600 dark:bg-red-700 text-white rounded-lg font-semibold text-sm hover:bg-red-700 dark:hover:bg-red-600 active:bg-red-800 dark:active:bg-red-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-red-300 dark:focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 ml-auto disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-red-600 disabled:hover:shadow-none disabled:active:scale-100"
|
||||
aria-label={isDeleting ? "Deleting pattern..." : "Delete the current pattern from machine"}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<svg className="w-4 h-4 animate-spin inline mr-2" 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>
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
'Delete Pattern'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Color Blocks */}
|
||||
<div>
|
||||
{/* Color Blocks */}
|
||||
{colorBlocks.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Color Blocks</h3>
|
||||
<div className="mb-3">
|
||||
<h4 className="text-xs font-semibold mb-2 text-gray-700 dark:text-gray-300">Color Blocks</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
{colorBlocks.map((block, index) => {
|
||||
const isCompleted = currentStitch >= block.endStitch;
|
||||
|
|
@ -265,23 +205,23 @@ export function ProgressMonitor({
|
|||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-3 rounded-lg border-2 transition-all duration-300 ${
|
||||
className={`p-2.5 rounded-lg border-2 transition-all duration-300 ${
|
||||
isCompleted
|
||||
? 'border-green-600 bg-green-50 dark:bg-green-900/20 hover:bg-green-100 dark:hover:bg-green-900/30'
|
||||
? 'border-green-600 bg-green-50 dark:bg-green-900/20'
|
||||
: isCurrent
|
||||
? 'border-blue-600 bg-blue-50 dark:bg-blue-900/20 shadow-lg shadow-blue-600/20 animate-pulseGlow'
|
||||
: 'border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50 opacity-70 hover:opacity-90'
|
||||
? 'border-purple-600 bg-purple-50 dark:bg-purple-900/20 shadow-lg shadow-purple-600/20 animate-pulseGlow'
|
||||
: 'border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50 opacity-70'
|
||||
}`}
|
||||
role="listitem"
|
||||
aria-label={`Thread ${block.colorIndex + 1}, ${block.stitchCount} stitches, ${isCompleted ? 'completed' : isCurrent ? 'in progress' : 'pending'}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Larger color swatch with better visibility */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
{/* Color swatch */}
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg border-2 border-gray-300 dark:border-gray-600 shadow-md flex-shrink-0 ring-2 ring-offset-2 ring-transparent dark:ring-offset-gray-800"
|
||||
className="w-7 h-7 rounded-lg border-2 border-gray-300 dark:border-gray-600 shadow-md flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: block.threadHex,
|
||||
...(isCurrent && { borderColor: '#2563eb', ringColor: '#93c5fd' })
|
||||
...(isCurrent && { borderColor: '#9333ea' })
|
||||
}}
|
||||
title={`Thread color: ${block.threadHex}`}
|
||||
aria-label={`Thread color ${block.threadHex}`}
|
||||
|
|
@ -289,29 +229,29 @@ export function ProgressMonitor({
|
|||
|
||||
{/* Thread info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-sm text-gray-900 dark:text-gray-100">
|
||||
<div className="font-semibold text-xs text-gray-900 dark:text-gray-100">
|
||||
Thread {block.colorIndex + 1}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 mt-0.5">
|
||||
<div className="text-[10px] text-gray-600 dark:text-gray-400 mt-0.5">
|
||||
{block.stitchCount.toLocaleString()} stitches
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status icon */}
|
||||
{isCompleted ? (
|
||||
<CheckCircleIcon className="w-6 h-6 text-green-600 flex-shrink-0" aria-label="Completed" />
|
||||
<CheckCircleIcon className="w-5 h-5 text-green-600 flex-shrink-0" aria-label="Completed" />
|
||||
) : isCurrent ? (
|
||||
<ArrowRightIcon className="w-6 h-6 text-blue-600 flex-shrink-0 animate-pulse" aria-label="In progress" />
|
||||
<ArrowRightIcon className="w-5 h-5 text-purple-600 flex-shrink-0 animate-pulse" aria-label="In progress" />
|
||||
) : (
|
||||
<CircleStackIcon className="w-6 h-6 text-gray-400 flex-shrink-0" aria-label="Pending" />
|
||||
<CircleStackIcon className="w-5 h-5 text-gray-400 flex-shrink-0" aria-label="Pending" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress bar for current block */}
|
||||
{isCurrent && (
|
||||
<div className="mt-2 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className="mt-2 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-600 dark:bg-blue-500 transition-all duration-300 rounded-full"
|
||||
className="h-full bg-purple-600 dark:bg-purple-500 transition-all duration-300 rounded-full"
|
||||
style={{ width: `${blockProgress}%` }}
|
||||
role="progressbar"
|
||||
aria-valuenow={Math.round(blockProgress)}
|
||||
|
|
@ -327,7 +267,42 @@ export function ProgressMonitor({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{/* Resume has highest priority when available */}
|
||||
{canResumeSewing(machineStatus) && (
|
||||
<button
|
||||
onClick={onResumeSewing}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-xs hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer"
|
||||
aria-label="Resume sewing the current pattern"
|
||||
>
|
||||
<PlayIcon className="w-3.5 h-3.5" />
|
||||
Resume Sewing
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Start Sewing - primary action */}
|
||||
{canStartSewing(machineStatus) && !canResumeSewing(machineStatus) && (
|
||||
<button
|
||||
onClick={onStartSewing}
|
||||
className="px-3 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-xs hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer"
|
||||
aria-label="Start sewing the pattern"
|
||||
>
|
||||
Start Sewing
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Start Mask Trace - secondary action */}
|
||||
{canStartMaskTrace(machineStatus) && (
|
||||
<button
|
||||
onClick={onStartMaskTrace}
|
||||
className="px-3 py-2 bg-gray-600 dark:bg-gray-700 text-white rounded font-semibold text-xs hover:bg-gray-700 dark:hover:bg-gray-600 active:bg-gray-800 dark:active:bg-gray-500 transition-colors cursor-pointer"
|
||||
aria-label={isMaskTraceComplete ? 'Start mask trace again' : 'Start mask trace'}
|
||||
>
|
||||
{isMaskTraceComplete ? 'Trace Again' : 'Start Mask Trace'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue