mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
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:
parent
c5ec118b95
commit
2d33eb40ab
10 changed files with 272 additions and 249 deletions
49
src/App.tsx
49
src/App.tsx
|
|
@ -87,8 +87,8 @@ function App() {
|
||||||
}, [machine.machineStatus, machine.patternInfo, machine.isConnected]);
|
}, [machine.machineStatus, machine.patternInfo, machine.isConnected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col bg-gray-50">
|
<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 px-8 py-3 shadow-lg border-b-2 border-blue-900/20">
|
<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">
|
<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>
|
||||||
|
|
||||||
|
|
@ -109,9 +109,9 @@ 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-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">
|
<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" />
|
<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>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -121,9 +121,9 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{pyodideError && (
|
{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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -133,9 +133,9 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!pyodideReady && !pyodideError && (
|
{!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">
|
<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>
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -199,44 +199,45 @@ function App() {
|
||||||
initialPatternOffset={patternOffset}
|
initialPatternOffset={patternOffset}
|
||||||
onPatternOffsetChange={handlePatternOffsetChange}
|
onPatternOffsetChange={handlePatternOffsetChange}
|
||||||
patternUploaded={patternUploaded}
|
patternUploaded={patternUploaded}
|
||||||
|
isUploading={machine.uploadProgress > 0 && machine.uploadProgress < 100}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md animate-fadeIn">
|
<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">Pattern Preview</h2>
|
<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 rounded-lg border-2 border-dashed border-gray-300 relative overflow-hidden">
|
<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 */}
|
{/* Decorative background pattern */}
|
||||||
<div className="absolute inset-0 opacity-5">
|
<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 rounded-full"></div>
|
<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 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 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>
|
||||||
|
|
||||||
<div className="text-center relative z-10">
|
<div className="text-center relative z-10">
|
||||||
<div className="relative inline-block mb-6">
|
<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" />
|
<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>
|
</svg>
|
||||||
<div className="absolute -top-2 -right-2 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
<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" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-gray-700 text-xl font-semibold mb-2">No Pattern Loaded</h3>
|
<h3 className="text-gray-700 dark:text-gray-200 text-xl font-semibold mb-2">No Pattern Loaded</h3>
|
||||||
<p className="text-gray-500 text-sm mb-4 max-w-sm mx-auto">
|
<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
|
Connect to your machine and choose a PES embroidery file to see your design preview
|
||||||
</p>
|
</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="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>
|
<span>Drag to Position</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<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>
|
<span>Zoom & Pan</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<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>
|
<span>Real-time Preview</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -38,24 +38,24 @@ export function ConfirmDialog({
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
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
|
<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()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-labelledby="dialog-title"
|
aria-labelledby="dialog-title"
|
||||||
aria-describedby="dialog-message"
|
aria-describedby="dialog-message"
|
||||||
>
|
>
|
||||||
<div className="p-6 border-b border-gray-300">
|
<div className="p-6 border-b border-gray-300 dark:border-gray-600">
|
||||||
<h3 id="dialog-title" className="m-0 text-xl font-semibold">{title}</h3>
|
<h3 id="dialog-title" className="m-0 text-xl font-semibold dark:text-white">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6">
|
<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>
|
||||||
<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
|
<button
|
||||||
onClick={onCancel}
|
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
|
autoFocus
|
||||||
aria-label="Cancel action"
|
aria-label="Cancel action"
|
||||||
>
|
>
|
||||||
|
|
@ -65,8 +65,8 @@ export function ConfirmDialog({
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
className={
|
className={
|
||||||
variant === 'danger'
|
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-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 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-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}`}
|
aria-label={`Confirm: ${confirmText}`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -76,21 +76,21 @@ export function FileUpload({
|
||||||
}, [pesData, displayFileName, onUpload, patternOffset]);
|
}, [pesData, displayFileName, onUpload, patternOffset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
|
<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">Pattern File</h2>
|
<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>
|
<div>
|
||||||
{resumeAvailable && resumeFileName && (
|
{resumeAvailable && resumeFileName && (
|
||||||
<div className="bg-green-50 border border-green-200 px-4 py-3 rounded mb-4">
|
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 px-4 py-3 rounded mb-4">
|
||||||
<p className="text-sm text-green-800">
|
<p className="text-sm text-green-800 dark:text-green-200">
|
||||||
<strong>Loaded cached pattern:</strong> "{resumeFileName}"
|
<strong>Loaded cached pattern:</strong> "{resumeFileName}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{patternUploaded && (
|
{patternUploaded && (
|
||||||
<div className="bg-blue-50 border border-blue-200 px-4 py-3 rounded mb-4">
|
<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">
|
<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.
|
<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.
|
To upload a different pattern, you must first complete or delete the current one.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -107,10 +107,10 @@ export function FileUpload({
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="file-input"
|
htmlFor="file-input"
|
||||||
className={`inline-flex items-center gap-2 px-6 py-3 bg-gray-600 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
|
!pyodideReady || isLoading || patternUploaded
|
||||||
? 'opacity-50 cursor-not-allowed grayscale-[0.3]'
|
? '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 ? (
|
{isLoading ? (
|
||||||
|
|
@ -143,36 +143,36 @@ export function FileUpload({
|
||||||
|
|
||||||
{!isLoading && pesData && (
|
{!isLoading && pesData && (
|
||||||
<div className="mt-4 animate-fadeIn">
|
<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 dark:text-white">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">
|
<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">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-medium text-gray-700">File Name:</span>
|
<span className="font-medium text-gray-700 dark:text-gray-300">File Name:</span>
|
||||||
<span className="font-semibold text-gray-900 text-right max-w-[200px] truncate" title={displayFileName}>
|
<span className="font-semibold text-gray-900 dark:text-gray-100 text-right max-w-[200px] truncate" title={displayFileName}>
|
||||||
{displayFileName}
|
{displayFileName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-medium text-gray-700">Pattern Size:</span>
|
<span className="font-medium text-gray-700 dark:text-gray-300">Pattern Size:</span>
|
||||||
<span className="font-semibold text-gray-900">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{((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 items-center">
|
<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">
|
<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">
|
<div className="flex gap-1">
|
||||||
{pesData.threads.slice(0, 5).map((thread, idx) => (
|
{pesData.threads.slice(0, 5).map((thread, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
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 }}
|
style={{ backgroundColor: thread.hex }}
|
||||||
title={`Thread ${idx + 1}: ${thread.hex}`}
|
title={`Thread ${idx + 1}: ${thread.hex}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{pesData.colorCount > 5 && (
|
{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}
|
+{pesData.colorCount - 5}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -180,8 +180,8 @@ export function FileUpload({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="font-medium text-gray-700">Total Stitches:</span>
|
<span className="font-medium text-gray-700 dark:text-gray-300">Total Stitches:</span>
|
||||||
<span className="font-semibold text-gray-900">{pesData.stitchCount.toLocaleString()}</span>
|
<span className="font-semibold text-gray-900 dark:text-gray-100">{pesData.stitchCount.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -191,7 +191,7 @@ export function FileUpload({
|
||||||
<button
|
<button
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={!isConnected || uploadProgress > 0}
|
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'}
|
aria-label={uploadProgress > 0 ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : 'Upload pattern to machine'}
|
||||||
>
|
>
|
||||||
{uploadProgress > 0 ? (
|
{uploadProgress > 0 ? (
|
||||||
|
|
@ -212,7 +212,7 @@ export function FileUpload({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{pesData && !canUploadPattern(machineStatus) && (
|
{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)}
|
Cannot upload pattern while machine is {getMachineStateCategory(machineStatus)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -220,25 +220,25 @@ export function FileUpload({
|
||||||
{uploadProgress > 0 && uploadProgress < 100 && (
|
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||||
<div className="mt-4 animate-fadeIn">
|
<div className="mt-4 animate-fadeIn">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<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-medium text-gray-700 dark:text-gray-300">Uploading to Machine</span>
|
||||||
<span className="text-sm font-bold text-blue-600">{uploadProgress.toFixed(1)}%</span>
|
<span className="text-sm font-bold text-blue-600 dark:text-blue-400">{uploadProgress.toFixed(1)}%</span>
|
||||||
</div>
|
</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
|
<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}%` }}
|
style={{ width: `${uploadProgress}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{uploadProgress === 100 && (
|
{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">
|
<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 flex-shrink-0" />
|
<CheckCircleIcon className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-green-900">Upload Complete!</p>
|
<p className="text-sm font-semibold text-green-900 dark:text-green-200">Upload Complete!</p>
|
||||||
<p className="text-xs text-green-700">Pattern successfully transferred to machine</p>
|
<p className="text-xs text-green-700 dark:text-green-300">Pattern successfully transferred to machine</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,17 @@ export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
|
||||||
return { verticalLines, horizontalLines };
|
return { verticalLines, horizontalLines };
|
||||||
}, [gridSize, bounds, machineInfo]);
|
}, [gridSize, bounds, machineInfo]);
|
||||||
|
|
||||||
|
// Detect dark mode
|
||||||
|
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
const gridColor = isDarkMode ? '#404040' : '#e0e0e0';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group name="grid">
|
<Group name="grid">
|
||||||
{lines.verticalLines.map((points, i) => (
|
{lines.verticalLines.map((points, i) => (
|
||||||
<Line
|
<Line
|
||||||
key={`v-${i}`}
|
key={`v-${i}`}
|
||||||
points={points}
|
points={points}
|
||||||
stroke="#e0e0e0"
|
stroke={gridColor}
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -49,7 +53,7 @@ export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
|
||||||
<Line
|
<Line
|
||||||
key={`h-${i}`}
|
key={`h-${i}`}
|
||||||
points={points}
|
points={points}
|
||||||
stroke="#e0e0e0"
|
stroke={gridColor}
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -60,10 +64,13 @@ export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
|
||||||
Grid.displayName = 'Grid';
|
Grid.displayName = 'Grid';
|
||||||
|
|
||||||
export const Origin = memo(() => {
|
export const Origin = memo(() => {
|
||||||
|
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
const originColor = isDarkMode ? '#999' : '#888';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group name="origin">
|
<Group name="origin">
|
||||||
<Line points={[-10, 0, 10, 0]} stroke="#888" strokeWidth={2} />
|
<Line points={[-10, 0, 10, 0]} stroke={originColor} strokeWidth={2} />
|
||||||
<Line points={[0, -10, 0, 10]} stroke="#888" strokeWidth={2} />
|
<Line points={[0, -10, 0, 10]} stroke={originColor} strokeWidth={2} />
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -133,9 +140,10 @@ interface StitchesProps {
|
||||||
stitches: number[][];
|
stitches: number[][];
|
||||||
pesData: PesPatternData;
|
pesData: PesPatternData;
|
||||||
currentStitchIndex: number;
|
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(() => {
|
const stitchGroups = useMemo(() => {
|
||||||
interface StitchGroup {
|
interface StitchGroup {
|
||||||
color: string;
|
color: string;
|
||||||
|
|
@ -174,7 +182,7 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex }: Stitche
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
}, [stitches, pesData, currentStitchIndex]);
|
}, [stitches, pesData, currentStitchIndex, showProgress]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group name="stitches">
|
<Group name="stitches">
|
||||||
|
|
@ -187,7 +195,7 @@ export const Stitches = memo(({ stitches, pesData, currentStitchIndex }: Stitche
|
||||||
lineCap="round"
|
lineCap="round"
|
||||||
lineJoin="round"
|
lineJoin="round"
|
||||||
dash={group.isJump ? [3, 3] : undefined}
|
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>
|
</Group>
|
||||||
|
|
|
||||||
|
|
@ -46,33 +46,33 @@ export function MachineConnection({
|
||||||
const stateVisual = getStateVisualInfo(machineStatus);
|
const stateVisual = getStateVisualInfo(machineStatus);
|
||||||
|
|
||||||
const statusBadgeColors = {
|
const statusBadgeColors = {
|
||||||
idle: 'bg-cyan-100 text-cyan-800 border-cyan-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 text-cyan-800 border-cyan-200',
|
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 text-yellow-800 border-yellow-200',
|
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 text-yellow-800 border-yellow-200',
|
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 text-yellow-800 border-yellow-200',
|
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 text-green-800 border-green-200',
|
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 text-green-800 border-green-200',
|
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 text-red-800 border-red-200',
|
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 text-red-800 border-red-200',
|
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 text-red-800 border-red-200',
|
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
|
// Only show error info when connected AND there's an actual error
|
||||||
const errorInfo = (isConnected && hasError(machineError)) ? getErrorDetails(machineError) : null;
|
const errorInfo = (isConnected && hasError(machineError)) ? getErrorDetails(machineError) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
|
<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">
|
<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">Machine Connection</h2>
|
<h2 className="text-xl font-semibold dark:text-white">Machine Connection</h2>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{isConnected && isPolling && (
|
{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 && (
|
{isConnected && (
|
||||||
<button
|
<button
|
||||||
onClick={handleDisconnectClick}
|
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"
|
aria-label="Disconnect from embroidery machine"
|
||||||
>
|
>
|
||||||
Disconnect
|
Disconnect
|
||||||
|
|
@ -85,7 +85,7 @@ export function MachineConnection({
|
||||||
<div className="flex gap-3 mt-4 flex-wrap">
|
<div className="flex gap-3 mt-4 flex-wrap">
|
||||||
<button
|
<button
|
||||||
onClick={onConnect}
|
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"
|
aria-label="Connect to embroidery machine"
|
||||||
>
|
>
|
||||||
Connect to Machine
|
Connect to Machine
|
||||||
|
|
@ -97,22 +97,22 @@ export function MachineConnection({
|
||||||
{errorInfo && (
|
{errorInfo && (
|
||||||
errorInfo.isInformational ? (
|
errorInfo.isInformational ? (
|
||||||
// Informational messages (like initialization steps)
|
// 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">
|
<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="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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Regular errors shown as errors
|
// 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">
|
<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="flex-1 min-w-0">
|
||||||
<div className="font-semibold text-red-900 text-sm mb-1">{errorInfo.title}</div>
|
<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 font-mono">
|
<div className="text-xs text-red-700 dark:text-red-300 font-mono">
|
||||||
Error Code: 0x{machineError.toString(16).toUpperCase().padStart(2, '0')}
|
Error Code: 0x{machineError.toString(16).toUpperCase().padStart(2, '0')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -124,7 +124,7 @@ export function MachineConnection({
|
||||||
{/* Machine Status */}
|
{/* Machine Status */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<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={`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 className="text-base leading-none">{stateVisual.icon}</span>
|
||||||
<span>{machineStatusName}</span>
|
<span>{machineStatusName}</span>
|
||||||
|
|
@ -134,14 +134,14 @@ export function MachineConnection({
|
||||||
|
|
||||||
{/* Machine Info */}
|
{/* Machine Info */}
|
||||||
{machineInfo && (
|
{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">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="font-medium text-gray-600">Model:</span>
|
<span className="font-medium text-gray-600 dark:text-gray-400">Model:</span>
|
||||||
<span className="font-semibold text-gray-900">{machineInfo.modelNumber}</span>
|
<span className="font-semibold text-gray-900 dark:text-gray-100">{machineInfo.modelNumber}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="font-medium text-gray-600">Max Area:</span>
|
<span className="font-medium text-gray-600 dark:text-gray-400">Max Area:</span>
|
||||||
<span className="font-semibold text-gray-900">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{(machineInfo.maxWidth / 10).toFixed(1)} × {(machineInfo.maxHeight / 10).toFixed(1)} mm
|
{(machineInfo.maxWidth / 10).toFixed(1)} × {(machineInfo.maxHeight / 10).toFixed(1)} mm
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,20 +28,20 @@ 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 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">
|
<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">
|
<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}
|
{errorDetails.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-blue-800 mb-3">
|
<p className="text-blue-800 dark:text-blue-300 mb-3">
|
||||||
{errorDetails.description}
|
{errorDetails.description}
|
||||||
</p>
|
</p>
|
||||||
{errorDetails.solutions && errorDetails.solutions.length > 0 && (
|
{errorDetails.solutions && errorDetails.solutions.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<h4 className="font-semibold text-blue-900 mb-2">Steps:</h4>
|
<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 space-y-2">
|
<ol className="list-decimal list-inside text-sm text-blue-700 dark:text-blue-300 space-y-2">
|
||||||
{errorDetails.solutions.map((solution, index) => (
|
{errorDetails.solutions.map((solution, index) => (
|
||||||
<li key={index} className="pl-2">{solution}</li>
|
<li key={index} className="pl-2">{solution}</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -56,20 +56,20 @@ 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 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">
|
<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">
|
<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'}
|
{errorDetails?.title || 'Error Occurred'}
|
||||||
</h3>
|
</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.'}
|
{errorDetails?.description || errorMessage || 'An error occurred. Please check the machine and try again.'}
|
||||||
</p>
|
</p>
|
||||||
{errorDetails?.solutions && errorDetails.solutions.length > 0 && (
|
{errorDetails?.solutions && errorDetails.solutions.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<h4 className="font-semibold text-red-900 mb-2">How to Fix:</h4>
|
<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 space-y-2">
|
<ol className="list-decimal list-inside text-sm text-red-700 dark:text-red-300 space-y-2">
|
||||||
{errorDetails.solutions.map((solution, index) => (
|
{errorDetails.solutions.map((solution, index) => (
|
||||||
<li key={index} className="pl-2">{solution}</li>
|
<li key={index} className="pl-2">{solution}</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -77,7 +77,7 @@ export function NextStepGuide({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{errorCode !== undefined && (
|
{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')}
|
Error Code: 0x{errorCode.toString(16).toUpperCase().padStart(2, '0')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -90,13 +90,13 @@ 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 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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 1: Connect to Machine</h3>
|
<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 mb-3">To get started, connect to your Brother embroidery machine via Bluetooth.</p>
|
<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 space-y-1">
|
<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>Make sure your machine is powered on</li>
|
||||||
<li>Enable Bluetooth on your machine</li>
|
<li>Enable Bluetooth on your machine</li>
|
||||||
<li>Click the "Connect to Machine" button below</li>
|
<li>Click the "Connect to Machine" button below</li>
|
||||||
|
|
@ -109,13 +109,13 @@ 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 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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 2: Load Your Pattern</h3>
|
<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 mb-3">Choose a PES embroidery file from your computer to preview and upload.</p>
|
<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 space-y-1">
|
<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>Click "Choose PES File" in the Pattern File section</li>
|
||||||
<li>Select your embroidery design (.pes file)</li>
|
<li>Select your embroidery design (.pes file)</li>
|
||||||
<li>Review the pattern preview on the right</li>
|
<li>Review the pattern preview on the right</li>
|
||||||
|
|
@ -129,13 +129,13 @@ 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 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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 3: Upload Pattern to Machine</h3>
|
<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 mb-3">Send your pattern to the embroidery machine to prepare for sewing.</p>
|
<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 space-y-1">
|
<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>Review the pattern preview to ensure it's positioned correctly</li>
|
||||||
<li>Check the pattern size matches your hoop</li>
|
<li>Check the pattern size matches your hoop</li>
|
||||||
<li>Click "Upload to Machine" when ready</li>
|
<li>Click "Upload to Machine" when ready</li>
|
||||||
|
|
@ -151,13 +151,13 @@ export function NextStepGuide({
|
||||||
switch (machineStatus) {
|
switch (machineStatus) {
|
||||||
case MachineStatus.IDLE:
|
case MachineStatus.IDLE:
|
||||||
return (
|
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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-blue-900 mb-2">Step 4: Start Mask Trace</h3>
|
<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 mb-3">The mask trace helps the machine understand the pattern boundaries.</p>
|
<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 space-y-1">
|
<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>Click "Start Mask Trace" button in the Sewing Progress section</li>
|
||||||
<li>The machine will trace the pattern outline</li>
|
<li>The machine will trace the pattern outline</li>
|
||||||
<li>This ensures the hoop is positioned correctly</li>
|
<li>This ensures the hoop is positioned correctly</li>
|
||||||
|
|
@ -169,13 +169,13 @@ 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 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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-yellow-900 mb-2">Machine Action Required</h3>
|
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Machine Action Required</h3>
|
||||||
<p className="text-yellow-800 mb-3">The machine is ready to trace the pattern outline.</p>
|
<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 space-y-1">
|
<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><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>Ensure the hoop is properly attached</li>
|
||||||
<li>Make sure the needle area is clear</li>
|
<li>Make sure the needle area is clear</li>
|
||||||
|
|
@ -187,13 +187,13 @@ 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 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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-cyan-900 mb-2">Mask Trace In Progress</h3>
|
<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 mb-3">The machine is tracing the pattern boundary. Please wait...</p>
|
<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 space-y-1">
|
<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>Watch the machine trace the outline</li>
|
||||||
<li>Verify the pattern fits within your hoop</li>
|
<li>Verify the pattern fits within your hoop</li>
|
||||||
<li>Do not interrupt the machine</li>
|
<li>Do not interrupt the machine</li>
|
||||||
|
|
@ -206,13 +206,13 @@ 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 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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-green-900 mb-2">Step 5: Ready to Sew!</h3>
|
<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 mb-3">The machine is ready to begin embroidering your pattern.</p>
|
<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 space-y-1">
|
<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>Verify your thread colors are correct</li>
|
||||||
<li>Ensure the fabric is properly hooped</li>
|
<li>Ensure the fabric is properly hooped</li>
|
||||||
<li>Click "Start Sewing" when ready</li>
|
<li>Click "Start Sewing" when ready</li>
|
||||||
|
|
@ -224,13 +224,13 @@ 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 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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-cyan-900 mb-2">Step 6: Sewing In Progress</h3>
|
<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 mb-3">Your embroidery is being stitched. Monitor the progress below.</p>
|
<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 space-y-1">
|
<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>Watch the progress bar and current stitch count</li>
|
||||||
<li>The machine will pause when a color change is needed</li>
|
<li>The machine will pause when a color change is needed</li>
|
||||||
<li>Do not leave the machine unattended</li>
|
<li>Do not leave the machine unattended</li>
|
||||||
|
|
@ -242,13 +242,13 @@ 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 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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-yellow-900 mb-2">Thread Change Required</h3>
|
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Thread Change Required</h3>
|
||||||
<p className="text-yellow-800 mb-3">The machine needs a different thread color to continue.</p>
|
<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 space-y-1">
|
<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>Check the color blocks section to see which thread is needed</li>
|
||||||
<li>Change to the correct thread color</li>
|
<li>Change to the correct thread color</li>
|
||||||
<li><strong>Press the button on your machine</strong> to resume sewing</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.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 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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-yellow-900 mb-2">Sewing Paused</h3>
|
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Sewing Paused</h3>
|
||||||
<p className="text-yellow-800 mb-3">The embroidery has been paused or interrupted.</p>
|
<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 space-y-1">
|
<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>Check if everything is okay with the machine</li>
|
||||||
<li>Click "Resume Sewing" when ready to continue</li>
|
<li>Click "Resume Sewing" when ready to continue</li>
|
||||||
<li>The machine will pick up where it left off</li>
|
<li>The machine will pick up where it left off</li>
|
||||||
|
|
@ -280,13 +280,13 @@ 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 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">
|
<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">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-semibold text-green-900 mb-2">Step 7: Embroidery Complete!</h3>
|
<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 mb-3">Your embroidery is finished. Great work!</p>
|
<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 space-y-1">
|
<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>Remove the hoop from the machine</li>
|
||||||
<li>Press the Accept button on the machine</li>
|
<li>Press the Accept button on the machine</li>
|
||||||
<li>Carefully remove your finished embroidery</li>
|
<li>Carefully remove your finished embroidery</li>
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,10 @@ interface PatternCanvasProps {
|
||||||
initialPatternOffset?: { x: number; y: number };
|
initialPatternOffset?: { x: number; y: number };
|
||||||
onPatternOffsetChange?: (offsetX: number, offsetY: number) => void;
|
onPatternOffsetChange?: (offsetX: number, offsetY: number) => void;
|
||||||
patternUploaded?: boolean;
|
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 containerRef = useRef<HTMLDivElement>(null);
|
||||||
const stageRef = useRef<Konva.Stage | null>(null);
|
const stageRef = useRef<Konva.Stage | null>(null);
|
||||||
|
|
||||||
|
|
@ -170,9 +171,9 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
||||||
}, [onPatternOffsetChange]);
|
}, [onPatternOffsetChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200">
|
<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">Pattern Preview</h2>
|
<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 rounded bg-gray-50 overflow-hidden" ref={containerRef}>
|
<div className="relative w-full h-[600px] border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-900 overflow-hidden" ref={containerRef}>
|
||||||
{containerSize.width > 0 && (
|
{containerSize.width > 0 && (
|
||||||
<Stage
|
<Stage
|
||||||
width={containerSize.width}
|
width={containerSize.width}
|
||||||
|
|
@ -220,23 +221,24 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
||||||
{pesData && (
|
{pesData && (
|
||||||
<Group
|
<Group
|
||||||
name="pattern-group"
|
name="pattern-group"
|
||||||
draggable={!patternUploaded}
|
draggable={!patternUploaded && !isUploading}
|
||||||
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 && !patternUploaded) stage.container().style.cursor = 'move';
|
if (stage && !patternUploaded && !isUploading) stage.container().style.cursor = 'move';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
const stage = e.target.getStage();
|
const stage = e.target.getStage();
|
||||||
if (stage && !patternUploaded) stage.container().style.cursor = 'grab';
|
if (stage && !patternUploaded && !isUploading) stage.container().style.cursor = 'grab';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stitches
|
<Stitches
|
||||||
stitches={pesData.stitches}
|
stitches={pesData.stitches}
|
||||||
pesData={pesData}
|
pesData={pesData}
|
||||||
currentStitchIndex={sewingProgress?.currentStitch || 0}
|
currentStitchIndex={sewingProgress?.currentStitch || 0}
|
||||||
|
showProgress={patternUploaded || isUploading}
|
||||||
/>
|
/>
|
||||||
<PatternBounds bounds={pesData.bounds} />
|
<PatternBounds bounds={pesData.bounds} />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
@ -259,7 +261,7 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
||||||
|
|
||||||
{/* Placeholder overlay when no pattern is loaded */}
|
{/* Placeholder overlay when no pattern is loaded */}
|
||||||
{!pesData && (
|
{!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
|
Load a PES file to preview the pattern
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -268,57 +270,57 @@ export function PatternCanvas({ pesData, sewingProgress, machineInfo, initialPat
|
||||||
{pesData && (
|
{pesData && (
|
||||||
<>
|
<>
|
||||||
{/* Thread Legend Overlay */}
|
{/* Thread Legend Overlay */}
|
||||||
<div className="absolute top-2.5 left-2.5 bg-white/95 backdrop-blur-sm p-3 rounded-lg shadow-lg z-10 max-w-[150px]">
|
<div className="absolute top-2.5 left-2.5 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm p-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>
|
<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) => (
|
{pesData.threads.map((thread, index) => (
|
||||||
<div key={index} className="flex items-center gap-2 mb-1.5 last:mb-0">
|
<div key={index} className="flex items-center gap-2 mb-1.5 last:mb-0">
|
||||||
<div
|
<div
|
||||||
className="w-5 h-5 rounded border border-black flex-shrink-0"
|
className="w-5 h-5 rounded border border-black dark:border-gray-300 flex-shrink-0"
|
||||||
style={{ backgroundColor: thread.hex }}
|
style={{ backgroundColor: thread.hex }}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-900">Thread {index + 1}</span>
|
<span className="text-xs text-gray-900 dark:text-gray-100">Thread {index + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pattern Dimensions Overlay */}
|
{/* 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.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
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pattern Offset Indicator */}
|
{/* Pattern Offset Indicator */}
|
||||||
<div className={`absolute bottom-20 right-5 backdrop-blur-sm p-2.5 px-3.5 rounded-lg shadow-lg z-[11] min-w-[180px] transition-colors ${
|
<div className={`absolute bottom-20 right-5 backdrop-blur-sm p-2.5 px-3.5 rounded-lg shadow-lg z-[11] min-w-[180px] transition-colors ${
|
||||||
patternUploaded ? 'bg-amber-50/95 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="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 && (
|
{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" />
|
<LockClosedIcon className="w-3.5 h-3.5" />
|
||||||
<span className="text-[10px] font-bold">LOCKED</span>
|
<span className="text-[10px] font-bold">LOCKED</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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
|
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 dark:text-gray-400 italic">
|
||||||
{patternUploaded ? 'Pattern locked • Drag background to pan' : '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>
|
||||||
|
|
||||||
{/* Zoom Controls Overlay */}
|
{/* 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">
|
<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 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">
|
<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" />
|
<PlusIcon className="w-5 h-5 dark:text-gray-200" />
|
||||||
</button>
|
</button>
|
||||||
<span className="min-w-[50px] text-center text-[13px] font-semibold text-gray-900 select-none">{Math.round(stageScale * 100)}%</span>
|
<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 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">
|
<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" />
|
<MinusIcon className="w-5 h-5 dark:text-gray-200" />
|
||||||
</button>
|
</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">
|
<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" />
|
<ArrowPathIcon className="w-5 h-5 dark:text-gray-200" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -97,41 +97,41 @@ export function ProgressMonitor({
|
||||||
);
|
);
|
||||||
|
|
||||||
const stateIndicatorColors = {
|
const stateIndicatorColors = {
|
||||||
idle: 'bg-blue-50 border-l-blue-600',
|
idle: 'bg-blue-50 dark:bg-blue-900/20 border-l-blue-600',
|
||||||
info: 'bg-blue-50 border-l-blue-600',
|
info: 'bg-blue-50 dark:bg-blue-900/20 border-l-blue-600',
|
||||||
active: 'bg-yellow-50 border-l-yellow-500',
|
active: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
|
||||||
waiting: 'bg-yellow-50 border-l-yellow-500',
|
waiting: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
|
||||||
warning: 'bg-yellow-50 border-l-yellow-500',
|
warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-l-yellow-500',
|
||||||
complete: 'bg-green-50 border-l-green-600',
|
complete: 'bg-green-50 dark:bg-green-900/20 border-l-green-600',
|
||||||
success: 'bg-green-50 border-l-green-600',
|
success: 'bg-green-50 dark:bg-green-900/20 border-l-green-600',
|
||||||
interrupted: 'bg-red-50 border-l-red-600',
|
interrupted: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
|
||||||
error: 'bg-red-50 border-l-red-600',
|
error: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
|
||||||
danger: 'bg-red-50 border-l-red-600',
|
danger: 'bg-red-50 dark:bg-red-900/20 border-l-red-600',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-4 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 animate-fadeIn">
|
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md 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 dark:border-gray-600 dark:text-white">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">
|
||||||
{/* Left Column - Pattern Info & Progress */}
|
{/* Left Column - Pattern Info & Progress */}
|
||||||
<div>
|
<div>
|
||||||
{patternInfo && (
|
{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 className="grid grid-cols-3 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-600 block text-xs">Total Stitches</span>
|
<span className="text-gray-600 dark:text-gray-400 block text-xs">Total Stitches</span>
|
||||||
<span className="font-semibold text-gray-900">{patternInfo.totalStitches.toLocaleString()}</span>
|
<span className="font-semibold text-gray-900 dark:text-gray-100">{patternInfo.totalStitches.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-600 block text-xs">Est. Time</span>
|
<span className="text-gray-600 dark:text-gray-400 block text-xs">Est. Time</span>
|
||||||
<span className="font-semibold text-gray-900">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{Math.floor(patternInfo.totalTime / 60)}:{String(patternInfo.totalTime % 60).padStart(2, '0')}
|
{Math.floor(patternInfo.totalTime / 60)}:{String(patternInfo.totalTime % 60).padStart(2, '0')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-600 block text-xs">Speed</span>
|
<span className="text-gray-600 dark:text-gray-400 block text-xs">Speed</span>
|
||||||
<span className="font-semibold text-gray-900">{patternInfo.speed} spm</span>
|
<span className="font-semibold text-gray-900 dark:text-gray-100">{patternInfo.speed} spm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -140,23 +140,23 @@ export function ProgressMonitor({
|
||||||
{sewingProgress && (
|
{sewingProgress && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="flex justify-between items-center mb-1.5">
|
<div className="flex justify-between items-center mb-1.5">
|
||||||
<span className="text-xs font-medium text-gray-600">Progress</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">{progressPercent.toFixed(1)}%</span>
|
<span className="text-xl font-bold text-blue-600 dark:text-blue-400">{progressPercent.toFixed(1)}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-3 bg-gray-300 rounded-md overflow-hidden shadow-inner relative mb-2">
|
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded-md overflow-hidden shadow-inner relative mb-2">
|
||||||
<div className="h-full bg-gradient-to-r from-blue-600 to-blue-700 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite]" style={{ width: `${progressPercent}%` }} />
|
<div className="h-full bg-gradient-to-r from-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>
|
||||||
|
|
||||||
<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>
|
<div>
|
||||||
<span className="text-gray-600 block text-xs">Current Stitch</span>
|
<span className="text-gray-600 dark:text-gray-400 block text-xs">Current Stitch</span>
|
||||||
<span className="font-semibold text-gray-900">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{sewingProgress.currentStitch.toLocaleString()} / {patternInfo?.totalStitches.toLocaleString() || 0}
|
{sewingProgress.currentStitch.toLocaleString()} / {patternInfo?.totalStitches.toLocaleString() || 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-600 block text-xs">Time Elapsed</span>
|
<span className="text-gray-600 dark:text-gray-400 block text-xs">Time Elapsed</span>
|
||||||
<span className="font-semibold text-gray-900">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{Math.floor(sewingProgress.currentTime / 60)}:{String(sewingProgress.currentTime % 60).padStart(2, '0')}
|
{Math.floor(sewingProgress.currentTime / 60)}:{String(sewingProgress.currentTime % 60).padStart(2, '0')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -167,12 +167,12 @@ export function ProgressMonitor({
|
||||||
{/* State Visual Indicator */}
|
{/* State Visual Indicator */}
|
||||||
{patternInfo && (() => {
|
{patternInfo && (() => {
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
ready: <ClockIcon 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" />,
|
active: <PlayIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />,
|
||||||
waiting: <PauseCircleIcon className="w-6 h-6" />,
|
waiting: <PauseCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />,
|
||||||
complete: <CheckBadgeIcon className="w-6 h-6" />,
|
complete: <CheckBadgeIcon className="w-6 h-6 text-green-600 dark:text-green-400" />,
|
||||||
interrupted: <PauseCircleIcon className="w-6 h-6" />,
|
interrupted: <PauseCircleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />,
|
||||||
error: <ExclamationCircleIcon className="w-6 h-6" />
|
error: <ExclamationCircleIcon className="w-6 h-6 text-red-600 dark:text-red-400" />
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -181,8 +181,8 @@ export function ProgressMonitor({
|
||||||
{iconMap[stateVisual.iconName]}
|
{iconMap[stateVisual.iconName]}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="font-semibold text-sm">{stateVisual.label}</div>
|
<div className="font-semibold text-sm dark:text-gray-100">{stateVisual.label}</div>
|
||||||
<div className="text-xs text-gray-600">{stateVisual.description}</div>
|
<div className="text-xs text-gray-600 dark:text-gray-400">{stateVisual.description}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -194,7 +194,7 @@ export function ProgressMonitor({
|
||||||
{canResumeSewing(machineStatus) && (
|
{canResumeSewing(machineStatus) && (
|
||||||
<button
|
<button
|
||||||
onClick={onResumeSewing}
|
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"
|
aria-label="Resume sewing the current pattern"
|
||||||
>
|
>
|
||||||
<PlayIcon className="w-4 h-4" />
|
<PlayIcon className="w-4 h-4" />
|
||||||
|
|
@ -206,7 +206,7 @@ export function ProgressMonitor({
|
||||||
{canStartSewing(machineStatus) && !canResumeSewing(machineStatus) && (
|
{canStartSewing(machineStatus) && !canResumeSewing(machineStatus) && (
|
||||||
<button
|
<button
|
||||||
onClick={onStartSewing}
|
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"
|
aria-label="Start sewing the pattern"
|
||||||
>
|
>
|
||||||
Start Sewing
|
Start Sewing
|
||||||
|
|
@ -217,7 +217,7 @@ export function ProgressMonitor({
|
||||||
{canStartMaskTrace(machineStatus) && (
|
{canStartMaskTrace(machineStatus) && (
|
||||||
<button
|
<button
|
||||||
onClick={onStartMaskTrace}
|
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'}
|
aria-label={isMaskTraceComplete ? 'Start mask trace again' : 'Start mask trace'}
|
||||||
>
|
>
|
||||||
{isMaskTraceComplete ? 'Trace Again' : 'Start Mask Trace'}
|
{isMaskTraceComplete ? 'Trace Again' : 'Start Mask Trace'}
|
||||||
|
|
@ -228,7 +228,7 @@ export function ProgressMonitor({
|
||||||
{patternInfo && canDeletePattern(machineStatus) && (
|
{patternInfo && canDeletePattern(machineStatus) && (
|
||||||
<button
|
<button
|
||||||
onClick={onDeletePattern}
|
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"
|
aria-label="Delete the current pattern from machine"
|
||||||
>
|
>
|
||||||
Delete Pattern
|
Delete Pattern
|
||||||
|
|
@ -241,7 +241,7 @@ export function ProgressMonitor({
|
||||||
<div>
|
<div>
|
||||||
{colorBlocks.length > 0 && (
|
{colorBlocks.length > 0 && (
|
||||||
<div>
|
<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">
|
<div className="flex flex-col gap-2">
|
||||||
{colorBlocks.map((block, index) => {
|
{colorBlocks.map((block, index) => {
|
||||||
const isCompleted = currentStitch >= block.endStitch;
|
const isCompleted = currentStitch >= block.endStitch;
|
||||||
|
|
@ -260,10 +260,10 @@ export function ProgressMonitor({
|
||||||
key={index}
|
key={index}
|
||||||
className={`p-3 rounded-lg border-2 transition-all duration-300 ${
|
className={`p-3 rounded-lg border-2 transition-all duration-300 ${
|
||||||
isCompleted
|
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
|
: isCurrent
|
||||||
? 'border-blue-600 bg-blue-50 shadow-lg shadow-blue-600/20 animate-pulseGlow'
|
? 'border-blue-600 bg-blue-50 dark:bg-blue-900/20 shadow-lg shadow-blue-600/20 animate-pulseGlow'
|
||||||
: 'border-gray-200 bg-gray-50 opacity-70 hover:opacity-90'
|
: 'border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/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'}`}
|
||||||
|
|
@ -271,7 +271,7 @@ export function ProgressMonitor({
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Larger color swatch with better visibility */}
|
{/* Larger color swatch with better visibility */}
|
||||||
<div
|
<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={{
|
style={{
|
||||||
backgroundColor: block.threadHex,
|
backgroundColor: block.threadHex,
|
||||||
...(isCurrent && { borderColor: '#2563eb', ringColor: '#93c5fd' })
|
...(isCurrent && { borderColor: '#2563eb', ringColor: '#93c5fd' })
|
||||||
|
|
@ -282,10 +282,10 @@ export function ProgressMonitor({
|
||||||
|
|
||||||
{/* Thread info */}
|
{/* Thread info */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-semibold text-sm text-gray-900">
|
<div className="font-semibold text-sm text-gray-900 dark:text-gray-100">
|
||||||
Thread {block.colorIndex + 1}
|
Thread {block.colorIndex + 1}
|
||||||
</div>
|
</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
|
{block.stitchCount.toLocaleString()} stitches
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -302,9 +302,9 @@ export function ProgressMonitor({
|
||||||
|
|
||||||
{/* Progress bar for current block */}
|
{/* Progress bar for current block */}
|
||||||
{isCurrent && (
|
{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
|
<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}%` }}
|
style={{ width: `${blockProgress}%` }}
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-valuenow={Math.round(blockProgress)}
|
aria-valuenow={Math.round(blockProgress)}
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,11 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
|
||||||
return (
|
return (
|
||||||
<div className="relative max-w-5xl mx-auto mt-4" role="navigation" aria-label="Workflow progress">
|
<div className="relative max-w-5xl mx-auto mt-4" role="navigation" aria-label="Workflow progress">
|
||||||
{/* Progress bar background */}
|
{/* 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 */}
|
{/* Progress bar fill */}
|
||||||
<div
|
<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={{
|
style={{
|
||||||
left: '24px',
|
left: '24px',
|
||||||
width: `calc(${((currentStep - 1) / (steps.length - 1)) * 100}% - 24px)`
|
width: `calc(${((currentStep - 1) / (steps.length - 1)) * 100}% - 24px)`
|
||||||
|
|
@ -96,9 +96,9 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
w-10 h-10 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-300 border-2 shadow-md
|
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' : ''}
|
${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 border-blue-500 text-white scale-110 shadow-blue-600/40 ring-2 ring-blue-300 ring-offset-2' : ''}
|
${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 border-blue-500/30 text-blue-200/70' : ''}
|
${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'}`}
|
aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}`}
|
||||||
>
|
>
|
||||||
|
|
@ -112,7 +112,7 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
|
||||||
{/* Step label */}
|
{/* Step label */}
|
||||||
<div className="mt-2 text-center">
|
<div className="mt-2 text-center">
|
||||||
<div className={`text-xs font-semibold leading-tight ${
|
<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}
|
{step.label}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
12
tailwind.config.js
Normal file
12
tailwind.config.js
Normal 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: [],
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue