Implement comprehensive dark mode support and improve pattern visibility

Dark Mode Implementation:
- Add Tailwind config with darkMode: 'media' for system preference detection
- Update all 9 components with 200+ dark mode variants
- Semantic color backgrounds with semi-transparent overlays in dark mode
- Proper text contrast (gray-900/gray-100) for readability
- Enhanced borders, shadows, and focus rings for dark backgrounds

Component Dark Mode Updates:
- App.tsx: Header gradient, error banners, empty states
- MachineConnection: Status badges with proper dark variants for all states
- FileUpload: Pattern info cards, thread swatches, upload progress
- PatternCanvas: Canvas background, overlays, zoom controls
- ProgressMonitor: Color blocks, progress bars, state indicators with colored icons
- NextStepGuide: All status boxes (blue/yellow/cyan/green/red)
- WorkflowStepper: Progress indicators and step states
- ConfirmDialog: Modal overlays and dialog backgrounds
- KonvaComponents: Grid lines and origin markers

Pattern Visibility Improvements:
- Pattern shows full opacity (1.0) when unlocked for easy positioning
- Pattern shows reduced opacity (0.75) for unstitched areas when locked/uploading
- Helps distinguish completed vs pending stitches during sewing
- Pattern locks during upload to prevent accidental repositioning
- Canvas dragging disabled when pattern is uploading or uploaded

Status Indicator Enhancements:
- Machine status badges: All states (idle/active/waiting/complete/error) have dark variants
- Progress monitor state icons: Colored icons (blue/yellow/green/red) in both modes
- Color blocks: Proper backgrounds and borders for completed/current/pending states
- All semantic colors maintain visibility and meaning in dark mode

Canvas Lock Behavior:
- Pattern locked during upload (uploadProgress > 0 && < 100)
- Pattern locked after upload (patternUploaded = true)
- Lock indicator shows amber background with lock icon
- Cursor changes prevent confusion about draggability
- Full opacity during positioning, transparency during progress tracking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik 2025-12-06 20:12:46 +01:00
parent c5ec118b95
commit 2d33eb40ab
10 changed files with 272 additions and 249 deletions

View file

@ -87,8 +87,8 @@ function App() {
}, [machine.machineStatus, machine.patternInfo, machine.isConnected]);
return (
<div className="min-h-screen flex flex-col bg-gray-50">
<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="min-h-screen flex flex-col bg-gray-50 dark:bg-gray-900">
<header className="bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 dark:from-blue-700 dark:via-blue-800 dark:to-blue-900 px-8 py-3 shadow-lg border-b-2 border-blue-900/20 dark:border-blue-800/30">
<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>
@ -109,9 +109,9 @@ function App() {
<div className="flex-1 p-6 max-w-[1600px] w-full mx-auto">
{/* Global errors */}
{machine.error && (
<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">
<div className="bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-200 px-6 py-4 rounded-lg border-l-4 border-red-600 dark:border-red-500 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn">
<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">
<svg className="w-5 h-5 text-red-600 dark:text-red-400 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>
@ -121,9 +121,9 @@ function App() {
</div>
)}
{pyodideError && (
<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">
<div className="bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-200 px-6 py-4 rounded-lg border-l-4 border-red-600 dark:border-red-500 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn">
<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">
<svg className="w-5 h-5 text-red-600 dark:text-red-400 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>
@ -133,9 +133,9 @@ function App() {
</div>
)}
{!pyodideReady && !pyodideError && (
<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">
<div className="bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-200 px-6 py-4 rounded-lg border-l-4 border-blue-600 dark:border-blue-500 mb-6 shadow-md animate-fadeIn">
<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">
<svg className="w-5 h-5 animate-spin text-blue-600 dark:text-blue-400" 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>
@ -199,44 +199,45 @@ function App() {
initialPatternOffset={patternOffset}
onPatternOffsetChange={handlePatternOffsetChange}
patternUploaded={patternUploaded}
isUploading={machine.uploadProgress > 0 && machine.uploadProgress < 100}
/>
) : (
<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>
<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="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md animate-fadeIn">
<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-center justify-center h-[600px] bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600 relative overflow-hidden">
{/* Decorative background pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-10 left-10 w-32 h-32 border-4 border-gray-400 rounded-full"></div>
<div className="absolute bottom-10 right-10 w-40 h-40 border-4 border-gray-400 rounded-full"></div>
<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>
<div className="absolute inset-0 opacity-5 dark:opacity-10">
<div className="absolute top-10 left-10 w-32 h-32 border-4 border-gray-400 dark:border-gray-500 rounded-full"></div>
<div className="absolute bottom-10 right-10 w-40 h-40 border-4 border-gray-400 dark:border-gray-500 rounded-full"></div>
<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 dark:border-gray-500 rounded-full"></div>
</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">
<svg className="w-28 h-28 mx-auto text-gray-300 dark:text-gray-600" 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">
<div className="absolute -top-2 -right-2 w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" 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">
<h3 className="text-gray-700 dark:text-gray-200 text-xl font-semibold mb-2">No Pattern Loaded</h3>
<p className="text-gray-500 dark:text-gray-400 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 justify-center gap-6 text-xs text-gray-400 dark:text-gray-500">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
<div className="w-2 h-2 bg-blue-400 dark:bg-blue-500 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>
<div className="w-2 h-2 bg-green-400 dark:bg-green-500 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>
<div className="w-2 h-2 bg-purple-400 dark:bg-purple-500 rounded-full"></div>
<span>Real-time Preview</span>
</div>
</div>

View file

@ -38,24 +38,24 @@ export function ConfirmDialog({
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[1000]" onClick={onCancel}>
<div className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-[1000]" onClick={onCancel}>
<div
className={`bg-white rounded-lg shadow-2xl max-w-lg w-[90%] m-4 ${variant === 'danger' ? 'border-t-4 border-red-600' : 'border-t-4 border-yellow-500'}`}
className={`bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-lg w-[90%] m-4 ${variant === 'danger' ? 'border-t-4 border-red-600 dark:border-red-500' : 'border-t-4 border-yellow-500 dark:border-yellow-600'}`}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-labelledby="dialog-title"
aria-describedby="dialog-message"
>
<div className="p-6 border-b border-gray-300">
<h3 id="dialog-title" className="m-0 text-xl font-semibold">{title}</h3>
<div className="p-6 border-b border-gray-300 dark:border-gray-600">
<h3 id="dialog-title" className="m-0 text-xl font-semibold dark:text-white">{title}</h3>
</div>
<div className="p-6">
<p id="dialog-message" className="m-0 leading-relaxed text-gray-900">{message}</p>
<p id="dialog-message" className="m-0 leading-relaxed text-gray-900 dark:text-gray-100">{message}</p>
</div>
<div className="p-4 px-6 flex gap-3 justify-end border-t border-gray-300">
<div className="p-4 px-6 flex gap-3 justify-end border-t border-gray-300 dark:border-gray-600">
<button
onClick={onCancel}
className="px-6 py-2.5 bg-gray-600 text-white rounded-lg font-semibold text-sm hover:bg-gray-700 active:bg-gray-800 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-300 focus:ring-offset-2"
className="px-6 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"
autoFocus
aria-label="Cancel action"
>
@ -65,8 +65,8 @@ export function ConfirmDialog({
onClick={onConfirm}
className={
variant === 'danger'
? 'px-6 py-2.5 bg-red-600 text-white rounded-lg font-semibold text-sm hover:bg-red-700 active:bg-red-800 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-red-300 focus:ring-offset-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'
? 'px-6 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'
: 'px-6 py-2.5 bg-blue-600 dark:bg-blue-700 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900'
}
aria-label={`Confirm: ${confirmText}`}
>

View file

@ -76,21 +76,21 @@ export function FileUpload({
}, [pesData, displayFileName, onUpload, patternOffset]);
return (
<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>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300 dark:border-gray-600 dark:text-white">Pattern File</h2>
<div>
{resumeAvailable && resumeFileName && (
<div className="bg-green-50 border border-green-200 px-4 py-3 rounded mb-4">
<p className="text-sm text-green-800">
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 px-4 py-3 rounded mb-4">
<p className="text-sm text-green-800 dark:text-green-200">
<strong>Loaded cached pattern:</strong> "{resumeFileName}"
</p>
</div>
)}
{patternUploaded && (
<div className="bg-blue-50 border border-blue-200 px-4 py-3 rounded mb-4">
<p className="text-sm text-blue-800">
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 px-4 py-3 rounded mb-4">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>Pattern uploaded successfully!</strong> The pattern is now locked and cannot be changed.
To upload a different pattern, you must first complete or delete the current one.
</p>
@ -107,10 +107,10 @@ export function FileUpload({
/>
<label
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 ${
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 ${
!pyodideReady || isLoading || patternUploaded
? 'opacity-50 cursor-not-allowed grayscale-[0.3]'
: 'cursor-pointer hover:bg-gray-700 hover:shadow-lg active:scale-[0.98]'
: 'cursor-pointer hover:bg-gray-700 dark:hover:bg-gray-600 hover:shadow-lg active:scale-[0.98]'
}`}
>
{isLoading ? (
@ -143,36 +143,36 @@ export function FileUpload({
{!isLoading && pesData && (
<div className="mt-4 animate-fadeIn">
<h3 className="text-base font-semibold my-4">Pattern Information</h3>
<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">
<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">File Name:</span>
<span className="font-semibold text-gray-900 text-right max-w-[200px] truncate" title={displayFileName}>
<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">Pattern Size:</span>
<span className="font-semibold text-gray-900">
<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">Thread Colors:</span>
<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">{pesData.colorCount}</span>
<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 shadow-sm"
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 border border-gray-400 flex items-center justify-center text-[8px] font-bold text-gray-600">
<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>
)}
@ -180,8 +180,8 @@ export function FileUpload({
</div>
</div>
<div className="flex justify-between items-center">
<span className="font-medium text-gray-700">Total Stitches:</span>
<span className="font-semibold text-gray-900">{pesData.stitchCount.toLocaleString()}</span>
<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>
@ -191,7 +191,7 @@ export function FileUpload({
<button
onClick={handleUpload}
disabled={!isConnected || uploadProgress > 0}
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"
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"
aria-label={uploadProgress > 0 ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : 'Upload pattern to machine'}
>
{uploadProgress > 0 ? (
@ -212,7 +212,7 @@ export function FileUpload({
)}
{pesData && !canUploadPattern(machineStatus) && (
<div className="bg-yellow-100 text-yellow-800 px-4 py-3 rounded-lg border border-yellow-200 my-4 font-medium animate-fadeIn">
<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>
)}
@ -220,25 +220,25 @@ export function FileUpload({
{uploadProgress > 0 && 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">Uploading to Machine</span>
<span className="text-sm font-bold text-blue-600">{uploadProgress.toFixed(1)}%</span>
<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.toFixed(1)}%</span>
</div>
<div className="h-3 bg-gray-300 rounded-full overflow-hidden shadow-inner relative">
<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 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"
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 mt-2 text-center">Please wait while your pattern is being transferred...</p>
<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>
)}
{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 className="mt-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 px-4 py-3 rounded-lg flex items-center gap-3 animate-fadeIn">
<CheckCircleIcon className="w-6 h-6 text-green-600 dark:text-green-400 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>
<p className="text-sm font-semibold text-green-900 dark:text-green-200">Upload Complete!</p>
<p className="text-xs text-green-700 dark:text-green-300">Pattern successfully transferred to machine</p>
</div>
</div>
)}

View file

@ -35,13 +35,17 @@ export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
return { verticalLines, horizontalLines };
}, [gridSize, bounds, machineInfo]);
// Detect dark mode
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const gridColor = isDarkMode ? '#404040' : '#e0e0e0';
return (
<Group name="grid">
{lines.verticalLines.map((points, i) => (
<Line
key={`v-${i}`}
points={points}
stroke="#e0e0e0"
stroke={gridColor}
strokeWidth={1}
/>
))}
@ -49,7 +53,7 @@ export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
<Line
key={`h-${i}`}
points={points}
stroke="#e0e0e0"
stroke={gridColor}
strokeWidth={1}
/>
))}
@ -60,10 +64,13 @@ export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
Grid.displayName = 'Grid';
export const Origin = memo(() => {
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const originColor = isDarkMode ? '#999' : '#888';
return (
<Group name="origin">
<Line points={[-10, 0, 10, 0]} stroke="#888" strokeWidth={2} />
<Line points={[0, -10, 0, 10]} stroke="#888" strokeWidth={2} />
<Line points={[-10, 0, 10, 0]} stroke={originColor} strokeWidth={2} />
<Line points={[0, -10, 0, 10]} stroke={originColor} strokeWidth={2} />
</Group>
);
});
@ -133,9 +140,10 @@ interface StitchesProps {
stitches: number[][];
pesData: PesPatternData;
currentStitchIndex: number;
showProgress?: boolean;
}
export const Stitches = memo(({ stitches, pesData, currentStitchIndex }: StitchesProps) => {
export const Stitches = memo(({ stitches, pesData, currentStitchIndex, showProgress = false }: StitchesProps) => {
const stitchGroups = useMemo(() => {
interface StitchGroup {
color: string;
@ -174,7 +182,7 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex }: Stitche
}
return groups;
}, [stitches, pesData, currentStitchIndex]);
}, [stitches, pesData, currentStitchIndex, showProgress]);
return (
<Group name="stitches">
@ -187,7 +195,7 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex }: Stitche
lineCap="round"
lineJoin="round"
dash={group.isJump ? [3, 3] : undefined}
opacity={group.isJump ? 1 : (group.completed ? 1.0 : 0.3)}
opacity={group.isJump ? 1 : (showProgress && !group.completed ? 0.75 : 1.0)}
/>
))}
</Group>

View file

@ -46,33 +46,33 @@ export function MachineConnection({
const stateVisual = getStateVisualInfo(machineStatus);
const statusBadgeColors = {
idle: 'bg-cyan-100 text-cyan-800 border-cyan-200',
info: 'bg-cyan-100 text-cyan-800 border-cyan-200',
active: 'bg-yellow-100 text-yellow-800 border-yellow-200',
waiting: 'bg-yellow-100 text-yellow-800 border-yellow-200',
warning: 'bg-yellow-100 text-yellow-800 border-yellow-200',
complete: 'bg-green-100 text-green-800 border-green-200',
success: 'bg-green-100 text-green-800 border-green-200',
interrupted: 'bg-red-100 text-red-800 border-red-200',
error: 'bg-red-100 text-red-800 border-red-200',
danger: 'bg-red-100 text-red-800 border-red-200',
idle: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300 border-cyan-200 dark:border-cyan-700',
info: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300 border-cyan-200 dark:border-cyan-700',
active: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 border-yellow-200 dark:border-yellow-700',
waiting: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 border-yellow-200 dark:border-yellow-700',
warning: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 border-yellow-200 dark:border-yellow-700',
complete: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-700',
success: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-700',
interrupted: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-700',
error: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-700',
danger: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-700',
};
// Only show error info when connected AND there's an actual error
const errorInfo = (isConnected && hasError(machineError)) ? getErrorDetails(machineError) : null;
return (
<div className="bg-white 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">
<h2 className="text-xl font-semibold">Machine Connection</h2>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
<div className="flex items-center justify-between mb-4 pb-2 border-b-2 border-gray-300 dark:border-gray-600">
<h2 className="text-xl font-semibold dark:text-white">Machine Connection</h2>
<div className="flex items-center gap-3">
{isConnected && isPolling && (
<span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" title="Auto-refreshing" aria-label="Auto-refreshing machine status"></span>
<span className="w-2 h-2 bg-blue-500 dark:bg-blue-400 rounded-full animate-pulse" title="Auto-refreshing" aria-label="Auto-refreshing machine status"></span>
)}
{isConnected && (
<button
onClick={handleDisconnectClick}
className="px-3 py-1.5 bg-gray-600 text-white rounded-lg font-semibold text-xs hover:bg-gray-700 active:bg-gray-800 hover:shadow-md active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-300 focus:ring-offset-2"
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
@ -85,7 +85,7 @@ export function MachineConnection({
<div className="flex gap-3 mt-4 flex-wrap">
<button
onClick={onConnect}
className="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"
className="px-6 py-2.5 bg-blue-600 dark:bg-blue-700 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
aria-label="Connect to embroidery machine"
>
Connect to Machine
@ -97,22 +97,22 @@ export function MachineConnection({
{errorInfo && (
errorInfo.isInformational ? (
// Informational messages (like initialization steps)
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 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">
<InformationCircleIcon className="w-5 h-5 text-blue-600 flex-shrink-0" />
<InformationCircleIcon className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-semibold text-blue-900 text-sm">{errorInfo.title}</div>
<div className="font-semibold text-blue-900 dark:text-blue-200 text-sm">{errorInfo.title}</div>
</div>
</div>
</div>
) : (
// Regular errors shown as errors
<div className="mb-4 p-4 bg-red-50 border border-red-200 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">
<span className="text-red-600 text-lg flex-shrink-0"></span>
<span className="text-red-600 dark:text-red-400 text-lg flex-shrink-0"></span>
<div className="flex-1 min-w-0">
<div className="font-semibold text-red-900 text-sm mb-1">{errorInfo.title}</div>
<div className="text-xs text-red-700 font-mono">
<div className="font-semibold text-red-900 dark:text-red-200 text-sm mb-1">{errorInfo.title}</div>
<div className="text-xs text-red-700 dark:text-red-300 font-mono">
Error Code: 0x{machineError.toString(16).toUpperCase().padStart(2, '0')}
</div>
</div>
@ -124,7 +124,7 @@ export function MachineConnection({
{/* Machine Status */}
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-600">Status:</span>
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Status:</span>
<span className={`flex items-center gap-2 px-3 py-1.5 rounded-lg font-semibold text-sm ${statusBadgeColors[stateVisual.color as keyof typeof statusBadgeColors] || statusBadgeColors.info}`}>
<span className="text-base leading-none">{stateVisual.icon}</span>
<span>{machineStatusName}</span>
@ -134,14 +134,14 @@ export function MachineConnection({
{/* Machine Info */}
{machineInfo && (
<div className="bg-gray-50 p-4 rounded-lg space-y-2 mb-4">
<div className="bg-gray-50 dark:bg-gray-700/50 p-4 rounded-lg space-y-2 mb-4">
<div className="flex justify-between text-sm">
<span className="font-medium text-gray-600">Model:</span>
<span className="font-semibold text-gray-900">{machineInfo.modelNumber}</span>
<span className="font-medium text-gray-600 dark:text-gray-400">Model:</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">{machineInfo.modelNumber}</span>
</div>
<div className="flex justify-between text-sm">
<span className="font-medium text-gray-600">Max Area:</span>
<span className="font-semibold text-gray-900">
<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">
{(machineInfo.maxWidth / 10).toFixed(1)} × {(machineInfo.maxHeight / 10).toFixed(1)} mm
</span>
</div>

View file

@ -28,20 +28,20 @@ export function NextStepGuide({
// Check if this is informational (like initialization steps) vs a real error
if (errorDetails?.isInformational) {
return (
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-600 dark:border-blue-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-blue-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 mb-2">
<h3 className="text-lg font-semibold text-blue-900 dark:text-blue-200 mb-2">
{errorDetails.title}
</h3>
<p className="text-blue-800 mb-3">
<p className="text-blue-800 dark:text-blue-300 mb-3">
{errorDetails.description}
</p>
{errorDetails.solutions && errorDetails.solutions.length > 0 && (
<>
<h4 className="font-semibold text-blue-900 mb-2">Steps:</h4>
<ol className="list-decimal list-inside text-sm text-blue-700 space-y-2">
<h4 className="font-semibold text-blue-900 dark:text-blue-200 mb-2">Steps:</h4>
<ol className="list-decimal list-inside text-sm text-blue-700 dark:text-blue-300 space-y-2">
{errorDetails.solutions.map((solution, index) => (
<li key={index} className="pl-2">{solution}</li>
))}
@ -56,20 +56,20 @@ export function NextStepGuide({
// Regular error display for actual errors
return (
<div className="bg-red-50 border-l-4 border-red-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-600 dark:border-red-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-red-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-red-900 mb-2">
<h3 className="text-lg font-semibold text-red-900 dark:text-red-200 mb-2">
{errorDetails?.title || 'Error Occurred'}
</h3>
<p className="text-red-800 mb-3">
<p className="text-red-800 dark:text-red-300 mb-3">
{errorDetails?.description || errorMessage || 'An error occurred. Please check the machine and try again.'}
</p>
{errorDetails?.solutions && errorDetails.solutions.length > 0 && (
<>
<h4 className="font-semibold text-red-900 mb-2">How to Fix:</h4>
<ol className="list-decimal list-inside text-sm text-red-700 space-y-2">
<h4 className="font-semibold text-red-900 dark:text-red-200 mb-2">How to Fix:</h4>
<ol className="list-decimal list-inside text-sm text-red-700 dark:text-red-300 space-y-2">
{errorDetails.solutions.map((solution, index) => (
<li key={index} className="pl-2">{solution}</li>
))}
@ -77,7 +77,7 @@ export function NextStepGuide({
</>
)}
{errorCode !== undefined && (
<p className="text-xs text-red-600 mt-4 font-mono">
<p className="text-xs text-red-600 dark:text-red-400 mt-4 font-mono">
Error Code: 0x{errorCode.toString(16).toUpperCase().padStart(2, '0')}
</p>
)}
@ -90,13 +90,13 @@ export function NextStepGuide({
// Determine what to show based on current state
if (!isConnected) {
return (
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-600 dark:border-blue-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-blue-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 1: Connect to Machine</h3>
<p className="text-blue-800 mb-3">To get started, connect to your Brother embroidery machine via Bluetooth.</p>
<ul className="list-disc list-inside text-sm text-blue-700 space-y-1">
<h3 className="text-lg font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 1: Connect to Machine</h3>
<p className="text-blue-800 dark:text-blue-300 mb-3">To get started, connect to your Brother embroidery machine via Bluetooth.</p>
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
<li>Make sure your machine is powered on</li>
<li>Enable Bluetooth on your machine</li>
<li>Click the "Connect to Machine" button below</li>
@ -109,13 +109,13 @@ export function NextStepGuide({
if (!hasPattern) {
return (
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-600 dark:border-blue-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-blue-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 2: Load Your Pattern</h3>
<p className="text-blue-800 mb-3">Choose a PES embroidery file from your computer to preview and upload.</p>
<ul className="list-disc list-inside text-sm text-blue-700 space-y-1">
<h3 className="text-lg font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 2: Load Your Pattern</h3>
<p className="text-blue-800 dark:text-blue-300 mb-3">Choose a PES embroidery file from your computer to preview and upload.</p>
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
<li>Click "Choose PES File" in the Pattern File section</li>
<li>Select your embroidery design (.pes file)</li>
<li>Review the pattern preview on the right</li>
@ -129,13 +129,13 @@ export function NextStepGuide({
if (!patternUploaded) {
return (
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-600 dark:border-blue-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-blue-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 3: Upload Pattern to Machine</h3>
<p className="text-blue-800 mb-3">Send your pattern to the embroidery machine to prepare for sewing.</p>
<ul className="list-disc list-inside text-sm text-blue-700 space-y-1">
<h3 className="text-lg font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 3: Upload Pattern to Machine</h3>
<p className="text-blue-800 dark:text-blue-300 mb-3">Send your pattern to the embroidery machine to prepare for sewing.</p>
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
<li>Review the pattern preview to ensure it's positioned correctly</li>
<li>Check the pattern size matches your hoop</li>
<li>Click "Upload to Machine" when ready</li>
@ -151,13 +151,13 @@ export function NextStepGuide({
switch (machineStatus) {
case MachineStatus.IDLE:
return (
<div className="bg-blue-50 border-l-4 border-blue-600 p-6 rounded-lg shadow-md">
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-600 dark:border-blue-500 p-6 rounded-lg shadow-md">
<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 dark:text-blue-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 4: Start Mask Trace</h3>
<p className="text-blue-800 mb-3">The mask trace helps the machine understand the pattern boundaries.</p>
<ul className="list-disc list-inside text-sm text-blue-700 space-y-1">
<h3 className="text-lg font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 4: Start Mask Trace</h3>
<p className="text-blue-800 dark:text-blue-300 mb-3">The mask trace helps the machine understand the pattern boundaries.</p>
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
<li>Click "Start Mask Trace" button in the Sewing Progress section</li>
<li>The machine will trace the pattern outline</li>
<li>This ensures the hoop is positioned correctly</li>
@ -169,13 +169,13 @@ export function NextStepGuide({
case MachineStatus.MASK_TRACE_LOCK_WAIT:
return (
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-600 dark:border-yellow-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-yellow-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-yellow-900 mb-2">Machine Action Required</h3>
<p className="text-yellow-800 mb-3">The machine is ready to trace the pattern outline.</p>
<ul className="list-disc list-inside text-sm text-yellow-700 space-y-1">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Machine Action Required</h3>
<p className="text-yellow-800 dark:text-yellow-300 mb-3">The machine is ready to trace the pattern outline.</p>
<ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
<li><strong>Press the button on your machine</strong> to confirm and start the mask trace</li>
<li>Ensure the hoop is properly attached</li>
<li>Make sure the needle area is clear</li>
@ -187,13 +187,13 @@ export function NextStepGuide({
case MachineStatus.MASK_TRACING:
return (
<div className="bg-cyan-50 border-l-4 border-cyan-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-cyan-50 dark:bg-cyan-900/20 border-l-4 border-cyan-600 dark:border-cyan-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-cyan-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-cyan-900 mb-2">Mask Trace In Progress</h3>
<p className="text-cyan-800 mb-3">The machine is tracing the pattern boundary. Please wait...</p>
<ul className="list-disc list-inside text-sm text-cyan-700 space-y-1">
<h3 className="text-lg font-semibold text-cyan-900 dark:text-cyan-200 mb-2">Mask Trace In Progress</h3>
<p className="text-cyan-800 dark:text-cyan-300 mb-3">The machine is tracing the pattern boundary. Please wait...</p>
<ul className="list-disc list-inside text-sm text-cyan-700 dark:text-cyan-300 space-y-1">
<li>Watch the machine trace the outline</li>
<li>Verify the pattern fits within your hoop</li>
<li>Do not interrupt the machine</li>
@ -206,13 +206,13 @@ export function NextStepGuide({
case MachineStatus.MASK_TRACE_COMPLETE:
case MachineStatus.SEWING_WAIT:
return (
<div className="bg-green-50 border-l-4 border-green-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-green-50 dark:bg-green-900/20 border-l-4 border-green-600 dark:border-green-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-green-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-green-900 mb-2">Step 5: Ready to Sew!</h3>
<p className="text-green-800 mb-3">The machine is ready to begin embroidering your pattern.</p>
<ul className="list-disc list-inside text-sm text-green-700 space-y-1">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-200 mb-2">Step 5: Ready to Sew!</h3>
<p className="text-green-800 dark:text-green-300 mb-3">The machine is ready to begin embroidering your pattern.</p>
<ul className="list-disc list-inside text-sm text-green-700 dark:text-green-300 space-y-1">
<li>Verify your thread colors are correct</li>
<li>Ensure the fabric is properly hooped</li>
<li>Click "Start Sewing" when ready</li>
@ -224,13 +224,13 @@ export function NextStepGuide({
case MachineStatus.SEWING:
return (
<div className="bg-cyan-50 border-l-4 border-cyan-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-cyan-50 dark:bg-cyan-900/20 border-l-4 border-cyan-600 dark:border-cyan-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-cyan-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-cyan-900 mb-2">Step 6: Sewing In Progress</h3>
<p className="text-cyan-800 mb-3">Your embroidery is being stitched. Monitor the progress below.</p>
<ul className="list-disc list-inside text-sm text-cyan-700 space-y-1">
<h3 className="text-lg font-semibold text-cyan-900 dark:text-cyan-200 mb-2">Step 6: Sewing In Progress</h3>
<p className="text-cyan-800 dark:text-cyan-300 mb-3">Your embroidery is being stitched. Monitor the progress below.</p>
<ul className="list-disc list-inside text-sm text-cyan-700 dark:text-cyan-300 space-y-1">
<li>Watch the progress bar and current stitch count</li>
<li>The machine will pause when a color change is needed</li>
<li>Do not leave the machine unattended</li>
@ -242,13 +242,13 @@ export function NextStepGuide({
case MachineStatus.COLOR_CHANGE_WAIT:
return (
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-600 dark:border-yellow-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-yellow-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-yellow-900 mb-2">Thread Change Required</h3>
<p className="text-yellow-800 mb-3">The machine needs a different thread color to continue.</p>
<ul className="list-disc list-inside text-sm text-yellow-700 space-y-1">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Thread Change Required</h3>
<p className="text-yellow-800 dark:text-yellow-300 mb-3">The machine needs a different thread color to continue.</p>
<ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
<li>Check the color blocks section to see which thread is needed</li>
<li>Change to the correct thread color</li>
<li><strong>Press the button on your machine</strong> to resume sewing</li>
@ -262,13 +262,13 @@ export function NextStepGuide({
case MachineStatus.STOP:
case MachineStatus.SEWING_INTERRUPTION:
return (
<div className="bg-yellow-50 border-l-4 border-yellow-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-600 dark:border-yellow-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-yellow-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-yellow-900 mb-2">Sewing Paused</h3>
<p className="text-yellow-800 mb-3">The embroidery has been paused or interrupted.</p>
<ul className="list-disc list-inside text-sm text-yellow-700 space-y-1">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Sewing Paused</h3>
<p className="text-yellow-800 dark:text-yellow-300 mb-3">The embroidery has been paused or interrupted.</p>
<ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
<li>Check if everything is okay with the machine</li>
<li>Click "Resume Sewing" when ready to continue</li>
<li>The machine will pick up where it left off</li>
@ -280,13 +280,13 @@ export function NextStepGuide({
case MachineStatus.SEWING_COMPLETE:
return (
<div className="bg-green-50 border-l-4 border-green-600 p-6 rounded-lg shadow-md animate-fadeIn">
<div className="bg-green-50 dark:bg-green-900/20 border-l-4 border-green-600 dark:border-green-500 p-6 rounded-lg shadow-md animate-fadeIn">
<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 dark:text-green-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-green-900 mb-2">Step 7: Embroidery Complete!</h3>
<p className="text-green-800 mb-3">Your embroidery is finished. Great work!</p>
<ul className="list-disc list-inside text-sm text-green-700 space-y-1">
<h3 className="text-lg font-semibold text-green-900 dark:text-green-200 mb-2">Step 7: Embroidery Complete!</h3>
<p className="text-green-800 dark:text-green-300 mb-3">Your embroidery is finished. Great work!</p>
<ul className="list-disc list-inside text-sm text-green-700 dark:text-green-300 space-y-1">
<li>Remove the hoop from the machine</li>
<li>Press the Accept button on the machine</li>
<li>Carefully remove your finished embroidery</li>

View file

@ -14,9 +14,10 @@ interface PatternCanvasProps {
initialPatternOffset?: { x: number; y: number };
onPatternOffsetChange?: (offsetX: number, offsetY: number) => void;
patternUploaded?: boolean;
isUploading?: boolean;
}
export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPatternOffset, onPatternOffsetChange, patternUploaded = false }: PatternCanvasProps) {
export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPatternOffset, onPatternOffsetChange, patternUploaded = false, isUploading = false }: PatternCanvasProps) {
const containerRef = useRef<HTMLDivElement>(null);
const stageRef = useRef<Konva.Stage | null>(null);
@ -170,9 +171,9 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
}, [onPatternOffsetChange]);
return (
<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>
<div className="relative w-full h-[600px] border border-gray-300 rounded bg-gray-50 overflow-hidden" ref={containerRef}>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300 dark:border-gray-600 dark:text-white">Pattern Preview</h2>
<div className="relative w-full h-[600px] border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-900 overflow-hidden" ref={containerRef}>
{containerSize.width > 0 && (
<Stage
width={containerSize.width}
@ -220,23 +221,24 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
{pesData && (
<Group
name="pattern-group"
draggable={!patternUploaded}
draggable={!patternUploaded && !isUploading}
x={patternOffset.x}
y={patternOffset.y}
onDragEnd={handlePatternDragEnd}
onMouseEnter={(e) => {
const stage = e.target.getStage();
if (stage && !patternUploaded) stage.container().style.cursor = 'move';
if (stage && !patternUploaded && !isUploading) stage.container().style.cursor = 'move';
}}
onMouseLeave={(e) => {
const stage = e.target.getStage();
if (stage && !patternUploaded) stage.container().style.cursor = 'grab';
if (stage && !patternUploaded && !isUploading) stage.container().style.cursor = 'grab';
}}
>
<Stitches
stitches={pesData.stitches}
pesData={pesData}
currentStitchIndex={sewingProgress?.currentStitch || 0}
showProgress={patternUploaded || isUploading}
/>
<PatternBounds bounds={pesData.bounds} />
</Group>
@ -259,7 +261,7 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
{/* Placeholder overlay when no pattern is loaded */}
{!pesData && (
<div className="flex items-center justify-center h-[600px] text-gray-600 italic">
<div className="flex items-center justify-center h-[600px] text-gray-600 dark:text-gray-400 italic">
Load a PES file to preview the pattern
</div>
)}
@ -268,57 +270,57 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
{pesData && (
<>
{/* Thread Legend Overlay */}
<div className="absolute top-2.5 left-2.5 bg-white/95 backdrop-blur-sm p-3 rounded-lg shadow-lg z-10 max-w-[150px]">
<h4 className="m-0 mb-2 text-[13px] font-semibold text-gray-900 border-b border-gray-300 pb-1.5">Threads</h4>
<div className="absolute top-2.5 left-2.5 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm p-3 rounded-lg shadow-lg z-10 max-w-[150px]">
<h4 className="m-0 mb-2 text-[13px] font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-300 dark:border-gray-600 pb-1.5">Threads</h4>
{pesData.threads.map((thread, index) => (
<div key={index} className="flex items-center gap-2 mb-1.5 last:mb-0">
<div
className="w-5 h-5 rounded border border-black flex-shrink-0"
className="w-5 h-5 rounded border border-black dark:border-gray-300 flex-shrink-0"
style={{ backgroundColor: thread.hex }}
/>
<span className="text-xs text-gray-900">Thread {index + 1}</span>
<span className="text-xs text-gray-900 dark:text-gray-100">Thread {index + 1}</span>
</div>
))}
</div>
{/* Pattern Dimensions Overlay */}
<div className="absolute bottom-[165px] right-5 bg-white/95 backdrop-blur-sm px-4 py-2 rounded-lg shadow-lg z-[11] text-sm font-semibold text-gray-900">
<div className="absolute bottom-[165px] right-5 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm px-4 py-2 rounded-lg shadow-lg z-[11] text-sm font-semibold text-gray-900 dark:text-gray-100">
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
</div>
{/* Pattern Offset Indicator */}
<div className={`absolute bottom-20 right-5 backdrop-blur-sm p-2.5 px-3.5 rounded-lg shadow-lg z-[11] min-w-[180px] transition-colors ${
patternUploaded ? 'bg-amber-50/95 border-2 border-amber-300' : 'bg-white/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'
}`}>
<div className="flex items-center justify-between mb-1">
<div className="text-[11px] font-semibold text-gray-600 uppercase tracking-wider">Pattern Position:</div>
<div className="text-[11px] font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">Pattern Position:</div>
{patternUploaded && (
<div className="flex items-center gap-1 text-amber-600">
<div className="flex items-center gap-1 text-amber-600 dark:text-amber-400">
<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 dark:text-blue-400 mb-1">
X: {(patternOffset.x / 10).toFixed(1)}mm, Y: {(patternOffset.y / 10).toFixed(1)}mm
</div>
<div className="text-[10px] text-gray-600 italic">
<div className="text-[10px] text-gray-600 dark:text-gray-400 italic">
{patternUploaded ? 'Pattern locked • Drag background to pan' : 'Drag pattern to move • Drag background to pan'}
</div>
</div>
{/* Zoom Controls Overlay */}
<div className="absolute bottom-5 right-5 flex gap-2 items-center bg-white/95 backdrop-blur-sm px-3 py-2 rounded-lg shadow-lg z-10">
<button className="w-8 h-8 p-1 border border-gray-300 bg-white rounded cursor-pointer transition-all flex items-center justify-center hover:bg-blue-600 hover:text-white hover:border-blue-600 hover:shadow-md hover:shadow-blue-600/30 disabled:opacity-50 disabled:cursor-not-allowed" onClick={handleZoomIn} title="Zoom In">
<PlusIcon className="w-5 h-5" />
<div className="absolute bottom-5 right-5 flex gap-2 items-center bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm px-3 py-2 rounded-lg shadow-lg z-10">
<button className="w-8 h-8 p-1 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded cursor-pointer transition-all flex items-center justify-center hover:bg-blue-600 hover:text-white hover:border-blue-600 dark:hover:border-blue-600 hover:shadow-md hover:shadow-blue-600/30 disabled:opacity-50 disabled:cursor-not-allowed" onClick={handleZoomIn} title="Zoom In">
<PlusIcon className="w-5 h-5 dark:text-gray-200" />
</button>
<span className="min-w-[50px] text-center text-[13px] font-semibold text-gray-900 select-none">{Math.round(stageScale * 100)}%</span>
<button className="w-8 h-8 p-1 border border-gray-300 bg-white rounded cursor-pointer transition-all flex items-center justify-center hover:bg-blue-600 hover:text-white hover:border-blue-600 hover:shadow-md hover:shadow-blue-600/30 disabled:opacity-50 disabled:cursor-not-allowed" onClick={handleZoomOut} title="Zoom Out">
<MinusIcon className="w-5 h-5" />
<span className="min-w-[50px] text-center text-[13px] font-semibold text-gray-900 dark:text-gray-100 select-none">{Math.round(stageScale * 100)}%</span>
<button className="w-8 h-8 p-1 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded cursor-pointer transition-all flex items-center justify-center hover:bg-blue-600 hover:text-white hover:border-blue-600 dark:hover:border-blue-600 hover:shadow-md hover:shadow-blue-600/30 disabled:opacity-50 disabled:cursor-not-allowed" onClick={handleZoomOut} title="Zoom Out">
<MinusIcon className="w-5 h-5 dark:text-gray-200" />
</button>
<button className="w-8 h-8 p-1 border border-gray-300 bg-white rounded cursor-pointer transition-all flex items-center justify-center hover:bg-blue-600 hover:text-white hover:border-blue-600 hover:shadow-md hover:shadow-blue-600/30 disabled:opacity-50 disabled:cursor-not-allowed ml-1" onClick={handleZoomReset} title="Reset Zoom">
<ArrowPathIcon className="w-5 h-5" />
<button className="w-8 h-8 p-1 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded cursor-pointer transition-all flex items-center justify-center hover:bg-blue-600 hover:text-white hover:border-blue-600 dark:hover:border-blue-600 hover:shadow-md hover:shadow-blue-600/30 disabled:opacity-50 disabled:cursor-not-allowed ml-1" onClick={handleZoomReset} title="Reset Zoom">
<ArrowPathIcon className="w-5 h-5 dark:text-gray-200" />
</button>
</div>
</>

View file

@ -97,41 +97,41 @@ export function ProgressMonitor({
);
const stateIndicatorColors = {
idle: 'bg-blue-50 border-l-blue-600',
info: 'bg-blue-50 border-l-blue-600',
active: 'bg-yellow-50 border-l-yellow-500',
waiting: 'bg-yellow-50 border-l-yellow-500',
warning: 'bg-yellow-50 border-l-yellow-500',
complete: 'bg-green-50 border-l-green-600',
success: 'bg-green-50 border-l-green-600',
interrupted: 'bg-red-50 border-l-red-600',
error: 'bg-red-50 border-l-red-600',
danger: 'bg-red-50 border-l-red-600',
idle: 'bg-blue-50 dark:bg-blue-900/20 border-l-blue-600',
info: 'bg-blue-50 dark:bg-blue-900/20 border-l-blue-600',
active: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
waiting: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
complete: 'bg-green-50 dark:bg-green-900/20 border-l-green-600',
success: 'bg-green-50 dark:bg-green-900/20 border-l-green-600',
interrupted: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
error: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
danger: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
};
return (
<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>
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 animate-fadeIn">
<h2 className="text-lg font-semibold mb-3 pb-2 border-b border-gray-300 dark:border-gray-600 dark:text-white">Sewing Progress</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Left Column - Pattern Info & Progress */}
<div>
{patternInfo && (
<div className="bg-gray-50 p-3 rounded-lg mb-3">
<div className="bg-gray-50 dark:bg-gray-700/50 p-3 rounded-lg mb-3">
<div className="grid grid-cols-3 gap-3 text-sm">
<div>
<span className="text-gray-600 block text-xs">Total Stitches</span>
<span className="font-semibold text-gray-900">{patternInfo.totalStitches.toLocaleString()}</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>
</div>
<div>
<span className="text-gray-600 block text-xs">Est. Time</span>
<span className="font-semibold text-gray-900">
<span className="text-gray-600 dark:text-gray-400 block text-xs">Est. Time</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{Math.floor(patternInfo.totalTime / 60)}:{String(patternInfo.totalTime % 60).padStart(2, '0')}
</span>
</div>
<div>
<span className="text-gray-600 block text-xs">Speed</span>
<span className="font-semibold text-gray-900">{patternInfo.speed} spm</span>
<span className="text-gray-600 dark:text-gray-400 block text-xs">Speed</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">{patternInfo.speed} spm</span>
</div>
</div>
</div>
@ -140,23 +140,23 @@ export function ProgressMonitor({
{sewingProgress && (
<div className="mb-3">
<div className="flex justify-between items-center mb-1.5">
<span className="text-xs font-medium text-gray-600">Progress</span>
<span className="text-xl font-bold text-blue-600">{progressPercent.toFixed(1)}%</span>
<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 rounded-md overflow-hidden shadow-inner relative mb-2">
<div 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]" style={{ width: `${progressPercent}%` }} />
<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>
<div className="bg-gray-50 p-2 rounded-lg grid grid-cols-2 gap-2 text-sm">
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded-lg grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-gray-600 block text-xs">Current Stitch</span>
<span className="font-semibold text-gray-900">
<span className="text-gray-600 dark:text-gray-400 block text-xs">Current Stitch</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{sewingProgress.currentStitch.toLocaleString()} / {patternInfo?.totalStitches.toLocaleString() || 0}
</span>
</div>
<div>
<span className="text-gray-600 block text-xs">Time Elapsed</span>
<span className="font-semibold text-gray-900">
<span className="text-gray-600 dark:text-gray-400 block text-xs">Time Elapsed</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{Math.floor(sewingProgress.currentTime / 60)}:{String(sewingProgress.currentTime % 60).padStart(2, '0')}
</span>
</div>
@ -167,12 +167,12 @@ export function ProgressMonitor({
{/* State Visual Indicator */}
{patternInfo && (() => {
const iconMap = {
ready: <ClockIcon className="w-6 h-6" />,
active: <PlayIcon className="w-6 h-6" />,
waiting: <PauseCircleIcon className="w-6 h-6" />,
complete: <CheckBadgeIcon className="w-6 h-6" />,
interrupted: <PauseCircleIcon className="w-6 h-6" />,
error: <ExclamationCircleIcon className="w-6 h-6" />
ready: <ClockIcon className="w-6 h-6 text-blue-600 dark:text-blue-400" />,
active: <PlayIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />,
waiting: <PauseCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />,
complete: <CheckBadgeIcon className="w-6 h-6 text-green-600 dark:text-green-400" />,
interrupted: <PauseCircleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />,
error: <ExclamationCircleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />
};
return (
@ -181,8 +181,8 @@ export function ProgressMonitor({
{iconMap[stateVisual.iconName]}
</div>
<div className="flex-1">
<div className="font-semibold text-sm">{stateVisual.label}</div>
<div className="text-xs text-gray-600">{stateVisual.description}</div>
<div className="font-semibold text-sm dark:text-gray-100">{stateVisual.label}</div>
<div className="text-xs text-gray-600 dark:text-gray-400">{stateVisual.description}</div>
</div>
</div>
);
@ -194,7 +194,7 @@ export function ProgressMonitor({
{canResumeSewing(machineStatus) && (
<button
onClick={onResumeSewing}
className="flex items-center gap-2 px-4 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"
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" />
@ -206,7 +206,7 @@ export function ProgressMonitor({
{canStartSewing(machineStatus) && !canResumeSewing(machineStatus) && (
<button
onClick={onStartSewing}
className="px-4 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"
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
@ -217,7 +217,7 @@ export function ProgressMonitor({
{canStartMaskTrace(machineStatus) && (
<button
onClick={onStartMaskTrace}
className="px-4 py-2.5 bg-gray-600 text-white rounded-lg font-semibold text-sm hover:bg-gray-700 active:bg-gray-800 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-gray-300 focus:ring-offset-2"
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'}
@ -228,7 +228,7 @@ export function ProgressMonitor({
{patternInfo && canDeletePattern(machineStatus) && (
<button
onClick={onDeletePattern}
className="px-4 py-2.5 bg-red-600 text-white rounded-lg font-semibold text-sm hover:bg-red-700 active:bg-red-800 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-red-300 focus:ring-offset-2 ml-auto"
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"
aria-label="Delete the current pattern from machine"
>
Delete Pattern
@ -241,7 +241,7 @@ export function ProgressMonitor({
<div>
{colorBlocks.length > 0 && (
<div>
<h3 className="text-sm font-semibold mb-2 text-gray-700">Color Blocks</h3>
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Color Blocks</h3>
<div className="flex flex-col gap-2">
{colorBlocks.map((block, index) => {
const isCompleted = currentStitch >= block.endStitch;
@ -260,10 +260,10 @@ export function ProgressMonitor({
key={index}
className={`p-3 rounded-lg border-2 transition-all duration-300 ${
isCompleted
? 'border-green-600 bg-green-50 hover:bg-green-100'
? 'border-green-600 bg-green-50 dark:bg-green-900/20 hover:bg-green-100 dark:hover:bg-green-900/30'
: isCurrent
? 'border-blue-600 bg-blue-50 shadow-lg shadow-blue-600/20 animate-pulseGlow'
: 'border-gray-200 bg-gray-50 opacity-70 hover:opacity-90'
? 'border-blue-600 bg-blue-50 dark:bg-blue-900/20 shadow-lg shadow-blue-600/20 animate-pulseGlow'
: 'border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50 opacity-70 hover:opacity-90'
}`}
role="listitem"
aria-label={`Thread ${block.colorIndex + 1}, ${block.stitchCount} stitches, ${isCompleted ? 'completed' : isCurrent ? 'in progress' : 'pending'}`}
@ -271,7 +271,7 @@ export function ProgressMonitor({
<div className="flex items-center gap-3">
{/* Larger color swatch with better visibility */}
<div
className="w-8 h-8 rounded-lg border-2 border-gray-300 shadow-md flex-shrink-0 ring-2 ring-offset-2 ring-transparent"
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"
style={{
backgroundColor: block.threadHex,
...(isCurrent && { borderColor: '#2563eb', ringColor: '#93c5fd' })
@ -282,10 +282,10 @@ export function ProgressMonitor({
{/* Thread info */}
<div className="flex-1 min-w-0">
<div className="font-semibold text-sm text-gray-900">
<div className="font-semibold text-sm text-gray-900 dark:text-gray-100">
Thread {block.colorIndex + 1}
</div>
<div className="text-xs text-gray-600 mt-0.5">
<div className="text-xs text-gray-600 dark:text-gray-400 mt-0.5">
{block.stitchCount.toLocaleString()} stitches
</div>
</div>
@ -302,9 +302,9 @@ export function ProgressMonitor({
{/* Progress bar for current block */}
{isCurrent && (
<div className="mt-2 h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="mt-2 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-blue-600 transition-all duration-300 rounded-full"
className="h-full bg-blue-600 dark:bg-blue-500 transition-all duration-300 rounded-full"
style={{ width: `${blockProgress}%` }}
role="progressbar"
aria-valuenow={Math.round(blockProgress)}

View file

@ -61,11 +61,11 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
return (
<div className="relative max-w-5xl mx-auto mt-4" role="navigation" aria-label="Workflow progress">
{/* Progress bar background */}
<div className="absolute top-5 left-0 right-0 h-1 bg-blue-400/20 rounded-full" style={{ left: '24px', right: '24px' }} />
<div className="absolute top-5 left-0 right-0 h-1 bg-blue-400/20 dark:bg-blue-600/20 rounded-full" style={{ left: '24px', right: '24px' }} />
{/* Progress bar fill */}
<div
className="absolute top-5 left-0 h-1 bg-gradient-to-r from-green-500 to-blue-500 transition-all duration-500 rounded-full"
className="absolute top-5 left-0 h-1 bg-gradient-to-r from-green-500 to-blue-500 dark:from-green-600 dark:to-blue-600 transition-all duration-500 rounded-full"
style={{
left: '24px',
width: `calc(${((currentStep - 1) / (steps.length - 1)) * 100}% - 24px)`
@ -96,9 +96,9 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
<div
className={`
w-10 h-10 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-300 border-2 shadow-md
${isComplete ? 'bg-green-500 border-green-400 text-white shadow-green-500/30' : ''}
${isCurrent ? 'bg-blue-600 border-blue-500 text-white scale-110 shadow-blue-600/40 ring-2 ring-blue-300 ring-offset-2' : ''}
${isUpcoming ? 'bg-blue-700 border-blue-500/30 text-blue-200/70' : ''}
${isComplete ? 'bg-green-500 dark:bg-green-600 border-green-400 dark:border-green-500 text-white shadow-green-500/30 dark:shadow-green-600/30' : ''}
${isCurrent ? 'bg-blue-600 dark:bg-blue-700 border-blue-500 dark:border-blue-600 text-white scale-110 shadow-blue-600/40 dark:shadow-blue-700/40 ring-2 ring-blue-300 dark:ring-blue-500 ring-offset-2 dark:ring-offset-gray-900' : ''}
${isUpcoming ? 'bg-blue-700 dark:bg-blue-800 border-blue-500/30 dark:border-blue-600/30 text-blue-200/70 dark:text-blue-300/70' : ''}
`}
aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}`}
>
@ -112,7 +112,7 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
{/* Step label */}
<div className="mt-2 text-center">
<div className={`text-xs font-semibold leading-tight ${
isCurrent ? 'text-white' : isComplete ? 'text-green-200' : 'text-blue-300/70'
isCurrent ? 'text-white' : isComplete ? 'text-green-200 dark:text-green-300' : 'text-blue-300/70 dark:text-blue-400/70'
}`}>
{step.label}
</div>

12
tailwind.config.js Normal file
View file

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'media', // Use system preference for dark mode
theme: {
extend: {},
},
plugins: [],
}