mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
Enhance UI/UX with loading states, animations, and pattern lock functionality
Loading State Improvements: - Add SkeletonLoader component with pattern info, canvas, and connection skeletons - Show loading spinner on file selection and during pattern upload - Display upload progress with enhanced progress bar and percentage - Add success confirmation message when upload completes - Show thread color preview dots inline with pattern info (up to 5 colors) Visual Polish & Animations: - Add custom animations: fadeIn, slideInRight, pulseGlow, skeleton-loading - Enhance all cards with subtle hover shadow effects - Improve header with richer gradient (blue-600 → blue-700 → blue-800) - Polish error messages with icons and improved layouts - Enhance empty state with decorative patterns and feature highlights - Add smooth transitions to all NextStepGuide states - Current color block pulses with blue glow animation - Color blocks have hover states for better interactivity Pattern Upload & Lock Functionality: - Hide upload button after pattern is uploaded (patternUploaded && uploadProgress === 100) - Disable pattern dragging when uploaded with visual lock indicator - Pattern position overlay shows amber background with lock icon when locked - Pattern remains in canvas after deletion for re-editing and re-upload - Delete pattern from cache when deleting from machine to prevent auto-resume - Add LockClosedIcon to indicate locked pattern state Pattern Management: - Keep pattern data in UI after deletion for repositioning and re-uploading - Clear machine-related state but preserve pattern visualization - Reset upload progress and pattern uploaded state on deletion 🤖 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
3d26291d6d
commit
c5ec118b95
9 changed files with 391 additions and 67 deletions
71
src/App.css
71
src/App.css
|
|
@ -9,3 +9,74 @@
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Skeleton loading animation */
|
||||||
|
@keyframes skeleton-loading {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade in animation */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slide in from right */
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse glow effect */
|
||||||
|
@keyframes pulseGlow {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(59, 130, 246, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success checkmark animation */
|
||||||
|
@keyframes checkmark {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 100;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility classes */
|
||||||
|
.animate-fadeIn {
|
||||||
|
animation: fadeIn 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slideInRight {
|
||||||
|
animation: slideInRight 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulseGlow {
|
||||||
|
animation: pulseGlow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-skeleton {
|
||||||
|
animation: skeleton-loading 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
|
||||||
84
src/App.tsx
84
src/App.tsx
|
|
@ -66,7 +66,8 @@ function App() {
|
||||||
const handleDeletePattern = useCallback(async () => {
|
const handleDeletePattern = useCallback(async () => {
|
||||||
await machine.deletePattern();
|
await machine.deletePattern();
|
||||||
setPatternUploaded(false);
|
setPatternUploaded(false);
|
||||||
setPesData(null);
|
// NOTE: We intentionally DON'T clear setPesData(null) here
|
||||||
|
// so the pattern remains visible in the canvas for re-editing and re-uploading
|
||||||
}, [machine]);
|
}, [machine]);
|
||||||
|
|
||||||
// Track pattern uploaded state based on machine status
|
// Track pattern uploaded state based on machine status
|
||||||
|
|
@ -87,7 +88,7 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col bg-gray-50">
|
<div className="min-h-screen flex flex-col bg-gray-50">
|
||||||
<header className="bg-gradient-to-r from-blue-600 to-blue-700 px-8 py-3 shadow-lg">
|
<header className="bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 px-8 py-3 shadow-lg border-b-2 border-blue-900/20">
|
||||||
<div className="max-w-[1600px] mx-auto flex items-center gap-8">
|
<div className="max-w-[1600px] mx-auto flex items-center gap-8">
|
||||||
<h1 className="text-xl font-bold text-white whitespace-nowrap">SKiTCH Controller</h1>
|
<h1 className="text-xl font-bold text-white whitespace-nowrap">SKiTCH Controller</h1>
|
||||||
|
|
||||||
|
|
@ -108,18 +109,38 @@ function App() {
|
||||||
<div className="flex-1 p-6 max-w-[1600px] w-full mx-auto">
|
<div className="flex-1 p-6 max-w-[1600px] w-full mx-auto">
|
||||||
{/* Global errors */}
|
{/* Global errors */}
|
||||||
{machine.error && (
|
{machine.error && (
|
||||||
<div className="bg-red-100 text-red-900 px-6 py-4 rounded-lg border-l-4 border-red-600 mb-6 shadow-md">
|
<div className="bg-red-50 text-red-900 px-6 py-4 rounded-lg border-l-4 border-red-600 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn">
|
||||||
<strong>Error:</strong> {machine.error}
|
<div className="flex items-center gap-2">
|
||||||
|
<svg className="w-5 h-5 text-red-600 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<strong className="font-semibold">Error:</strong> {machine.error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{pyodideError && (
|
{pyodideError && (
|
||||||
<div className="bg-red-100 text-red-900 px-6 py-4 rounded-lg border-l-4 border-red-600 mb-6 shadow-md">
|
<div className="bg-red-50 text-red-900 px-6 py-4 rounded-lg border-l-4 border-red-600 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn">
|
||||||
<strong>Python Error:</strong> {pyodideError}
|
<div className="flex items-center gap-2">
|
||||||
|
<svg className="w-5 h-5 text-red-600 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<strong className="font-semibold">Python Error:</strong> {pyodideError}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!pyodideReady && !pyodideError && (
|
{!pyodideReady && !pyodideError && (
|
||||||
<div className="bg-blue-100 text-blue-900 px-6 py-4 rounded-lg border-l-4 border-blue-600 mb-6 shadow-md">
|
<div className="bg-blue-50 text-blue-900 px-6 py-4 rounded-lg border-l-4 border-blue-600 mb-6 shadow-md animate-fadeIn">
|
||||||
Initializing Python environment...
|
<div className="flex items-center gap-3">
|
||||||
|
<svg className="w-5 h-5 animate-spin text-blue-600" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span className="font-medium">Initializing Python environment...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -177,17 +198,48 @@ function App() {
|
||||||
machineInfo={machine.machineInfo}
|
machineInfo={machine.machineInfo}
|
||||||
initialPatternOffset={patternOffset}
|
initialPatternOffset={patternOffset}
|
||||||
onPatternOffsetChange={handlePatternOffsetChange}
|
onPatternOffsetChange={handlePatternOffsetChange}
|
||||||
|
patternUploaded={patternUploaded}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
<div className="bg-white p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Pattern Preview</h2>
|
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Pattern Preview</h2>
|
||||||
<div className="flex items-center justify-center h-[600px] bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
|
<div className="flex items-center justify-center h-[600px] bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg border-2 border-dashed border-gray-300 relative overflow-hidden">
|
||||||
<div className="text-center">
|
{/* Decorative background pattern */}
|
||||||
<svg className="w-24 h-24 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="absolute inset-0 opacity-5">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
<div className="absolute top-10 left-10 w-32 h-32 border-4 border-gray-400 rounded-full"></div>
|
||||||
</svg>
|
<div className="absolute bottom-10 right-10 w-40 h-40 border-4 border-gray-400 rounded-full"></div>
|
||||||
<p className="text-gray-600 text-lg mb-2">No Pattern Loaded</p>
|
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-48 h-48 border-4 border-gray-400 rounded-full"></div>
|
||||||
<p className="text-gray-500 text-sm">Connect to your machine and choose a PES file to begin</p>
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center relative z-10">
|
||||||
|
<div className="relative inline-block mb-6">
|
||||||
|
<svg className="w-28 h-28 mx-auto text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
<div className="absolute -top-2 -right-2 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-gray-700 text-xl font-semibold mb-2">No Pattern Loaded</h3>
|
||||||
|
<p className="text-gray-500 text-sm mb-4 max-w-sm mx-auto">
|
||||||
|
Connect to your machine and choose a PES embroidery file to see your design preview
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-center gap-6 text-xs text-gray-400">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
|
||||||
|
<span>Drag to Position</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
|
||||||
|
<span>Zoom & Pan</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-2 h-2 bg-purple-400 rounded-full"></div>
|
||||||
|
<span>Real-time Preview</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { useState, useCallback } from 'react';
|
||||||
import { convertPesToPen, type PesPatternData } from '../utils/pystitchConverter';
|
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 { ArrowUpTrayIcon, CheckCircleIcon } from '@heroicons/react/24/solid';
|
||||||
|
|
||||||
interface FileUploadProps {
|
interface FileUploadProps {
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
|
@ -74,7 +76,7 @@ export function FileUpload({
|
||||||
}, [pesData, displayFileName, onUpload, patternOffset]);
|
}, [pesData, displayFileName, onUpload, patternOffset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
<div className="bg-white 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">Pattern File</h2>
|
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Pattern File</h2>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -103,30 +105,81 @@ export function FileUpload({
|
||||||
className="hidden"
|
className="hidden"
|
||||||
disabled={!pyodideReady || isLoading || patternUploaded}
|
disabled={!pyodideReady || isLoading || patternUploaded}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="file-input" className={`inline-block px-6 py-3 bg-gray-600 text-white rounded font-semibold text-sm transition-all ${!pyodideReady || isLoading || patternUploaded ? 'opacity-50 cursor-not-allowed grayscale-[0.3]' : 'cursor-pointer hover:bg-gray-700 hover:shadow-md'}`}>
|
<label
|
||||||
{isLoading ? 'Loading...' : !pyodideReady ? 'Initializing...' : patternUploaded ? 'Pattern Locked' : 'Choose PES File'}
|
htmlFor="file-input"
|
||||||
|
className={`inline-flex items-center gap-2 px-6 py-3 bg-gray-600 text-white rounded-lg font-semibold text-sm transition-all ${
|
||||||
|
!pyodideReady || isLoading || patternUploaded
|
||||||
|
? 'opacity-50 cursor-not-allowed grayscale-[0.3]'
|
||||||
|
: 'cursor-pointer hover:bg-gray-700 hover:shadow-lg active:scale-[0.98]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<svg className="w-5 h-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>
|
||||||
|
</>
|
||||||
|
) : !pyodideReady ? (
|
||||||
|
<>
|
||||||
|
<svg className="w-5 h-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>Initializing...</span>
|
||||||
|
</>
|
||||||
|
) : patternUploaded ? (
|
||||||
|
<>
|
||||||
|
<CheckCircleIcon className="w-5 h-5" />
|
||||||
|
<span>Pattern Locked</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>Choose PES File</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{pesData && (
|
{isLoading && <PatternInfoSkeleton />}
|
||||||
<div className="mt-4">
|
|
||||||
|
{!isLoading && pesData && (
|
||||||
|
<div className="mt-4 animate-fadeIn">
|
||||||
<h3 className="text-base font-semibold my-4">Pattern Information</h3>
|
<h3 className="text-base font-semibold my-4">Pattern Information</h3>
|
||||||
<div className="bg-gray-50 p-4 rounded-lg space-y-3">
|
<div className="bg-gradient-to-br from-gray-50 to-gray-100 p-4 rounded-lg space-y-3 border border-gray-200 shadow-sm">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-medium text-gray-700">File Name:</span>
|
<span className="font-medium text-gray-700">File Name:</span>
|
||||||
<span className="font-semibold text-gray-900">{displayFileName}</span>
|
<span className="font-semibold text-gray-900 text-right max-w-[200px] truncate" title={displayFileName}>
|
||||||
|
{displayFileName}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-medium text-gray-700">Pattern Size:</span>
|
<span className="font-medium text-gray-700">Pattern Size:</span>
|
||||||
<span className="font-semibold text-gray-900">
|
<span className="font-semibold text-gray-900">
|
||||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
|
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
|
||||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-medium text-gray-700">Thread Colors:</span>
|
<span className="font-medium text-gray-700">Thread Colors:</span>
|
||||||
<span className="font-semibold text-gray-900">{pesData.colorCount}</span>
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold text-gray-900">{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 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 border border-gray-400 flex items-center justify-center text-[8px] font-bold text-gray-600">
|
||||||
|
+{pesData.colorCount - 5}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-medium text-gray-700">Total Stitches:</span>
|
<span className="font-medium text-gray-700">Total Stitches:</span>
|
||||||
<span className="font-semibold text-gray-900">{pesData.stitchCount.toLocaleString()}</span>
|
<span className="font-semibold text-gray-900">{pesData.stitchCount.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -134,31 +187,59 @@ export function FileUpload({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{pesData && canUploadPattern(machineStatus) && (
|
{pesData && canUploadPattern(machineStatus) && !patternUploaded && uploadProgress < 100 && (
|
||||||
<button
|
<button
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={!isConnected || uploadProgress > 0}
|
disabled={!isConnected || uploadProgress > 0}
|
||||||
className="mt-4 px-6 py-2.5 bg-blue-600 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 active:bg-blue-800 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 disabled:hover:shadow-none disabled:active:scale-100"
|
className="mt-4 inline-flex items-center gap-2 px-6 py-2.5 bg-blue-600 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 active:bg-blue-800 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600 disabled:hover:shadow-none disabled:active:scale-100"
|
||||||
aria-label={uploadProgress > 0 ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : 'Upload pattern to machine'}
|
aria-label={uploadProgress > 0 ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : 'Upload pattern to machine'}
|
||||||
>
|
>
|
||||||
{uploadProgress > 0
|
{uploadProgress > 0 ? (
|
||||||
? `Uploading... ${uploadProgress.toFixed(0)}%`
|
<>
|
||||||
: 'Upload to Machine'}
|
<svg className="w-5 h-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>Uploading... {uploadProgress.toFixed(0)}%</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowUpTrayIcon className="w-5 h-5" />
|
||||||
|
<span>Upload to Machine</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{pesData && !canUploadPattern(machineStatus) && (
|
{pesData && !canUploadPattern(machineStatus) && (
|
||||||
<div className="bg-yellow-100 text-yellow-800 px-4 py-3 rounded border border-yellow-200 my-4 font-medium">
|
<div className="bg-yellow-100 text-yellow-800 px-4 py-3 rounded-lg border border-yellow-200 my-4 font-medium animate-fadeIn">
|
||||||
Cannot upload pattern while machine is {getMachineStateCategory(machineStatus)}
|
Cannot upload pattern while machine is {getMachineStateCategory(machineStatus)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{uploadProgress > 0 && uploadProgress < 100 && (
|
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||||
<div className="h-3 bg-gray-300 rounded-md overflow-hidden my-4 shadow-inner">
|
<div className="mt-4 animate-fadeIn">
|
||||||
<div
|
<div className="flex justify-between items-center mb-2">
|
||||||
className="h-full bg-gradient-to-r from-blue-600 to-blue-700 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]"
|
<span className="text-sm font-medium text-gray-700">Uploading to Machine</span>
|
||||||
style={{ width: `${uploadProgress}%` }}
|
<span className="text-sm font-bold text-blue-600">{uploadProgress.toFixed(1)}%</span>
|
||||||
/>
|
</div>
|
||||||
|
<div className="h-3 bg-gray-300 rounded-full overflow-hidden shadow-inner relative">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 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 mt-2 text-center">Please wait while your pattern is being transferred...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{uploadProgress === 100 && (
|
||||||
|
<div className="mt-4 bg-green-50 border border-green-200 px-4 py-3 rounded-lg flex items-center gap-3 animate-fadeIn">
|
||||||
|
<CheckCircleIcon className="w-6 h-6 text-green-600 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-green-900">Upload Complete!</p>
|
||||||
|
<p className="text-xs text-green-700">Pattern successfully transferred to machine</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export function MachineConnection({
|
||||||
const errorInfo = (isConnected && hasError(machineError)) ? getErrorDetails(machineError) : null;
|
const errorInfo = (isConnected && hasError(machineError)) ? getErrorDetails(machineError) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
<div className="bg-white 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">
|
<div className="flex items-center justify-between mb-4 pb-2 border-b-2 border-gray-300">
|
||||||
<h2 className="text-xl font-semibold">Machine Connection</h2>
|
<h2 className="text-xl font-semibold">Machine Connection</h2>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export function NextStepGuide({
|
||||||
// Check if this is informational (like initialization steps) vs a real error
|
// Check if this is informational (like initialization steps) vs a real error
|
||||||
if (errorDetails?.isInformational) {
|
if (errorDetails?.isInformational) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md">
|
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -56,7 +56,7 @@ export function NextStepGuide({
|
||||||
|
|
||||||
// Regular error display for actual errors
|
// Regular error display for actual errors
|
||||||
return (
|
return (
|
||||||
<div className="bg-red-50 border-l-4 border-red-600 p-6 rounded-lg shadow-md">
|
<div className="bg-red-50 border-l-4 border-red-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<ExclamationTriangleIcon className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
|
<ExclamationTriangleIcon className="w-8 h-8 text-red-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -90,7 +90,7 @@ export function NextStepGuide({
|
||||||
// Determine what to show based on current state
|
// Determine what to show based on current state
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md">
|
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -109,7 +109,7 @@ export function NextStepGuide({
|
||||||
|
|
||||||
if (!hasPattern) {
|
if (!hasPattern) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md">
|
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -129,7 +129,7 @@ export function NextStepGuide({
|
||||||
|
|
||||||
if (!patternUploaded) {
|
if (!patternUploaded) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md">
|
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-blue-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -169,7 +169,7 @@ export function NextStepGuide({
|
||||||
|
|
||||||
case MachineStatus.MASK_TRACE_LOCK_WAIT:
|
case MachineStatus.MASK_TRACE_LOCK_WAIT:
|
||||||
return (
|
return (
|
||||||
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md">
|
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -187,7 +187,7 @@ export function NextStepGuide({
|
||||||
|
|
||||||
case MachineStatus.MASK_TRACING:
|
case MachineStatus.MASK_TRACING:
|
||||||
return (
|
return (
|
||||||
<div className="bg-cyan-50 border-l-4 border-cyan-600 p-6 rounded-lg shadow-md">
|
<div className="bg-cyan-50 border-l-4 border-cyan-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-cyan-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-cyan-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -206,7 +206,7 @@ export function NextStepGuide({
|
||||||
case MachineStatus.MASK_TRACE_COMPLETE:
|
case MachineStatus.MASK_TRACE_COMPLETE:
|
||||||
case MachineStatus.SEWING_WAIT:
|
case MachineStatus.SEWING_WAIT:
|
||||||
return (
|
return (
|
||||||
<div className="bg-green-50 border-l-4 border-green-600 p-6 rounded-lg shadow-md">
|
<div className="bg-green-50 border-l-4 border-green-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-green-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-green-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -224,7 +224,7 @@ export function NextStepGuide({
|
||||||
|
|
||||||
case MachineStatus.SEWING:
|
case MachineStatus.SEWING:
|
||||||
return (
|
return (
|
||||||
<div className="bg-cyan-50 border-l-4 border-cyan-600 p-6 rounded-lg shadow-md">
|
<div className="bg-cyan-50 border-l-4 border-cyan-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-cyan-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-cyan-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -242,7 +242,7 @@ export function NextStepGuide({
|
||||||
|
|
||||||
case MachineStatus.COLOR_CHANGE_WAIT:
|
case MachineStatus.COLOR_CHANGE_WAIT:
|
||||||
return (
|
return (
|
||||||
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md">
|
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -262,7 +262,7 @@ export function NextStepGuide({
|
||||||
case MachineStatus.STOP:
|
case MachineStatus.STOP:
|
||||||
case MachineStatus.SEWING_INTERRUPTION:
|
case MachineStatus.SEWING_INTERRUPTION:
|
||||||
return (
|
return (
|
||||||
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md">
|
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-yellow-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
@ -280,7 +280,7 @@ export function NextStepGuide({
|
||||||
|
|
||||||
case MachineStatus.SEWING_COMPLETE:
|
case MachineStatus.SEWING_COMPLETE:
|
||||||
return (
|
return (
|
||||||
<div className="bg-green-50 border-l-4 border-green-600 p-6 rounded-lg shadow-md">
|
<div className="bg-green-50 border-l-4 border-green-600 p-6 rounded-lg shadow-md animate-fadeIn">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<InformationCircleIcon className="w-8 h-8 text-green-600 flex-shrink-0 mt-1" />
|
<InformationCircleIcon className="w-8 h-8 text-green-600 flex-shrink-0 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|
|
||||||
|
|
@ -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 } from '@heroicons/react/24/solid';
|
import { PlusIcon, MinusIcon, ArrowPathIcon, LockClosedIcon } 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';
|
||||||
|
|
@ -13,9 +13,10 @@ interface PatternCanvasProps {
|
||||||
machineInfo: MachineInfo | null;
|
machineInfo: MachineInfo | null;
|
||||||
initialPatternOffset?: { x: number; y: number };
|
initialPatternOffset?: { x: number; y: number };
|
||||||
onPatternOffsetChange?: (offsetX: number, offsetY: number) => void;
|
onPatternOffsetChange?: (offsetX: number, offsetY: number) => void;
|
||||||
|
patternUploaded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPatternOffset, onPatternOffsetChange }: PatternCanvasProps) {
|
export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPatternOffset, onPatternOffsetChange, patternUploaded = false }: PatternCanvasProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const stageRef = useRef<Konva.Stage | null>(null);
|
const stageRef = useRef<Konva.Stage | null>(null);
|
||||||
|
|
||||||
|
|
@ -169,7 +170,7 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
||||||
}, [onPatternOffsetChange]);
|
}, [onPatternOffsetChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
<div className="bg-white 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">Pattern Preview</h2>
|
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Pattern Preview</h2>
|
||||||
<div className="relative w-full h-[600px] border border-gray-300 rounded bg-gray-50 overflow-hidden" ref={containerRef}>
|
<div className="relative w-full h-[600px] border border-gray-300 rounded bg-gray-50 overflow-hidden" ref={containerRef}>
|
||||||
{containerSize.width > 0 && (
|
{containerSize.width > 0 && (
|
||||||
|
|
@ -219,17 +220,17 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
||||||
{pesData && (
|
{pesData && (
|
||||||
<Group
|
<Group
|
||||||
name="pattern-group"
|
name="pattern-group"
|
||||||
draggable
|
draggable={!patternUploaded}
|
||||||
x={patternOffset.x}
|
x={patternOffset.x}
|
||||||
y={patternOffset.y}
|
y={patternOffset.y}
|
||||||
onDragEnd={handlePatternDragEnd}
|
onDragEnd={handlePatternDragEnd}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (stage) stage.container().style.cursor = 'move';
|
if (stage && !patternUploaded) stage.container().style.cursor = 'move';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (stage) stage.container().style.cursor = 'grab';
|
if (stage && !patternUploaded) stage.container().style.cursor = 'grab';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stitches
|
<Stitches
|
||||||
|
|
@ -287,13 +288,23 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pattern Offset Indicator */}
|
{/* Pattern Offset Indicator */}
|
||||||
<div className="absolute bottom-20 right-5 bg-white/95 backdrop-blur-sm p-2.5 px-3.5 rounded-lg shadow-lg z-[11] min-w-[180px]">
|
<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="text-[11px] font-semibold text-gray-600 uppercase tracking-wider mb-1">Pattern Position:</div>
|
patternUploaded ? 'bg-amber-50/95 border-2 border-amber-300' : 'bg-white/95'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<div className="text-[11px] font-semibold text-gray-600 uppercase tracking-wider">Pattern Position:</div>
|
||||||
|
{patternUploaded && (
|
||||||
|
<div className="flex items-center gap-1 text-amber-600">
|
||||||
|
<LockClosedIcon className="w-3.5 h-3.5" />
|
||||||
|
<span className="text-[10px] font-bold">LOCKED</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="text-[13px] font-semibold text-blue-600 mb-1">
|
<div className="text-[13px] font-semibold text-blue-600 mb-1">
|
||||||
X: {(patternOffset.x / 10).toFixed(1)}mm, Y: {(patternOffset.y / 10).toFixed(1)}mm
|
X: {(patternOffset.x / 10).toFixed(1)}mm, Y: {(patternOffset.y / 10).toFixed(1)}mm
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-gray-600 italic">
|
<div className="text-[10px] text-gray-600 italic">
|
||||||
Drag pattern to move • Drag background to pan
|
{patternUploaded ? 'Pattern locked • Drag background to pan' : 'Drag pattern to move • Drag background to pan'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ export function ProgressMonitor({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
<div className="bg-white 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">Sewing Progress</h2>
|
<h2 className="text-lg font-semibold mb-3 pb-2 border-b border-gray-300">Sewing Progress</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|
@ -258,12 +258,12 @@ export function ProgressMonitor({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`p-3 rounded-lg border-2 transition-all ${
|
className={`p-3 rounded-lg border-2 transition-all duration-300 ${
|
||||||
isCompleted
|
isCompleted
|
||||||
? 'border-green-600 bg-green-50'
|
? 'border-green-600 bg-green-50 hover:bg-green-100'
|
||||||
: isCurrent
|
: isCurrent
|
||||||
? 'border-blue-600 bg-blue-50 shadow-lg shadow-blue-600/20'
|
? 'border-blue-600 bg-blue-50 shadow-lg shadow-blue-600/20 animate-pulseGlow'
|
||||||
: 'border-gray-200 bg-gray-50 opacity-70'
|
: 'border-gray-200 bg-gray-50 opacity-70 hover:opacity-90'
|
||||||
}`}
|
}`}
|
||||||
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'}`}
|
||||||
|
|
|
||||||
89
src/components/SkeletonLoader.tsx
Normal file
89
src/components/SkeletonLoader.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
interface SkeletonLoaderProps {
|
||||||
|
className?: string;
|
||||||
|
variant?: 'text' | 'rect' | 'circle';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkeletonLoader({ className = '', variant = 'rect' }: SkeletonLoaderProps) {
|
||||||
|
const baseClasses = 'animate-pulse bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200 bg-[length:200%_100%]';
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
text: 'h-4 rounded',
|
||||||
|
rect: 'rounded-lg',
|
||||||
|
circle: 'rounded-full'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${baseClasses} ${variantClasses[variant]} ${className}`} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PatternCanvasSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||||
|
<div className="flex items-center justify-between mb-4 pb-2 border-b-2 border-gray-300">
|
||||||
|
<SkeletonLoader className="h-7 w-40" variant="text" />
|
||||||
|
</div>
|
||||||
|
<div className="relative w-full h-[600px] border border-gray-300 rounded bg-gray-50 overflow-hidden">
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<div className="relative w-24 h-24 mx-auto">
|
||||||
|
<SkeletonLoader className="w-24 h-24" variant="circle" />
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<svg className="w-12 h-12 text-gray-400 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<SkeletonLoader className="h-5 w-48 mx-auto" variant="text" />
|
||||||
|
<SkeletonLoader className="h-4 w-64 mx-auto" variant="text" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PatternInfoSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<SkeletonLoader className="h-6 w-40 mb-4" variant="text" />
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg space-y-3">
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<div key={i} className="flex justify-between">
|
||||||
|
<SkeletonLoader className="h-4 w-24" variant="text" />
|
||||||
|
<SkeletonLoader className="h-4 w-32" variant="text" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MachineConnectionSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||||
|
<div className="flex items-center justify-between mb-4 pb-2 border-b-2 border-gray-300">
|
||||||
|
<SkeletonLoader className="h-7 w-48" variant="text" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<SkeletonLoader className="h-4 w-16" variant="text" />
|
||||||
|
<SkeletonLoader className="h-8 w-32 rounded-lg" />
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<SkeletonLoader className="h-4 w-20" variant="text" />
|
||||||
|
<SkeletonLoader className="h-4 w-24" variant="text" />
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<SkeletonLoader className="h-4 w-24" variant="text" />
|
||||||
|
<SkeletonLoader className="h-4 w-32" variant="text" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -277,10 +277,30 @@ export function useBrotherMachine() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// Delete pattern from cache to prevent auto-resume
|
||||||
|
try {
|
||||||
|
const machineUuid = await service.getPatternUUID();
|
||||||
|
if (machineUuid) {
|
||||||
|
const uuidStr = uuidToString(machineUuid);
|
||||||
|
PatternCacheService.deletePattern(uuidStr);
|
||||||
|
console.log("[Cache] Deleted pattern with UUID:", uuidStr);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[Cache] Failed to get UUID for cache deletion:", err);
|
||||||
|
}
|
||||||
|
|
||||||
await service.deletePattern();
|
await service.deletePattern();
|
||||||
|
|
||||||
|
// Clear machine-related state but keep pattern data in UI for re-editing
|
||||||
setPatternInfo(null);
|
setPatternInfo(null);
|
||||||
setSewingProgress(null);
|
setSewingProgress(null);
|
||||||
setUploadProgress(0); // Reset upload progress to allow new uploads
|
setUploadProgress(0); // Reset upload progress to allow new uploads
|
||||||
|
setResumeAvailable(false);
|
||||||
|
setResumeFileName(null);
|
||||||
|
// NOTE: We intentionally DON'T clear setResumedPattern(null)
|
||||||
|
// so the pattern remains visible in the canvas for re-editing
|
||||||
|
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to delete pattern");
|
setError(err instanceof Error ? err.message : "Failed to delete pattern");
|
||||||
|
|
@ -314,7 +334,7 @@ export function useBrotherMachine() {
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
|
|
||||||
// Also refresh progress during sewing
|
// Refresh progress during sewing
|
||||||
if (machineStatus === MachineStatus.SEWING) {
|
if (machineStatus === MachineStatus.SEWING) {
|
||||||
await refreshProgress();
|
await refreshProgress();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue