From c5ec118b9565a2f72703ca1e02a7a9a05071420c Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sat, 6 Dec 2025 19:28:24 +0100 Subject: [PATCH] Enhance UI/UX with loading states, animations, and pattern lock functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/App.css | 71 +++++++++++++++ src/App.tsx | 84 ++++++++++++++---- src/components/FileUpload.tsx | 127 ++++++++++++++++++++++----- src/components/MachineConnection.tsx | 2 +- src/components/NextStepGuide.tsx | 24 ++--- src/components/PatternCanvas.tsx | 29 ++++-- src/components/ProgressMonitor.tsx | 10 +-- src/components/SkeletonLoader.tsx | 89 +++++++++++++++++++ src/hooks/useBrotherMachine.ts | 22 ++++- 9 files changed, 391 insertions(+), 67 deletions(-) create mode 100644 src/components/SkeletonLoader.tsx diff --git a/src/App.css b/src/App.css index 5eaacb4..88339c3 100644 --- a/src/App.css +++ b/src/App.css @@ -9,3 +9,74 @@ 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; +} diff --git a/src/App.tsx b/src/App.tsx index 3c0a4c5..5a2dff3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -66,7 +66,8 @@ function App() { const handleDeletePattern = useCallback(async () => { await machine.deletePattern(); 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]); // Track pattern uploaded state based on machine status @@ -87,7 +88,7 @@ function App() { return (
-
+

SKiTCH Controller

@@ -108,18 +109,38 @@ function App() {
{/* Global errors */} {machine.error && ( -
- Error: {machine.error} +
+
+ + + +
+ Error: {machine.error} +
+
)} {pyodideError && ( -
- Python Error: {pyodideError} +
+
+ + + +
+ Python Error: {pyodideError} +
+
)} {!pyodideReady && !pyodideError && ( -
- Initializing Python environment... +
+
+ + + + + Initializing Python environment... +
)} @@ -177,17 +198,48 @@ function App() { machineInfo={machine.machineInfo} initialPatternOffset={patternOffset} onPatternOffsetChange={handlePatternOffsetChange} + patternUploaded={patternUploaded} /> ) : ( -
+

Pattern Preview

-
-
- - - -

No Pattern Loaded

-

Connect to your machine and choose a PES file to begin

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

No Pattern Loaded

+

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

+
+
+
+ Drag to Position +
+
+
+ Zoom & Pan +
+
+
+ Real-time Preview +
+
diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index 31818ec..118eee9 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -2,6 +2,8 @@ import { useState, useCallback } from 'react'; import { convertPesToPen, type PesPatternData } from '../utils/pystitchConverter'; import { MachineStatus } from '../types/machine'; import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers'; +import { PatternInfoSkeleton } from './SkeletonLoader'; +import { ArrowUpTrayIcon, CheckCircleIcon } from '@heroicons/react/24/solid'; interface FileUploadProps { isConnected: boolean; @@ -74,7 +76,7 @@ export function FileUpload({ }, [pesData, displayFileName, onUpload, patternOffset]); return ( -
+

Pattern File

@@ -103,30 +105,81 @@ export function FileUpload({ className="hidden" disabled={!pyodideReady || isLoading || patternUploaded} /> -