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 { ProgressMonitor } from './components/ProgressMonitor';
|
||||||
import { WorkflowStepper } from './components/WorkflowStepper';
|
import { WorkflowStepper } from './components/WorkflowStepper';
|
||||||
import { NextStepGuide } from './components/NextStepGuide';
|
import { NextStepGuide } from './components/NextStepGuide';
|
||||||
|
import { PatternSummaryCard } from './components/PatternSummaryCard';
|
||||||
import type { PesPatternData } from './utils/pystitchConverter';
|
import type { PesPatternData } from './utils/pystitchConverter';
|
||||||
import { pyodideLoader } from './utils/pyodideLoader';
|
import { pyodideLoader } from './utils/pyodideLoader';
|
||||||
import { hasError } from './utils/errorCodeHelpers';
|
import { hasError } from './utils/errorCodeHelpers';
|
||||||
|
import { canDeletePattern } from './utils/machineStateHelpers';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
@ -153,17 +155,6 @@ function App() {
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-6">
|
||||||
{/* Left Column - Controls */}
|
{/* Left Column - Controls */}
|
||||||
<div className="flex flex-col gap-6">
|
<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 */}
|
{/* Machine Connection - Always visible */}
|
||||||
<MachineConnection
|
<MachineConnection
|
||||||
isConnected={machine.isConnected}
|
isConnected={machine.isConnected}
|
||||||
|
|
@ -177,8 +168,8 @@ function App() {
|
||||||
onRefresh={machine.refreshStatus}
|
onRefresh={machine.refreshStatus}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Pattern File - Only show when connected */}
|
{/* Pattern File - Show during upload stage (before pattern is uploaded) */}
|
||||||
{machine.isConnected && (
|
{machine.isConnected && !patternUploaded && (
|
||||||
<FileUpload
|
<FileUpload
|
||||||
isConnected={machine.isConnected}
|
isConnected={machine.isConnected}
|
||||||
machineStatus={machine.machineStatus}
|
machineStatus={machine.machineStatus}
|
||||||
|
|
@ -195,6 +186,32 @@ function App() {
|
||||||
isUploading={machine.isUploading}
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Pattern Preview */}
|
{/* Right Column - Pattern Preview */}
|
||||||
|
|
@ -254,20 +271,16 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Progress Monitor - Wide section below pattern preview */}
|
{/* Next Step Guide - Below pattern preview */}
|
||||||
{machine.isConnected && patternUploaded && (
|
<NextStepGuide
|
||||||
<ProgressMonitor
|
|
||||||
machineStatus={machine.machineStatus}
|
machineStatus={machine.machineStatus}
|
||||||
patternInfo={machine.patternInfo}
|
isConnected={machine.isConnected}
|
||||||
sewingProgress={machine.sewingProgress}
|
hasPattern={pesData !== null}
|
||||||
pesData={pesData}
|
patternUploaded={patternUploaded}
|
||||||
onStartMaskTrace={machine.startMaskTrace}
|
hasError={hasError(machine.machineError)}
|
||||||
onStartSewing={machine.startSewing}
|
errorMessage={machine.error || undefined}
|
||||||
onResumeSewing={machine.resumeSewing}
|
errorCode={machine.machineError}
|
||||||
onDeletePattern={handleDeletePattern}
|
|
||||||
isDeleting={machine.isDeleting}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { convertPesToPen, type PesPatternData } from '../utils/pystitchConverter
|
||||||
import { MachineStatus } from '../types/machine';
|
import { MachineStatus } from '../types/machine';
|
||||||
import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
|
import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers';
|
||||||
import { PatternInfoSkeleton } from './SkeletonLoader';
|
import { PatternInfoSkeleton } from './SkeletonLoader';
|
||||||
import { ArrowUpTrayIcon, CheckCircleIcon } from '@heroicons/react/24/solid';
|
import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon } from '@heroicons/react/24/solid';
|
||||||
|
|
||||||
interface FileUploadProps {
|
interface FileUploadProps {
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
|
@ -80,28 +80,98 @@ export function FileUpload({
|
||||||
}
|
}
|
||||||
}, [pesData, displayFileName, onUpload, patternOffset]);
|
}, [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 (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
|
<div className={`bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 ${borderColor}`}>
|
||||||
<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="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 && (
|
{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">
|
<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-sm text-green-800 dark:text-green-200">
|
<p className="text-xs text-green-800 dark:text-green-200">
|
||||||
<strong>Loaded cached pattern:</strong> "{resumeFileName}"
|
<strong>Cached:</strong> "{resumeFileName}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{patternUploaded && (
|
{isLoading && <PatternInfoSkeleton />}
|
||||||
<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">
|
{!isLoading && pesData && (
|
||||||
<strong>Pattern uploaded successfully!</strong> The pattern is now locked and cannot be changed.
|
<div className="mb-3">
|
||||||
To upload a different pattern, you must first complete or delete the current one.
|
<div className="grid grid-cols-2 gap-2 text-xs mb-2">
|
||||||
</p>
|
<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>
|
</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
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pes"
|
accept=".pes"
|
||||||
|
|
@ -112,23 +182,23 @@ export function FileUpload({
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="file-input"
|
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
|
!pyodideReady || isLoading || patternUploaded
|
||||||
? 'opacity-50 cursor-not-allowed grayscale-[0.3]'
|
? 'opacity-50 cursor-not-allowed bg-gray-400 dark:bg-gray-600 text-white'
|
||||||
: 'cursor-pointer hover:bg-gray-700 dark:hover:bg-gray-600 hover:shadow-lg active:scale-[0.98]'
|
: 'cursor-pointer bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{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>
|
<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>
|
<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>
|
</svg>
|
||||||
<span>Loading Pattern...</span>
|
<span>Loading...</span>
|
||||||
</>
|
</>
|
||||||
) : !pyodideReady ? (
|
) : !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>
|
<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>
|
<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>
|
</svg>
|
||||||
|
|
@ -136,107 +206,37 @@ export function FileUpload({
|
||||||
</>
|
</>
|
||||||
) : patternUploaded ? (
|
) : patternUploaded ? (
|
||||||
<>
|
<>
|
||||||
<CheckCircleIcon className="w-5 h-5" />
|
<CheckCircleIcon className="w-3.5 h-3.5" />
|
||||||
<span>Pattern Locked</span>
|
<span>Locked</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span>Choose PES File</span>
|
<span>Choose PES File</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</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 && (
|
{pesData && canUploadPattern(machineStatus) && !patternUploaded && uploadProgress < 100 && (
|
||||||
<button
|
<button
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={!isConnected || isUploading}
|
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'}
|
aria-label={isUploading ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : 'Upload pattern to machine'}
|
||||||
>
|
>
|
||||||
{isUploading ? (
|
{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>
|
<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>
|
<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>
|
</svg>
|
||||||
<span>Uploading... {uploadProgress > 0 ? uploadProgress.toFixed(0) + '%' : ''}</span>
|
{uploadProgress > 0 ? uploadProgress.toFixed(0) + '%' : 'Uploading'}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ArrowUpTrayIcon className="w-5 h-5" />
|
<ArrowUpTrayIcon className="w-3.5 h-3.5 inline mr-1" />
|
||||||
<span>Upload to Machine</span>
|
Upload
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
BoltIcon,
|
BoltIcon,
|
||||||
PauseCircleIcon,
|
PauseCircleIcon,
|
||||||
ExclamationTriangleIcon,
|
ExclamationTriangleIcon,
|
||||||
|
WifiIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import type { MachineInfo } from '../types/machine';
|
import type { MachineInfo } from '../types/machine';
|
||||||
import { MachineStatus } from '../types/machine';
|
import { MachineStatus } from '../types/machine';
|
||||||
|
|
@ -62,73 +63,77 @@ export function MachineConnection({
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusBadgeColors = {
|
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',
|
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 border-cyan-200 dark:border-cyan-700',
|
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 border-yellow-200 dark:border-yellow-700',
|
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 border-yellow-200 dark:border-yellow-700',
|
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 border-yellow-200 dark:border-yellow-700',
|
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 border-green-200 dark:border-green-700',
|
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 border-green-200 dark:border-green-700',
|
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 border-red-200 dark:border-red-700',
|
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 border-red-200 dark:border-red-700',
|
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 border-red-200 dark:border-red-700',
|
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
|
// Only show error info when connected AND there's an actual error
|
||||||
const errorInfo = (isConnected && hasError(machineError)) ? getErrorDetails(machineError) : null;
|
const errorInfo = (isConnected && hasError(machineError)) ? getErrorDetails(machineError) : null;
|
||||||
|
|
||||||
return (
|
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">
|
{!isConnected ? (
|
||||||
<h2 className="text-xl font-semibold dark:text-white">Machine Connection</h2>
|
<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-center gap-3">
|
<div className="flex items-start gap-3 mb-3">
|
||||||
{isConnected && isPolling && (
|
<WifiIcon className="w-6 h-6 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5" />
|
||||||
<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 className="flex-1 min-w-0">
|
||||||
)}
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Machine Connection</h3>
|
||||||
{isConnected && (
|
<p className="text-xs text-gray-600 dark:text-gray-400">Not connected</p>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isConnected ? (
|
|
||||||
<div className="flex gap-3 mt-4 flex-wrap">
|
|
||||||
<button
|
<button
|
||||||
onClick={onConnect}
|
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"
|
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"
|
||||||
aria-label="Connect to embroidery machine"
|
|
||||||
>
|
>
|
||||||
Connect to Machine
|
Connect to Machine
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Error/Info Display */}
|
||||||
{errorInfo && (
|
{errorInfo && (
|
||||||
errorInfo.isInformational ? (
|
errorInfo.isInformational ? (
|
||||||
// Informational messages (like initialization steps)
|
<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="mb-4 p-4 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">
|
<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="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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Regular errors shown as errors
|
<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="mb-4 p-4 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">
|
<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="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="font-semibold text-red-900 dark:text-red-200 text-xs mb-1">{errorInfo.title}</div>
|
||||||
<div className="text-xs text-red-700 dark:text-red-300 font-mono">
|
<div className="text-[10px] text-red-700 dark:text-red-300 font-mono">
|
||||||
Error Code: 0x{machineError.toString(16).toUpperCase().padStart(2, '0')}
|
Error Code: 0x{machineError.toString(16).toUpperCase().padStart(2, '0')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -137,36 +142,30 @@ export function MachineConnection({
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Machine Status */}
|
{/* Status Badge */}
|
||||||
<div className="mb-4">
|
<div className="mb-3">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<span className="text-xs text-gray-600 dark:text-gray-400 block mb-1">Status:</span>
|
||||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">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}`}>
|
||||||
<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}`}>
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const Icon = stateIcons[stateVisual.iconName];
|
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>{machineStatusName}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Machine Info */}
|
{/* Machine Info */}
|
||||||
{machineInfo && (
|
{machineInfo && (
|
||||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-4 rounded-lg space-y-2 mb-4">
|
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||||
<span className="font-medium text-gray-600 dark:text-gray-400">Serial Number:</span>
|
<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.serialNumber}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="font-medium text-gray-600 dark:text-gray-400">Max Area:</span>
|
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{(machineInfo.maxWidth / 10).toFixed(1)} × {(machineInfo.maxHeight / 10).toFixed(1)} mm
|
{(machineInfo.maxWidth / 10).toFixed(1)} × {(machineInfo.maxHeight / 10).toFixed(1)} mm
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{machineInfo.totalCount !== undefined && (
|
{machineInfo.totalCount !== undefined && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||||
<span className="font-medium text-gray-600 dark:text-gray-400">Total Stitches:</span>
|
<span className="text-gray-600 dark:text-gray-400 block">Total Stitches</span>
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{machineInfo.totalCount.toLocaleString()}
|
{machineInfo.totalCount.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -174,6 +173,13 @@ export function MachineConnection({
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -187,6 +193,6 @@ export function MachineConnection({
|
||||||
onCancel={() => setShowDisconnectConfirm(false)}
|
onCancel={() => setShowDisconnectConfirm(false)}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import { Stage, Layer, Group } from 'react-konva';
|
import { Stage, Layer, Group } from 'react-konva';
|
||||||
import Konva from '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 { PesPatternData } from '../utils/pystitchConverter';
|
||||||
import type { SewingProgress, MachineInfo } from '../types/machine';
|
import type { SewingProgress, MachineInfo } from '../types/machine';
|
||||||
import { calculateInitialScale } from '../utils/konvaRenderers';
|
import { calculateInitialScale } from '../utils/konvaRenderers';
|
||||||
|
|
@ -181,9 +181,24 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
||||||
}
|
}
|
||||||
}, [onPatternOffsetChange]);
|
}, [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 (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
|
<div className={`bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 ${borderColor}`}>
|
||||||
<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="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}>
|
<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 && (
|
{containerSize.width > 0 && (
|
||||||
<Stage
|
<Stage
|
||||||
|
|
@ -281,25 +296,19 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
||||||
{pesData && (
|
{pesData && (
|
||||||
<>
|
<>
|
||||||
{/* Thread Legend Overlay */}
|
{/* 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]">
|
<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-[13px] font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-300 dark:border-gray-600 pb-1.5">Threads</h4>
|
<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) => (
|
{pesData.threads.map((thread, index) => (
|
||||||
<div key={index} className="flex items-center gap-2 mb-1.5 last:mb-0">
|
<div key={index} className="flex items-center gap-2 mb-1.5 last:mb-0">
|
||||||
<div
|
<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 }}
|
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>
|
||||||
))}
|
))}
|
||||||
</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 */}
|
{/* 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 ${
|
<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'
|
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,
|
CheckBadgeIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
PauseCircleIcon,
|
PauseCircleIcon,
|
||||||
ExclamationCircleIcon
|
ExclamationCircleIcon,
|
||||||
|
ChartBarIcon
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import type { PatternInfo, SewingProgress } from '../types/machine';
|
import type { PatternInfo, SewingProgress } from '../types/machine';
|
||||||
import { MachineStatus } from '../types/machine';
|
import { MachineStatus } from '../types/machine';
|
||||||
|
|
@ -14,7 +15,6 @@ import type { PesPatternData } from '../utils/pystitchConverter';
|
||||||
import {
|
import {
|
||||||
canStartSewing,
|
canStartSewing,
|
||||||
canStartMaskTrace,
|
canStartMaskTrace,
|
||||||
canDeletePattern,
|
|
||||||
canResumeSewing,
|
canResumeSewing,
|
||||||
getStateVisualInfo
|
getStateVisualInfo
|
||||||
} from '../utils/machineStateHelpers';
|
} from '../utils/machineStateHelpers';
|
||||||
|
|
@ -39,8 +39,6 @@ export function ProgressMonitor({
|
||||||
onStartMaskTrace,
|
onStartMaskTrace,
|
||||||
onStartSewing,
|
onStartSewing,
|
||||||
onResumeSewing,
|
onResumeSewing,
|
||||||
onDeletePattern,
|
|
||||||
isDeleting = false,
|
|
||||||
}: ProgressMonitorProps) {
|
}: ProgressMonitorProps) {
|
||||||
// State indicators
|
// State indicators
|
||||||
const isMaskTraceComplete = machineStatus === MachineStatus.MASK_TRACE_COMPLETE;
|
const isMaskTraceComplete = machineStatus === MachineStatus.MASK_TRACE_COMPLETE;
|
||||||
|
|
@ -93,65 +91,68 @@ export function ProgressMonitor({
|
||||||
);
|
);
|
||||||
|
|
||||||
const stateIndicatorColors = {
|
const stateIndicatorColors = {
|
||||||
idle: 'bg-blue-50 dark:bg-blue-900/20 border-l-blue-600',
|
idle: 'bg-blue-50 dark:bg-blue-900/20 border-blue-600',
|
||||||
info: 'bg-blue-50 dark:bg-blue-900/20 border-l-blue-600',
|
info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-600',
|
||||||
active: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
|
active: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500',
|
||||||
waiting: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
|
waiting: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500',
|
||||||
warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
|
warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500',
|
||||||
complete: 'bg-green-50 dark:bg-green-900/20 border-l-green-600',
|
complete: 'bg-green-50 dark:bg-green-900/20 border-green-600',
|
||||||
success: 'bg-green-50 dark:bg-green-900/20 border-l-green-600',
|
success: 'bg-green-50 dark:bg-green-900/20 border-green-600',
|
||||||
interrupted: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
|
interrupted: 'bg-red-50 dark:bg-red-900/20 border-red-600',
|
||||||
error: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
|
error: 'bg-red-50 dark:bg-red-900/20 border-red-600',
|
||||||
danger: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
|
danger: 'bg-red-50 dark:bg-red-900/20 border-red-600',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 animate-fadeIn">
|
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-purple-600 dark:border-purple-500">
|
||||||
<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="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">
|
{/* Pattern Info */}
|
||||||
{/* Left Column - Pattern Info & Progress */}
|
|
||||||
<div>
|
|
||||||
{patternInfo && (
|
{patternInfo && (
|
||||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-3 rounded-lg mb-3">
|
<div className="grid grid-cols-3 gap-2 text-xs mb-3">
|
||||||
<div className="grid grid-cols-3 gap-3 text-sm">
|
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||||
<div>
|
<span className="text-gray-600 dark:text-gray-400 block">Total Stitches</span>
|
||||||
<span className="text-gray-600 dark:text-gray-400 block text-xs">Total Stitches</span>
|
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">{patternInfo.totalStitches.toLocaleString()}</span>
|
<span className="font-semibold text-gray-900 dark:text-gray-100">{patternInfo.totalStitches.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||||
<span className="text-gray-600 dark:text-gray-400 block text-xs">Est. Time</span>
|
<span className="text-gray-600 dark:text-gray-400 block">Est. Time</span>
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{Math.floor(patternInfo.totalTime / 60)}:{String(patternInfo.totalTime % 60).padStart(2, '0')}
|
{Math.floor(patternInfo.totalTime / 60)}:{String(patternInfo.totalTime % 60).padStart(2, '0')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||||
<span className="text-gray-600 dark:text-gray-400 block text-xs">Speed</span>
|
<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>
|
<span className="font-semibold text-gray-900 dark:text-gray-100">{patternInfo.speed} spm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
{sewingProgress && (
|
{sewingProgress && (
|
||||||
<div className="mb-3">
|
<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-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>
|
||||||
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded-lg grid grid-cols-2 gap-2 text-sm">
|
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
|
||||||
<div>
|
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||||
<span className="text-gray-600 dark:text-gray-400 block text-xs">Current Stitch</span>
|
<span className="text-gray-600 dark:text-gray-400 block">Current Stitch</span>
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{sewingProgress.currentStitch.toLocaleString()} / {patternInfo?.totalStitches.toLocaleString() || 0}
|
{sewingProgress.currentStitch.toLocaleString()} / {patternInfo?.totalStitches.toLocaleString() || 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded">
|
||||||
<span className="text-gray-600 dark:text-gray-400 block text-xs">Time Elapsed</span>
|
<span className="text-gray-600 dark:text-gray-400 block">Time Elapsed</span>
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{Math.floor(sewingProgress.currentTime / 60)}:{String(sewingProgress.currentTime % 60).padStart(2, '0')}
|
{Math.floor(sewingProgress.currentTime / 60)}:{String(sewingProgress.currentTime % 60).padStart(2, '0')}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -163,92 +164,31 @@ export function ProgressMonitor({
|
||||||
{/* State Visual Indicator */}
|
{/* State Visual Indicator */}
|
||||||
{patternInfo && (() => {
|
{patternInfo && (() => {
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
ready: <ClockIcon className="w-6 h-6 text-blue-600 dark:text-blue-400" />,
|
ready: <ClockIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />,
|
||||||
active: <PlayIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />,
|
active: <PlayIcon className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />,
|
||||||
waiting: <PauseCircleIcon className="w-6 h-6 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-6 h-6 text-green-600 dark:text-green-400" />,
|
complete: <CheckBadgeIcon className="w-5 h-5 text-green-600 dark:text-green-400" />,
|
||||||
interrupted: <PauseCircleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />,
|
interrupted: <PauseCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400" />,
|
||||||
error: <ExclamationCircleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />
|
error: <ExclamationCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400" />
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex-shrink-0">
|
||||||
{iconMap[stateVisual.iconName]}
|
{iconMap[stateVisual.iconName]}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="font-semibold text-sm dark:text-gray-100">{stateVisual.label}</div>
|
<div className="font-semibold text-xs dark:text-gray-100">{stateVisual.label}</div>
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-400">{stateVisual.description}</div>
|
<div className="text-[10px] text-gray-600 dark:text-gray-400">{stateVisual.description}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Color Blocks */}
|
||||||
<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>
|
|
||||||
{colorBlocks.length > 0 && (
|
{colorBlocks.length > 0 && (
|
||||||
<div>
|
<div className="mb-3">
|
||||||
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Color Blocks</h3>
|
<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">
|
<div className="flex flex-col gap-2">
|
||||||
{colorBlocks.map((block, index) => {
|
{colorBlocks.map((block, index) => {
|
||||||
const isCompleted = currentStitch >= block.endStitch;
|
const isCompleted = currentStitch >= block.endStitch;
|
||||||
|
|
@ -265,23 +205,23 @@ export function ProgressMonitor({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
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
|
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
|
: isCurrent
|
||||||
? 'border-blue-600 bg-blue-50 dark:bg-blue-900/20 shadow-lg shadow-blue-600/20 animate-pulseGlow'
|
? '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 hover:opacity-90'
|
: 'border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50 opacity-70'
|
||||||
}`}
|
}`}
|
||||||
role="listitem"
|
role="listitem"
|
||||||
aria-label={`Thread ${block.colorIndex + 1}, ${block.stitchCount} stitches, ${isCompleted ? 'completed' : isCurrent ? 'in progress' : 'pending'}`}
|
aria-label={`Thread ${block.colorIndex + 1}, ${block.stitchCount} stitches, ${isCompleted ? 'completed' : isCurrent ? 'in progress' : 'pending'}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2.5">
|
||||||
{/* Larger color swatch with better visibility */}
|
{/* Color swatch */}
|
||||||
<div
|
<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={{
|
style={{
|
||||||
backgroundColor: block.threadHex,
|
backgroundColor: block.threadHex,
|
||||||
...(isCurrent && { borderColor: '#2563eb', ringColor: '#93c5fd' })
|
...(isCurrent && { borderColor: '#9333ea' })
|
||||||
}}
|
}}
|
||||||
title={`Thread color: ${block.threadHex}`}
|
title={`Thread color: ${block.threadHex}`}
|
||||||
aria-label={`Thread color ${block.threadHex}`}
|
aria-label={`Thread color ${block.threadHex}`}
|
||||||
|
|
@ -289,29 +229,29 @@ export function ProgressMonitor({
|
||||||
|
|
||||||
{/* Thread info */}
|
{/* Thread info */}
|
||||||
<div className="flex-1 min-w-0">
|
<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}
|
Thread {block.colorIndex + 1}
|
||||||
</div>
|
</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
|
{block.stitchCount.toLocaleString()} stitches
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status icon */}
|
{/* Status icon */}
|
||||||
{isCompleted ? (
|
{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 ? (
|
) : 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>
|
</div>
|
||||||
|
|
||||||
{/* Progress bar for current block */}
|
{/* Progress bar for current block */}
|
||||||
{isCurrent && (
|
{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
|
<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}%` }}
|
style={{ width: `${blockProgress}%` }}
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-valuenow={Math.round(blockProgress)}
|
aria-valuenow={Math.round(blockProgress)}
|
||||||
|
|
@ -327,7 +267,42 @@ export function ProgressMonitor({
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue