Merge pull request #20 from jhbruhn/feature/unified-color-system

feature: Implement unified semantic color system with Tailwind v4
This commit is contained in:
Jan-Henrik Bruhn 2025-12-17 21:45:17 +01:00 committed by GitHub
commit 8ae52a79fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 316 additions and 301 deletions

View file

@ -1,6 +1,129 @@
@import "tailwindcss"; @import "tailwindcss";
/* Custom animations for shimmer effect */ /* ============================================
THEME DEFINITION - Tailwind v4
Semantic color system that references Tailwind colors
============================================ */
@theme {
/* PRIMARY - Main brand color (references Blue) */
--color-primary-50: var(--color-blue-50);
--color-primary-100: var(--color-blue-100);
--color-primary-200: var(--color-blue-200);
--color-primary-300: var(--color-blue-300);
--color-primary-400: var(--color-blue-400);
--color-primary-500: var(--color-blue-500);
--color-primary-600: var(--color-blue-600);
--color-primary-700: var(--color-blue-700);
--color-primary-800: var(--color-blue-800);
--color-primary-900: var(--color-blue-900);
--color-primary-950: var(--color-blue-950);
/* SUCCESS - Positive states, completion (references Green) */
--color-success-50: var(--color-green-50);
--color-success-100: var(--color-green-100);
--color-success-200: var(--color-green-200);
--color-success-300: var(--color-green-300);
--color-success-400: var(--color-green-400);
--color-success-500: var(--color-green-500);
--color-success-600: var(--color-green-600);
--color-success-700: var(--color-green-700);
--color-success-800: var(--color-green-800);
--color-success-900: var(--color-green-900);
--color-success-950: var(--color-green-950);
/* WARNING - Caution, waiting states (references Amber) */
--color-warning-50: var(--color-amber-50);
--color-warning-100: var(--color-amber-100);
--color-warning-200: var(--color-amber-200);
--color-warning-300: var(--color-amber-300);
--color-warning-400: var(--color-amber-400);
--color-warning-500: var(--color-amber-500);
--color-warning-600: var(--color-amber-600);
--color-warning-700: var(--color-amber-700);
--color-warning-800: var(--color-amber-800);
--color-warning-900: var(--color-amber-900);
--color-warning-950: var(--color-amber-950);
/* DANGER - Errors, destructive actions (references Red) */
--color-danger-50: var(--color-red-50);
--color-danger-100: var(--color-red-100);
--color-danger-200: var(--color-red-200);
--color-danger-300: var(--color-red-300);
--color-danger-400: var(--color-red-400);
--color-danger-500: var(--color-red-500);
--color-danger-600: var(--color-red-600);
--color-danger-700: var(--color-red-700);
--color-danger-800: var(--color-red-800);
--color-danger-900: var(--color-red-900);
--color-danger-950: var(--color-red-950);
/* INFO - Informational states (references Cyan) */
--color-info-50: var(--color-cyan-50);
--color-info-100: var(--color-cyan-100);
--color-info-200: var(--color-cyan-200);
--color-info-300: var(--color-cyan-300);
--color-info-400: var(--color-cyan-400);
--color-info-500: var(--color-cyan-500);
--color-info-600: var(--color-cyan-600);
--color-info-700: var(--color-cyan-700);
--color-info-800: var(--color-cyan-800);
--color-info-900: var(--color-cyan-900);
--color-info-950: var(--color-cyan-950);
/* ACCENT - Progress, sewing states (references Purple) */
--color-accent-50: var(--color-purple-50);
--color-accent-100: var(--color-purple-100);
--color-accent-200: var(--color-purple-200);
--color-accent-300: var(--color-purple-300);
--color-accent-400: var(--color-purple-400);
--color-accent-500: var(--color-purple-500);
--color-accent-600: var(--color-purple-600);
--color-accent-700: var(--color-purple-700);
--color-accent-800: var(--color-purple-800);
--color-accent-900: var(--color-purple-900);
--color-accent-950: var(--color-purple-950);
/* SECONDARY - Upload operations (references Orange) */
--color-secondary-50: var(--color-orange-50);
--color-secondary-100: var(--color-orange-100);
--color-secondary-200: var(--color-orange-200);
--color-secondary-300: var(--color-orange-300);
--color-secondary-400: var(--color-orange-400);
--color-secondary-500: var(--color-orange-500);
--color-secondary-600: var(--color-orange-600);
--color-secondary-700: var(--color-orange-700);
--color-secondary-800: var(--color-orange-800);
--color-secondary-900: var(--color-orange-900);
--color-secondary-950: var(--color-orange-950);
/* TERTIARY - Pattern canvas theme (references Teal) */
--color-tertiary-500: var(--color-teal-500);
--color-tertiary-600: var(--color-teal-600);
/* Canvas/Konva-specific colors for embroidery rendering */
--color-canvas-grid: #e0e0e0;
--color-canvas-origin: #888888;
--color-canvas-hoop: #2196F3;
--color-canvas-bounds: #ff0000;
--color-canvas-position: #ff0000;
}
/* Dark Mode Overrides - Media Query */
@media (prefers-color-scheme: dark) {
@theme {
/* Canvas colors adjusted for dark mode */
--color-canvas-grid: #404040;
--color-canvas-origin: #999999;
/* hoop, bounds, position stay the same for visibility */
}
}
/* ============================================
CUSTOM ANIMATIONS
============================================ */
/* Shimmer effect for progress bars */
@keyframes shimmer { @keyframes shimmer {
0% { 0% {
transform: translateX(-100%); transform: translateX(-100%);
@ -44,13 +167,13 @@
} }
} }
/* Pulse glow effect */ /* Pulse glow effect - uses primary-600 */
@keyframes pulseGlow { @keyframes pulseGlow {
0%, 100% { 0%, 100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); box-shadow: 0 0 0 0 rgb(37 99 235 / 0.4); /* primary-600 with 40% opacity */
} }
50% { 50% {
box-shadow: 0 0 0 8px rgba(59, 130, 246, 0); box-shadow: 0 0 0 8px rgb(37 99 235 / 0); /* primary-600 fully transparent */
} }
} }

View file

@ -67,7 +67,7 @@ function App() {
}, [resumedPattern, resumeFileName, pesData, setPattern, setPatternOffset]); }, [resumedPattern, resumeFileName, pesData, setPattern, setPatternOffset]);
return ( return (
<div className="h-screen flex flex-col bg-gray-50 dark:bg-gray-900 overflow-hidden"> <div className="h-screen flex flex-col bg-gray-300 dark:bg-gray-900 overflow-hidden">
<AppHeader /> <AppHeader />
<div className="flex-1 p-4 sm:p-5 lg:p-6 w-full overflow-y-auto lg:overflow-hidden flex flex-col"> <div className="flex-1 p-4 sm:p-5 lg:p-6 w-full overflow-y-auto lg:overflow-hidden flex flex-col">

View file

@ -86,18 +86,18 @@ export function AppHeader() {
}, [showErrorPopover, setErrorPopover]); }, [showErrorPopover, setErrorPopover]);
return ( return (
<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-4 sm:px-6 lg:px-8 py-3 shadow-lg border-b-2 border-blue-900/20 dark:border-blue-800/30 flex-shrink-0"> <header className="bg-gradient-to-r from-primary-600 via-primary-700 to-primary-800 dark:from-primary-700 dark:via-primary-800 dark:to-primary-900 px-4 sm:px-6 lg:px-8 py-3 shadow-lg border-b-2 border-primary-900/20 dark:border-primary-800/30 flex-shrink-0">
<div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-4 lg:gap-8 items-center"> <div className="grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-4 lg:gap-8 items-center">
{/* Machine Connection Status - Responsive width column */} {/* Machine Connection Status - Responsive width column */}
<div className="flex items-center gap-3 w-full lg:w-[280px]"> <div className="flex items-center gap-3 w-full lg:w-[280px]">
<div className="w-2.5 h-2.5 bg-green-400 rounded-full animate-pulse shadow-lg shadow-green-400/50" style={{ visibility: isConnected ? 'visible' : 'hidden' }}></div> <div className="w-2.5 h-2.5 bg-success-400 rounded-full animate-pulse shadow-lg shadow-success-400/50" style={{ visibility: isConnected ? 'visible' : 'hidden' }}></div>
<div className="w-2.5 h-2.5 bg-gray-400 rounded-full -ml-2.5" style={{ visibility: !isConnected ? 'visible' : 'hidden' }}></div> <div className="w-2.5 h-2.5 bg-gray-400 rounded-full -ml-2.5" style={{ visibility: !isConnected ? 'visible' : 'hidden' }}></div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h1 className="text-lg lg:text-xl font-bold text-white leading-tight">Respira</h1> <h1 className="text-lg lg:text-xl font-bold text-white leading-tight">Respira</h1>
{isConnected && machineInfo?.serialNumber && ( {isConnected && machineInfo?.serialNumber && (
<span <span
className="text-xs text-blue-200 cursor-help" className="text-xs text-primary-200 cursor-help"
title={`Serial: ${machineInfo.serialNumber}${ title={`Serial: ${machineInfo.serialNumber}${
machineInfo.macAddress machineInfo.macAddress
? `\nMAC: ${machineInfo.macAddress}` ? `\nMAC: ${machineInfo.macAddress}`
@ -116,7 +116,7 @@ export function AppHeader() {
</span> </span>
)} )}
{isPolling && ( {isPolling && (
<ArrowPathIcon className="w-3.5 h-3.5 text-blue-200 animate-spin" title="Auto-refreshing status" /> <ArrowPathIcon className="w-3.5 h-3.5 text-primary-200 animate-spin" title="Auto-refreshing status" />
)} )}
</div> </div>
<div className="flex items-center gap-2 mt-1 min-h-[32px]"> <div className="flex items-center gap-2 mt-1 min-h-[32px]">
@ -124,7 +124,7 @@ export function AppHeader() {
<> <>
<button <button
onClick={disconnect} onClick={disconnect}
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 sm:py-1 rounded text-sm font-medium bg-white/10 hover:bg-red-600 text-blue-100 hover:text-white border border-white/20 hover:border-red-600 cursor-pointer transition-all flex-shrink-0" className="inline-flex items-center gap-1.5 px-2.5 py-1.5 sm:py-1 rounded text-sm font-medium bg-white/10 hover:bg-danger-600 text-primary-100 hover:text-white border border-white/20 hover:border-danger-600 cursor-pointer transition-all flex-shrink-0"
title="Disconnect from machine" title="Disconnect from machine"
aria-label="Disconnect from machine" aria-label="Disconnect from machine"
> >
@ -137,7 +137,7 @@ export function AppHeader() {
</span> </span>
</> </>
) : ( ) : (
<p className="text-xs text-blue-200">Not Connected</p> <p className="text-xs text-primary-200">Not Connected</p>
)} )}
{/* Error indicator - always render to prevent layout shift */} {/* Error indicator - always render to prevent layout shift */}
@ -145,7 +145,7 @@ export function AppHeader() {
<button <button
ref={errorButtonRef} ref={errorButtonRef}
onClick={() => setErrorPopover(!showErrorPopover)} onClick={() => setErrorPopover(!showErrorPopover)}
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 sm:py-1 rounded text-sm font-medium bg-red-500/90 hover:bg-red-600 text-white border border-red-400 transition-all flex-shrink-0 ${ className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 sm:py-1 rounded text-sm font-medium bg-danger-500/90 hover:bg-danger-600 text-white border border-danger-400 transition-all flex-shrink-0 ${
(machineErrorMessage || pyodideError) (machineErrorMessage || pyodideError)
? 'cursor-pointer animate-pulse hover:animate-none' ? 'cursor-pointer animate-pulse hover:animate-none'
: 'invisible pointer-events-none' : 'invisible pointer-events-none'

View file

@ -59,7 +59,7 @@ export function BluetoothDevicePicker() {
return ( return (
<div className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-[1000]" onClick={handleCancel}> <div className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-[1000]" onClick={handleCancel}>
<div <div
className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-lg w-[90%] m-4 border-t-4 border-blue-600 dark:border-blue-500" className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-lg w-[90%] m-4 border-t-4 border-primary-600 dark:border-primary-500"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
role="dialog" role="dialog"
aria-labelledby="bluetooth-picker-title" aria-labelledby="bluetooth-picker-title"
@ -73,7 +73,7 @@ export function BluetoothDevicePicker() {
<div className="p-6"> <div className="p-6">
{isScanning && devices.length === 0 ? ( {isScanning && devices.length === 0 ? (
<div className="flex items-center gap-3 text-gray-700 dark:text-gray-300"> <div className="flex items-center gap-3 text-gray-700 dark:text-gray-300">
<svg className="animate-spin h-5 w-5 text-blue-600 dark:text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg className="animate-spin h-5 w-5 text-primary-600 dark:text-primary-400" xmlns="http://www.w3.org/2000/svg" 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>
@ -89,7 +89,7 @@ export function BluetoothDevicePicker() {
<button <button
key={device.deviceId} key={device.deviceId}
onClick={() => handleSelectDevice(device.deviceId)} onClick={() => handleSelectDevice(device.deviceId)}
className="w-full px-4 py-3 bg-gray-100 dark:bg-gray-700 text-left rounded-lg font-medium text-sm hover:bg-blue-100 dark:hover:bg-blue-900 hover:text-blue-900 dark:hover:text-blue-100 active:bg-blue-200 dark:active:bg-blue-800 transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" className="w-full px-4 py-3 bg-gray-100 dark:bg-gray-700 text-left rounded-lg font-medium text-sm hover:bg-primary-100 dark:hover:bg-primary-900 hover:text-primary-900 dark:hover:text-primary-100 active:bg-primary-200 dark:active:bg-primary-800 transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-300 dark:focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
aria-label={`Connect to ${device.deviceName}`} aria-label={`Connect to ${device.deviceName}`}
> >
<div className="font-semibold text-gray-900 dark:text-white">{device.deviceName}</div> <div className="font-semibold text-gray-900 dark:text-white">{device.deviceName}</div>

View file

@ -40,7 +40,7 @@ export function ConfirmDialog({
return ( return (
<div className="fixed inset-0 bg-black/50 dark:bg-black/70 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 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'}`} className={`bg-white dark:bg-gray-800 rounded-lg shadow-2xl max-w-lg w-[90%] m-4 ${variant === 'danger' ? 'border-t-4 border-danger-600 dark:border-danger-500' : 'border-t-4 border-warning-500 dark:border-warning-600'}`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
role="dialog" role="dialog"
aria-labelledby="dialog-title" aria-labelledby="dialog-title"
@ -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 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-danger-600 dark:bg-danger-700 text-white rounded-lg font-semibold text-sm hover:bg-danger-700 dark:hover:bg-danger-600 active:bg-danger-800 dark:active:bg-danger-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-danger-300 dark:focus:ring-danger-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900'
: 'px-6 py-2.5 bg-blue-600 dark:bg-blue-700 text-white rounded-lg font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900' : 'px-6 py-2.5 bg-primary-600 dark:bg-primary-700 text-white rounded-lg font-semibold text-sm hover:bg-primary-700 dark:hover:bg-primary-600 active:bg-primary-800 dark:active:bg-primary-500 hover:shadow-lg active:scale-[0.98] transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-300 dark:focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900'
} }
aria-label={`Confirm: ${confirmText}`} aria-label={`Confirm: ${confirmText}`}
> >

View file

@ -26,7 +26,7 @@ export function ConnectionPrompt() {
</div> </div>
<button <button
onClick={connect} onClick={connect}
className="w-full flex items-center justify-center gap-2 px-3 py-2.5 sm:py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer" className="w-full flex items-center justify-center gap-2 px-3 py-2.5 sm:py-2 bg-primary-600 dark:bg-primary-700 text-white rounded font-semibold text-sm hover:bg-primary-700 dark:hover:bg-primary-600 active:bg-primary-800 dark:active:bg-primary-500 transition-colors cursor-pointer"
> >
Connect to Machine Connect to Machine
</button> </button>
@ -35,17 +35,17 @@ export function ConnectionPrompt() {
} }
return ( return (
<div className="bg-amber-50 dark:bg-amber-900/20 p-4 rounded-lg shadow-md border-l-4 border-amber-500 dark:border-amber-600"> <div className="bg-warning-50 dark:bg-warning-900/20 p-4 rounded-lg shadow-md border-l-4 border-warning-500 dark:border-warning-600">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<ExclamationTriangleIcon className="w-6 h-6 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" /> <ExclamationTriangleIcon className="w-6 h-6 text-warning-600 dark:text-warning-400 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-base font-semibold text-amber-900 dark:text-amber-100 mb-2">Browser Not Supported</h3> <h3 className="text-base font-semibold text-warning-900 dark:text-warning-100 mb-2">Browser Not Supported</h3>
<p className="text-sm text-amber-800 dark:text-amber-200 mb-3"> <p className="text-sm text-warning-800 dark:text-warning-200 mb-3">
Your browser doesn't support Web Bluetooth, which is required to connect to your embroidery machine. Your browser doesn't support Web Bluetooth, which is required to connect to your embroidery machine.
</p> </p>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm font-semibold text-amber-900 dark:text-amber-100">Please try one of these options:</p> <p className="text-sm font-semibold text-warning-900 dark:text-warning-100">Please try one of these options:</p>
<ul className="text-sm text-amber-800 dark:text-amber-200 space-y-1.5 ml-4 list-disc"> <ul className="text-sm text-warning-800 dark:text-warning-200 space-y-1.5 ml-4 list-disc">
<li>Use a supported browser (Chrome, Edge, or Opera)</li> <li>Use a supported browser (Chrome, Edge, or Opera)</li>
<li> <li>
Download the Desktop app from{' '} Download the Desktop app from{' '}
@ -53,7 +53,7 @@ export function ConnectionPrompt() {
href="https://github.com/jhbruhn/respira/releases/latest" href="https://github.com/jhbruhn/respira/releases/latest"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-semibold underline hover:text-amber-900 dark:hover:text-amber-50 transition-colors" className="font-semibold underline hover:text-warning-900 dark:hover:text-warning-50 transition-colors"
> >
GitHub Releases GitHub Releases
</a> </a>

View file

@ -17,24 +17,24 @@ export const ErrorPopover = forwardRef<HTMLDivElement, ErrorPopoverProps>(
const isInfo = isPairingErr || errorDetails?.isInformational; const isInfo = isPairingErr || errorDetails?.isInformational;
const bgColor = isInfo const bgColor = isInfo
? 'bg-blue-50 dark:bg-blue-900/95 border-blue-600 dark:border-blue-500' ? 'bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500'
: 'bg-red-50 dark:bg-red-900/95 border-red-600 dark:border-red-500'; : 'bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500';
const iconColor = isInfo const iconColor = isInfo
? 'text-blue-600 dark:text-blue-400' ? 'text-info-600 dark:text-info-400'
: 'text-red-600 dark:text-red-400'; : 'text-danger-600 dark:text-danger-400';
const textColor = isInfo const textColor = isInfo
? 'text-blue-900 dark:text-blue-200' ? 'text-info-900 dark:text-info-200'
: 'text-red-900 dark:text-red-200'; : 'text-danger-900 dark:text-danger-200';
const descColor = isInfo const descColor = isInfo
? 'text-blue-800 dark:text-blue-300' ? 'text-info-800 dark:text-info-300'
: 'text-red-800 dark:text-red-300'; : 'text-danger-800 dark:text-danger-300';
const listColor = isInfo const listColor = isInfo
? 'text-blue-700 dark:text-blue-300' ? 'text-info-700 dark:text-info-300'
: 'text-red-700 dark:text-red-300'; : 'text-danger-700 dark:text-danger-300';
const Icon = isInfo ? InformationCircleIcon : ExclamationTriangleIcon; const Icon = isInfo ? InformationCircleIcon : ExclamationTriangleIcon;
const title = errorDetails?.title || (isPairingErr ? 'Pairing Required' : 'Error'); const title = errorDetails?.title || (isPairingErr ? 'Pairing Required' : 'Error');

View file

@ -171,8 +171,8 @@ export function FileUpload() {
const boundsCheck = checkPatternFitsInHoop(); const boundsCheck = checkPatternFitsInHoop();
const borderColor = pesData ? 'border-orange-600 dark:border-orange-500' : 'border-gray-400 dark:border-gray-600'; const borderColor = pesData ? 'border-secondary-600 dark:border-secondary-500' : 'border-gray-400 dark:border-gray-600';
const iconColor = pesData ? 'text-orange-600 dark:text-orange-400' : 'text-gray-600 dark:text-gray-400'; const iconColor = pesData ? 'text-secondary-600 dark:text-secondary-400' : 'text-gray-600 dark:text-gray-400';
return ( return (
<div className={`bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 ${borderColor}`}> <div className={`bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 ${borderColor}`}>
@ -191,8 +191,8 @@ export function FileUpload() {
</div> </div>
{resumeAvailable && resumeFileName && ( {resumeAvailable && resumeFileName && (
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 px-3 py-2 rounded mb-3"> <div className="bg-success-50 dark:bg-success-900/20 border border-success-200 dark:border-success-800 px-3 py-2 rounded mb-3">
<p className="text-xs text-green-800 dark:text-green-200"> <p className="text-xs text-success-800 dark:text-success-200">
<strong>Cached:</strong> "{resumeFileName}" <strong>Cached:</strong> "{resumeFileName}"
</p> </p>
</div> </div>
@ -249,7 +249,7 @@ export function FileUpload() {
<button <button
onClick={handleUpload} onClick={handleUpload}
disabled={!isConnected || isUploading || !boundsCheck.fits} disabled={!isConnected || isUploading || !boundsCheck.fits}
className="flex-1 px-3 py-2.5 sm:py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-sm hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" className="flex-1 px-3 py-2.5 sm:py-2 bg-primary-600 dark:bg-primary-700 text-white rounded font-semibold text-sm hover:bg-primary-700 dark:hover:bg-primary-600 active:bg-primary-800 dark:active:bg-primary-500 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
aria-label={isUploading ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : boundsCheck.error || 'Upload pattern to machine'} aria-label={isUploading ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` : boundsCheck.error || 'Upload pattern to machine'}
> >
{isUploading ? ( {isUploading ? (
@ -279,13 +279,13 @@ export function FileUpload() {
? 'Please wait - initializing Python environment...' ? 'Please wait - initializing Python environment...'
: pyodideLoadingStep || 'Initializing Python environment...'} : pyodideLoadingStep || 'Initializing Python environment...'}
</span> </span>
<span className="text-xs font-bold text-blue-600 dark:text-blue-400"> <span className="text-xs font-bold text-primary-600 dark:text-primary-400">
{pyodideProgress.toFixed(0)}% {pyodideProgress.toFixed(0)}%
</span> </span>
</div> </div>
<div className="h-2.5 bg-gray-300 dark:bg-gray-600 rounded-full overflow-hidden shadow-inner relative"> <div className="h-2.5 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 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" className="h-full bg-gradient-to-r from-primary-500 via-primary-600 to-primary-700 dark:from-primary-600 dark:via-primary-700 dark:to-primary-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: `${pyodideProgress}%` }} style={{ width: `${pyodideProgress}%` }}
/> />
</div> </div>
@ -303,13 +303,13 @@ export function FileUpload() {
marginTop: (pesData && (boundsCheck.error || !canUploadPattern(machineStatus))) ? '12px' : '0px' marginTop: (pesData && (boundsCheck.error || !canUploadPattern(machineStatus))) ? '12px' : '0px'
}}> }}>
{pesData && !canUploadPattern(machineStatus) && ( {pesData && !canUploadPattern(machineStatus) && (
<div className="bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 px-3 py-2 rounded border border-yellow-200 dark:border-yellow-800 text-sm"> <div className="bg-warning-100 dark:bg-warning-900/20 text-warning-800 dark:text-warning-200 px-3 py-2 rounded border border-warning-200 dark:border-warning-800 text-sm">
Cannot upload while {getMachineStateCategory(machineStatus)} Cannot upload while {getMachineStateCategory(machineStatus)}
</div> </div>
)} )}
{pesData && boundsCheck.error && ( {pesData && boundsCheck.error && (
<div className="bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-200 px-3 py-2 rounded border border-red-200 dark:border-red-800 text-sm"> <div className="bg-danger-100 dark:bg-danger-900/20 text-danger-800 dark:text-danger-200 px-3 py-2 rounded border border-danger-200 dark:border-danger-800 text-sm">
<strong>Pattern too large:</strong> {boundsCheck.error} <strong>Pattern too large:</strong> {boundsCheck.error}
</div> </div>
)} )}
@ -319,13 +319,13 @@ export function FileUpload() {
<div className="mt-3"> <div className="mt-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 dark:text-gray-400">Uploading</span> <span className="text-xs font-medium text-gray-600 dark:text-gray-400">Uploading</span>
<span className="text-xs font-bold text-orange-600 dark:text-orange-400"> <span className="text-xs font-bold text-secondary-600 dark:text-secondary-400">
{uploadProgress > 0 ? uploadProgress.toFixed(1) + '%' : 'Starting...'} {uploadProgress > 0 ? uploadProgress.toFixed(1) + '%' : 'Starting...'}
</span> </span>
</div> </div>
<div className="h-2.5 bg-gray-300 dark:bg-gray-600 rounded-full overflow-hidden shadow-inner relative"> <div className="h-2.5 bg-gray-300 dark:bg-gray-600 rounded-full overflow-hidden shadow-inner relative">
<div <div
className="h-full bg-gradient-to-r from-orange-500 via-orange-600 to-orange-700 dark:from-orange-600 dark:via-orange-700 dark:to-orange-800 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite] rounded-full" className="h-full bg-gradient-to-r from-secondary-500 via-secondary-600 to-secondary-700 dark:from-secondary-600 dark:via-secondary-700 dark:to-secondary-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>

View file

@ -4,6 +4,7 @@ import type { PesPatternData } from '../formats/import/pesImporter';
import { getThreadColor } from '../formats/import/pesImporter'; import { getThreadColor } from '../formats/import/pesImporter';
import type { MachineInfo } from '../types/machine'; import type { MachineInfo } from '../types/machine';
import { MOVE } from '../formats/import/constants'; import { MOVE } from '../formats/import/constants';
import { canvasColors } from '../utils/cssVariables';
interface GridProps { interface GridProps {
gridSize: number; gridSize: number;
@ -34,9 +35,7 @@ export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
return { verticalLines, horizontalLines }; return { verticalLines, horizontalLines };
}, [gridSize, bounds, machineInfo]); }, [gridSize, bounds, machineInfo]);
// Detect dark mode const gridColor = canvasColors.grid();
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const gridColor = isDarkMode ? '#404040' : '#e0e0e0';
return ( return (
<Group name="grid"> <Group name="grid">
@ -63,8 +62,7 @@ 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 = canvasColors.origin();
const originColor = isDarkMode ? '#999' : '#888';
return ( return (
<Group name="origin"> <Group name="origin">
@ -84,6 +82,7 @@ export const Hoop = memo(({ machineInfo }: HoopProps) => {
const { maxWidth, maxHeight } = machineInfo; const { maxWidth, maxHeight } = machineInfo;
const hoopLeft = -maxWidth / 2; const hoopLeft = -maxWidth / 2;
const hoopTop = -maxHeight / 2; const hoopTop = -maxHeight / 2;
const hoopColor = canvasColors.hoop();
return ( return (
<Group name="hoop"> <Group name="hoop">
@ -92,7 +91,7 @@ export const Hoop = memo(({ machineInfo }: HoopProps) => {
y={hoopTop} y={hoopTop}
width={maxWidth} width={maxWidth}
height={maxHeight} height={maxHeight}
stroke="#2196F3" stroke={hoopColor}
strokeWidth={3} strokeWidth={3}
dash={[10, 5]} dash={[10, 5]}
/> />
@ -103,7 +102,7 @@ export const Hoop = memo(({ machineInfo }: HoopProps) => {
fontSize={14} fontSize={14}
fontFamily="sans-serif" fontFamily="sans-serif"
fontStyle="bold" fontStyle="bold"
fill="#2196F3" fill={hoopColor}
/> />
</Group> </Group>
); );
@ -119,6 +118,7 @@ export const PatternBounds = memo(({ bounds }: PatternBoundsProps) => {
const { minX, maxX, minY, maxY } = bounds; const { minX, maxX, minY, maxY } = bounds;
const width = maxX - minX; const width = maxX - minX;
const height = maxY - minY; const height = maxY - minY;
const boundsColor = canvasColors.bounds();
return ( return (
<Rect <Rect
@ -126,7 +126,7 @@ export const PatternBounds = memo(({ bounds }: PatternBoundsProps) => {
y={minY} y={minY}
width={width} width={width}
height={height} height={height}
stroke="#ff0000" stroke={boundsColor}
strokeWidth={2} strokeWidth={2}
dash={[5, 5]} dash={[5, 5]}
/> />
@ -231,6 +231,7 @@ export const CurrentPosition = memo(({ currentStitchIndex, stitches }: CurrentPo
} }
const [x, y] = stitches[currentStitchIndex]; const [x, y] = stitches[currentStitchIndex];
const positionColor = canvasColors.position();
return ( return (
<Group name="currentPosition"> <Group name="currentPosition">
@ -238,14 +239,14 @@ export const CurrentPosition = memo(({ currentStitchIndex, stitches }: CurrentPo
x={x} x={x}
y={y} y={y}
radius={8} radius={8}
fill="rgba(255, 0, 0, 0.3)" fill={`${positionColor}4d`}
stroke="#ff0000" stroke={positionColor}
strokeWidth={3} strokeWidth={3}
/> />
<Line points={[x - 12, y, x - 3, y]} stroke="#ff0000" strokeWidth={2} /> <Line points={[x - 12, y, x - 3, y]} stroke={positionColor} strokeWidth={2} />
<Line points={[x + 12, y, x + 3, y]} stroke="#ff0000" strokeWidth={2} /> <Line points={[x + 12, y, x + 3, y]} stroke={positionColor} strokeWidth={2} />
<Line points={[x, y - 12, x, y - 3]} stroke="#ff0000" strokeWidth={2} /> <Line points={[x, y - 12, x, y - 3]} stroke={positionColor} strokeWidth={2} />
<Line points={[x, y + 12, x, y + 3]} stroke="#ff0000" strokeWidth={2} /> <Line points={[x, y + 12, x, y + 3]} stroke={positionColor} strokeWidth={2} />
</Group> </Group>
); );
}); });

View file

@ -61,16 +61,16 @@ export function MachineConnection({
}; };
const statusBadgeColors = { const statusBadgeColors = {
idle: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300', idle: 'bg-info-100 dark:bg-info-900/30 text-info-800 dark:text-info-300',
info: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300', info: 'bg-info-100 dark:bg-info-900/30 text-info-800 dark:text-info-300',
active: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300', active: 'bg-warning-100 dark:bg-warning-900/30 text-warning-800 dark:text-warning-300',
waiting: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300', waiting: 'bg-warning-100 dark:bg-warning-900/30 text-warning-800 dark:text-warning-300',
warning: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300', warning: 'bg-warning-100 dark:bg-warning-900/30 text-warning-800 dark:text-warning-300',
complete: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300', complete: 'bg-success-100 dark:bg-success-900/30 text-success-800 dark:text-success-300',
success: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300', success: 'bg-success-100 dark:bg-success-900/30 text-success-800 dark:text-success-300',
interrupted: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300', interrupted: 'bg-danger-100 dark:bg-danger-900/30 text-danger-800 dark:text-danger-300',
error: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300', error: 'bg-danger-100 dark:bg-danger-900/30 text-danger-800 dark:text-danger-300',
danger: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300', danger: 'bg-danger-100 dark:bg-danger-900/30 text-danger-800 dark:text-danger-300',
}; };
// Only show error info when connected AND there's an actual error // Only show error info when connected AND there's an actual error
@ -90,15 +90,15 @@ export function MachineConnection({
<button <button
onClick={onConnect} onClick={onConnect}
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-xs hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer" className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-primary-600 dark:bg-primary-700 text-white rounded font-semibold text-xs hover:bg-primary-700 dark:hover:bg-primary-600 active:bg-primary-800 dark:active:bg-primary-500 transition-colors cursor-pointer"
> >
Connect to Machine Connect to Machine
</button> </button>
</div> </div>
) : ( ) : (
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-green-600 dark:border-green-500"> <div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-success-600 dark:border-success-500">
<div className="flex items-start gap-3 mb-3"> <div className="flex items-start gap-3 mb-3">
<WifiIcon className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" /> <WifiIcon className="w-6 h-6 text-success-600 dark:text-success-400 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Machine Info</h3> <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Machine Info</h3>
<p className="text-xs text-gray-600 dark:text-gray-400"> <p className="text-xs text-gray-600 dark:text-gray-400">
@ -110,21 +110,21 @@ export function MachineConnection({
{/* Error/Info Display */} {/* Error/Info Display */}
{errorInfo && ( {errorInfo && (
errorInfo.isInformational ? ( errorInfo.isInformational ? (
<div className="mb-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg"> <div className="mb-3 p-3 bg-info-50 dark:bg-info-900/20 border border-info-200 dark:border-info-800 rounded-lg">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<InformationCircleIcon className="w-4 h-4 text-blue-600 dark:text-blue-400 flex-shrink-0" /> <InformationCircleIcon className="w-4 h-4 text-info-600 dark:text-info-400 flex-shrink-0" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-semibold text-blue-900 dark:text-blue-200 text-xs">{errorInfo.title}</div> <div className="font-semibold text-info-900 dark:text-info-200 text-xs">{errorInfo.title}</div>
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div className="mb-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"> <div className="mb-3 p-3 bg-danger-50 dark:bg-danger-900/20 border border-danger-200 dark:border-danger-800 rounded-lg">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span className="text-red-600 dark:text-red-400 flex-shrink-0"></span> <span className="text-danger-600 dark:text-danger-400 flex-shrink-0"></span>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-semibold text-red-900 dark:text-red-200 text-xs mb-1">{errorInfo.title}</div> <div className="font-semibold text-danger-900 dark:text-danger-200 text-xs mb-1">{errorInfo.title}</div>
<div className="text-xs text-red-700 dark:text-red-300 font-mono"> <div className="text-xs text-danger-700 dark:text-danger-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>
@ -148,14 +148,14 @@ export function MachineConnection({
{/* Machine Info */} {/* Machine Info */}
{machineInfo && ( {machineInfo && (
<div className="grid grid-cols-2 gap-2 text-xs mb-3"> <div className="grid grid-cols-2 gap-2 text-xs mb-3">
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded"> <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block">Max Area</span> <span className="text-gray-600 dark:text-gray-400 block">Max Area</span>
<span className="font-semibold text-gray-900 dark:text-gray-100"> <span className="font-semibold text-gray-900 dark:text-gray-100">
{(machineInfo.maxWidth / 10).toFixed(1)} × {(machineInfo.maxHeight / 10).toFixed(1)} mm {(machineInfo.maxWidth / 10).toFixed(1)} × {(machineInfo.maxHeight / 10).toFixed(1)} mm
</span> </span>
</div> </div>
{machineInfo.totalCount !== undefined && ( {machineInfo.totalCount !== undefined && (
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded"> <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block">Total Stitches</span> <span className="text-gray-600 dark:text-gray-400 block">Total Stitches</span>
<span className="font-semibold text-gray-900 dark:text-gray-100"> <span className="font-semibold text-gray-900 dark:text-gray-100">
{machineInfo.totalCount.toLocaleString()} {machineInfo.totalCount.toLocaleString()}

View file

@ -213,8 +213,8 @@ export function PatternCanvas() {
setPatternOffset(newOffset.x, newOffset.y); setPatternOffset(newOffset.x, newOffset.y);
}, [setPatternOffset]); }, [setPatternOffset]);
const borderColor = pesData ? 'border-teal-600 dark:border-teal-500' : 'border-gray-400 dark:border-gray-600'; const borderColor = pesData ? 'border-tertiary-600 dark:border-tertiary-500' : 'border-gray-400 dark:border-gray-600';
const iconColor = pesData ? 'text-teal-600 dark:text-teal-400' : 'text-gray-600 dark:text-gray-400'; const iconColor = pesData ? 'text-tertiary-600 dark:text-tertiary-400' : 'text-gray-600 dark:text-gray-400';
return ( return (
<div className={`lg:h-full bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 ${borderColor} flex flex-col`}> <div className={`lg:h-full bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 ${borderColor} flex flex-col`}>
@ -231,7 +231,7 @@ export function PatternCanvas() {
)} )}
</div> </div>
</div> </div>
<div className="relative w-full h-[400px] sm:h-[500px] lg:flex-1 lg:min-h-0 border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-900 overflow-hidden" ref={containerRef}> <div className="relative w-full h-[400px] sm:h-[500px] lg:flex-1 lg:min-h-0 border border-gray-300 dark:border-gray-600 rounded bg-gray-200 dark:bg-gray-900 overflow-hidden" ref={containerRef}>
{containerSize.width > 0 && ( {containerSize.width > 0 && (
<Stage <Stage
width={containerSize.width} width={containerSize.width}
@ -392,7 +392,7 @@ export function PatternCanvas() {
</div> </div>
)} )}
</div> </div>
<div className="text-sm font-semibold text-blue-600 dark:text-blue-400 mb-1"> <div className="text-sm font-semibold text-primary-600 dark:text-primary-400 mb-1">
X: {(localPatternOffset.x / 10).toFixed(1)}mm, Y: {(localPatternOffset.y / 10).toFixed(1)}mm X: {(localPatternOffset.x / 10).toFixed(1)}mm, Y: {(localPatternOffset.y / 10).toFixed(1)}mm
</div> </div>
<div className="text-xs text-gray-600 dark:text-gray-400 italic"> <div className="text-xs text-gray-600 dark:text-gray-400 italic">
@ -402,17 +402,17 @@ export function PatternCanvas() {
{/* Zoom Controls Overlay */} {/* Zoom Controls Overlay */}
<div className="absolute bottom-2 sm:bottom-5 right-2 sm:right-5 flex gap-1.5 sm:gap-2 items-center bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg shadow-lg z-10"> <div className="absolute bottom-2 sm:bottom-5 right-2 sm:right-5 flex gap-1.5 sm:gap-2 items-center bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg shadow-lg z-10">
<button className="w-7 h-7 sm:w-8 sm: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={handleCenterPattern} disabled={!pesData || patternUploaded || isUploading} title="Center Pattern in Hoop"> <button className="w-7 h-7 sm:w-8 sm: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-primary-600 hover:text-white hover:border-primary-600 dark:hover:border-primary-600 hover:shadow-md hover:shadow-primary-600/30 disabled:opacity-50 disabled:cursor-not-allowed" onClick={handleCenterPattern} disabled={!pesData || patternUploaded || isUploading} title="Center Pattern in Hoop">
<ArrowsPointingInIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" /> <ArrowsPointingInIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" />
</button> </button>
<button className="w-7 h-7 sm:w-8 sm: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"> <button className="w-7 h-7 sm:w-8 sm: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-primary-600 hover:text-white hover:border-primary-600 dark:hover:border-primary-600 hover:shadow-md hover:shadow-primary-600/30 disabled:opacity-50 disabled:cursor-not-allowed" onClick={handleZoomIn} title="Zoom In">
<PlusIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" /> <PlusIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" />
</button> </button>
<span className="min-w-[40px] sm:min-w-[50px] text-center text-sm font-semibold text-gray-900 dark:text-gray-100 select-none">{Math.round(stageScale * 100)}%</span> <span className="min-w-[40px] sm:min-w-[50px] text-center text-sm font-semibold text-gray-900 dark:text-gray-100 select-none">{Math.round(stageScale * 100)}%</span>
<button className="w-7 h-7 sm:w-8 sm: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"> <button className="w-7 h-7 sm:w-8 sm: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-primary-600 hover:text-white hover:border-primary-600 dark:hover:border-primary-600 hover:shadow-md hover:shadow-primary-600/30 disabled:opacity-50 disabled:cursor-not-allowed" onClick={handleZoomOut} title="Zoom Out">
<MinusIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" /> <MinusIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" />
</button> </button>
<button className="w-7 h-7 sm:w-8 sm: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"> <button className="w-7 h-7 sm:w-8 sm: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-primary-600 hover:text-white hover:border-primary-600 dark:hover:border-primary-600 hover:shadow-md hover:shadow-primary-600/30 disabled:opacity-50 disabled:cursor-not-allowed ml-1" onClick={handleZoomReset} title="Reset Zoom">
<ArrowPathIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" /> <ArrowPathIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" />
</button> </button>
</div> </div>

View file

@ -9,14 +9,14 @@ export function PatternInfo({ pesData, showThreadBlocks = false }: PatternInfoPr
return ( return (
<> <>
<div className="grid grid-cols-3 gap-2 text-xs mb-2"> <div className="grid grid-cols-3 gap-2 text-xs mb-2">
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded"> <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block">Size</span> <span className="text-gray-600 dark:text-gray-400 block">Size</span>
<span className="font-semibold text-gray-900 dark:text-gray-100"> <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="bg-gray-50 dark:bg-gray-700/50 p-2 rounded"> <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block">Stitches</span> <span className="text-gray-600 dark:text-gray-400 block">Stitches</span>
<span className="font-semibold text-gray-900 dark:text-gray-100"> <span className="font-semibold text-gray-900 dark:text-gray-100">
{pesData.penStitches?.stitches.length.toLocaleString() || pesData.stitchCount.toLocaleString()} {pesData.penStitches?.stitches.length.toLocaleString() || pesData.stitchCount.toLocaleString()}
@ -30,7 +30,7 @@ export function PatternInfo({ pesData, showThreadBlocks = false }: PatternInfoPr
)} )}
</span> </span>
</div> </div>
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded"> <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block"> <span className="text-gray-600 dark:text-gray-400 block">
{showThreadBlocks ? 'Colors / Blocks' : 'Colors'} {showThreadBlocks ? 'Colors / Blocks' : 'Colors'}
</span> </span>

View file

@ -15,8 +15,8 @@ export function PatternPreviewPlaceholder() {
<svg className="w-28 h-28 mx-auto text-gray-300 dark:text-gray-600" 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 dark:bg-blue-900 rounded-full flex items-center justify-center"> <div className="absolute -top-2 -right-2 w-8 h-8 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 text-primary-600 dark:text-primary-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>
@ -27,15 +27,15 @@ export function PatternPreviewPlaceholder() {
</p> </p>
<div className="flex items-center justify-center gap-6 text-xs text-gray-400 dark:text-gray-500"> <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 dark:bg-blue-500 rounded-full"></div> <div className="w-2 h-2 bg-primary-400 dark:bg-primary-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 dark:bg-green-500 rounded-full"></div> <div className="w-2 h-2 bg-success-400 dark:bg-success-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 dark:bg-purple-500 rounded-full"></div> <div className="w-2 h-2 bg-accent-400 dark:bg-accent-500 rounded-full"></div>
<span>Real-time Preview</span> <span>Real-time Preview</span>
</div> </div>
</div> </div>

View file

@ -34,9 +34,9 @@ export function PatternSummaryCard() {
const canDelete = canDeletePattern(machineStatus); const canDelete = canDeletePattern(machineStatus);
return ( return (
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-blue-600 dark:border-blue-500"> <div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-primary-600 dark:border-primary-500">
<div className="flex items-start gap-3 mb-3"> <div className="flex items-start gap-3 mb-3">
<DocumentTextIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" /> <DocumentTextIcon className="w-6 h-6 text-primary-600 dark:text-primary-400 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Active Pattern</h3> <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">Active Pattern</h3>
<p className="text-xs text-gray-600 dark:text-gray-400 truncate" title={currentFileName}> <p className="text-xs text-gray-600 dark:text-gray-400 truncate" title={currentFileName}>
@ -51,7 +51,7 @@ export function PatternSummaryCard() {
<button <button
onClick={deletePattern} onClick={deletePattern}
disabled={isDeleting} disabled={isDeleting}
className="w-full flex items-center justify-center gap-2 px-3 py-2.5 sm:py-2 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 rounded border border-red-300 dark:border-red-700 hover:bg-red-100 dark:hover:bg-red-900/30 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer" className="w-full flex items-center justify-center gap-2 px-3 py-2.5 sm:py-2 bg-danger-50 dark:bg-danger-900/20 text-danger-700 dark:text-danger-300 rounded border border-danger-300 dark:border-danger-700 hover:bg-danger-100 dark:hover:bg-danger-900/30 text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
> >
{isDeleting ? ( {isDeleting ? (
<> <>

View file

@ -154,22 +154,22 @@ export function ProgressMonitor() {
}, [colorBlocks]); }, [colorBlocks]);
const stateIndicatorColors = { const stateIndicatorColors = {
idle: "bg-blue-50 dark:bg-blue-900/20 border-blue-600", idle: "bg-info-50 dark:bg-info-900/20 border-info-600",
info: "bg-blue-50 dark:bg-blue-900/20 border-blue-600", info: "bg-info-50 dark:bg-info-900/20 border-info-600",
active: "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500", active: "bg-warning-50 dark:bg-warning-900/20 border-warning-500",
waiting: "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500", waiting: "bg-warning-50 dark:bg-warning-900/20 border-warning-500",
warning: "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500", warning: "bg-warning-50 dark:bg-warning-900/20 border-warning-500",
complete: "bg-green-50 dark:bg-green-900/20 border-green-600", complete: "bg-success-50 dark:bg-success-900/20 border-success-600",
success: "bg-green-50 dark:bg-green-900/20 border-green-600", success: "bg-success-50 dark:bg-success-900/20 border-success-600",
interrupted: "bg-red-50 dark:bg-red-900/20 border-red-600", interrupted: "bg-danger-50 dark:bg-danger-900/20 border-danger-600",
error: "bg-red-50 dark:bg-red-900/20 border-red-600", error: "bg-danger-50 dark:bg-danger-900/20 border-danger-600",
danger: "bg-red-50 dark:bg-red-900/20 border-red-600", danger: "bg-danger-50 dark:bg-danger-900/20 border-danger-600",
}; };
return ( return (
<div className="lg:h-full bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-purple-600 dark:border-purple-500 flex flex-col lg:overflow-hidden"> <div className="lg:h-full bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 border-accent-600 dark:border-accent-500 flex flex-col lg:overflow-hidden">
<div className="flex items-start gap-3 mb-3"> <div className="flex items-start gap-3 mb-3">
<ChartBarIcon className="w-6 h-6 text-purple-600 dark:text-purple-400 flex-shrink-0 mt-0.5" /> <ChartBarIcon className="w-6 h-6 text-accent-600 dark:text-accent-400 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1"> <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">
Sewing Progress Sewing Progress
@ -185,7 +185,7 @@ export function ProgressMonitor() {
{/* Pattern Info */} {/* Pattern Info */}
{patternInfo && ( {patternInfo && (
<div className="grid grid-cols-3 gap-2 text-xs mb-3"> <div className="grid grid-cols-3 gap-2 text-xs mb-3">
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded"> <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block"> <span className="text-gray-600 dark:text-gray-400 block">
Total Stitches Total Stitches
</span> </span>
@ -193,7 +193,7 @@ export function ProgressMonitor() {
{totalStitches.toLocaleString()} {totalStitches.toLocaleString()}
</span> </span>
</div> </div>
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded"> <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block"> <span className="text-gray-600 dark:text-gray-400 block">
Total Time Total Time
</span> </span>
@ -201,7 +201,7 @@ export function ProgressMonitor() {
{totalMinutes} min {totalMinutes} min
</span> </span>
</div> </div>
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded"> <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block"> <span className="text-gray-600 dark:text-gray-400 block">
Speed Speed
</span> </span>
@ -217,13 +217,13 @@ export function ProgressMonitor() {
<div className="mb-3"> <div className="mb-3">
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded-md overflow-hidden shadow-inner relative mb-2"> <div className="h-3 bg-gray-300 dark:bg-gray-600 rounded-md overflow-hidden shadow-inner relative mb-2">
<div <div
className="h-full bg-gradient-to-r from-purple-600 to-purple-700 dark:from-purple-600 dark:to-purple-800 transition-all duration-300 ease-out relative overflow-hidden after:absolute after:inset-0 after:bg-gradient-to-r after:from-transparent after:via-white/30 after:to-transparent after:animate-[shimmer_2s_infinite]" className="h-full bg-gradient-to-r from-accent-600 to-accent-700 dark:from-accent-600 dark:to-accent-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}%` }} style={{ width: `${progressPercent}%` }}
/> />
</div> </div>
<div className="grid grid-cols-2 gap-2 text-xs mb-3"> <div className="grid grid-cols-2 gap-2 text-xs mb-3">
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded"> <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block"> <span className="text-gray-600 dark:text-gray-400 block">
Current Stitch Current Stitch
</span> </span>
@ -232,7 +232,7 @@ export function ProgressMonitor() {
{totalStitches.toLocaleString()} {totalStitches.toLocaleString()}
</span> </span>
</div> </div>
<div className="bg-gray-50 dark:bg-gray-700/50 p-2 rounded"> <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block"> <span className="text-gray-600 dark:text-gray-400 block">
Time Time
</span> </span>
@ -249,22 +249,22 @@ export function ProgressMonitor() {
(() => { (() => {
const iconMap = { const iconMap = {
ready: ( ready: (
<ClockIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" /> <ClockIcon className="w-5 h-5 text-info-600 dark:text-info-400" />
), ),
active: ( active: (
<PlayIcon className="w-5 h-5 text-yellow-600 dark:text-yellow-400" /> <PlayIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
), ),
waiting: ( waiting: (
<PauseCircleIcon className="w-5 h-5 text-yellow-600 dark:text-yellow-400" /> <PauseCircleIcon className="w-5 h-5 text-warning-600 dark:text-warning-400" />
), ),
complete: ( complete: (
<CheckBadgeIcon className="w-5 h-5 text-green-600 dark:text-green-400" /> <CheckBadgeIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
), ),
interrupted: ( interrupted: (
<PauseCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400" /> <PauseCircleIcon className="w-5 h-5 text-danger-600 dark:text-danger-400" />
), ),
error: ( error: (
<ExclamationCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400" /> <ExclamationCircleIcon className="w-5 h-5 text-danger-600 dark:text-danger-400" />
), ),
}; };
@ -297,7 +297,7 @@ export function ProgressMonitor() {
<div <div
ref={colorBlocksScrollRef} ref={colorBlocksScrollRef}
onScroll={handleColorBlocksScroll} onScroll={handleColorBlocksScroll}
className="lg:absolute lg:inset-0 flex flex-col gap-2 lg:overflow-y-auto scroll-smooth pr-1 [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-track]:bg-gray-100 dark:[&::-webkit-scrollbar-track]:bg-gray-700 [&::-webkit-scrollbar-thumb]:bg-blue-600 dark:[&::-webkit-scrollbar-thumb]:bg-blue-500 [&::-webkit-scrollbar-thumb]:rounded-full" className="lg:absolute lg:inset-0 flex flex-col gap-2 lg:overflow-y-auto scroll-smooth pr-1 [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-track]:bg-gray-100 dark:[&::-webkit-scrollbar-track]:bg-gray-700 [&::-webkit-scrollbar-thumb]:bg-primary-600 dark:[&::-webkit-scrollbar-thumb]:bg-primary-500 [&::-webkit-scrollbar-thumb]:rounded-full"
> >
{colorBlocks.map((block, index) => { {colorBlocks.map((block, index) => {
const isCompleted = currentStitch >= block.endStitch; const isCompleted = currentStitch >= block.endStitch;
@ -319,10 +319,10 @@ export function ProgressMonitor() {
ref={isCurrent ? currentBlockRef : null} ref={isCurrent ? currentBlockRef : null}
className={`p-2.5 rounded-lg border-2 transition-all duration-300 ${ className={`p-2.5 rounded-lg border-2 transition-all duration-300 ${
isCompleted isCompleted
? "border-green-600 bg-green-50 dark:bg-green-900/20" ? "border-success-600 bg-success-50 dark:bg-success-900/20"
: isCurrent : isCurrent
? "border-purple-600 bg-purple-50 dark:bg-purple-900/20 shadow-lg shadow-purple-600/20 animate-pulseGlow" ? "border-accent-600 bg-accent-50 dark:bg-accent-900/20 shadow-lg shadow-accent-600/20 animate-pulseGlow"
: "border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50 opacity-70" : "border-gray-200 dark:border-gray-600 bg-gray-300 dark:bg-gray-800/50 opacity-70"
}`} }`}
role="listitem" role="listitem"
aria-label={`Thread ${block.colorIndex + 1}, ${block.stitchCount} stitches, ${isCompleted ? "completed" : isCurrent ? "in progress" : "pending"}`} aria-label={`Thread ${block.colorIndex + 1}, ${block.stitchCount} stitches, ${isCompleted ? "completed" : isCurrent ? "in progress" : "pending"}`}
@ -374,12 +374,12 @@ export function ProgressMonitor() {
{/* Status icon */} {/* Status icon */}
{isCompleted ? ( {isCompleted ? (
<CheckCircleIcon <CheckCircleIcon
className="w-5 h-5 text-green-600 flex-shrink-0" className="w-5 h-5 text-success-600 flex-shrink-0"
aria-label="Completed" aria-label="Completed"
/> />
) : isCurrent ? ( ) : isCurrent ? (
<ArrowRightIcon <ArrowRightIcon
className="w-5 h-5 text-purple-600 flex-shrink-0 animate-pulse" className="w-5 h-5 text-accent-600 flex-shrink-0 animate-pulse"
aria-label="In progress" aria-label="In progress"
/> />
) : ( ) : (
@ -394,7 +394,7 @@ export function ProgressMonitor() {
{isCurrent && ( {isCurrent && (
<div className="mt-2 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden"> <div className="mt-2 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div <div
className="h-full bg-purple-600 dark:bg-purple-500 transition-all duration-300 rounded-full" className="h-full bg-accent-600 dark:bg-accent-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)}
@ -423,7 +423,7 @@ export function ProgressMonitor() {
<button <button
onClick={resumeSewing} onClick={resumeSewing}
disabled={isDeleting} disabled={isDeleting}
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2.5 sm:py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-xs hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2.5 sm:py-2 bg-primary-600 dark:bg-primary-700 text-white rounded font-semibold text-xs hover:bg-primary-700 dark:hover:bg-primary-600 active:bg-primary-800 dark:active:bg-primary-500 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Resume sewing the current pattern" aria-label="Resume sewing the current pattern"
> >
<PlayIcon className="w-3.5 h-3.5" /> <PlayIcon className="w-3.5 h-3.5" />
@ -436,7 +436,7 @@ export function ProgressMonitor() {
<button <button
onClick={startSewing} onClick={startSewing}
disabled={isDeleting} disabled={isDeleting}
className="flex-[2] flex items-center justify-center gap-1.5 px-3 py-2.5 sm:py-2 bg-blue-600 dark:bg-blue-700 text-white rounded font-semibold text-xs hover:bg-blue-700 dark:hover:bg-blue-600 active:bg-blue-800 dark:active:bg-blue-500 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" className="flex-[2] flex items-center justify-center gap-1.5 px-3 py-2.5 sm:py-2 bg-primary-600 dark:bg-primary-700 text-white rounded font-semibold text-xs hover:bg-primary-700 dark:hover:bg-primary-600 active:bg-primary-800 dark:active:bg-primary-500 transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Start sewing the pattern" aria-label="Start sewing the pattern"
> >
<PlayIcon className="w-3.5 h-3.5" /> <PlayIcon className="w-3.5 h-3.5" />

View file

@ -23,7 +23,7 @@ export function PatternCanvasSkeleton() {
<div className="flex items-center justify-between mb-4 pb-2 border-b-2 border-gray-300 dark:border-gray-600"> <div className="flex items-center justify-between mb-4 pb-2 border-b-2 border-gray-300 dark:border-gray-600">
<SkeletonLoader className="h-7 w-40" variant="text" /> <SkeletonLoader className="h-7 w-40" variant="text" />
</div> </div>
<div className="relative w-full h-[600px] border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-900 overflow-hidden"> <div className="relative w-full h-[600px] border border-gray-300 dark:border-gray-600 rounded bg-gray-200 dark:bg-gray-900 overflow-hidden">
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className="text-center space-y-4"> <div className="text-center space-y-4">
<div className="relative w-24 h-24 mx-auto"> <div className="relative w-24 h-24 mx-auto">
@ -50,7 +50,7 @@ export function PatternInfoSkeleton() {
return ( return (
<div className="mt-4"> <div className="mt-4">
<SkeletonLoader className="h-6 w-40 mb-4" variant="text" /> <SkeletonLoader className="h-6 w-40 mb-4" variant="text" />
<div className="bg-gray-50 dark:bg-gray-900 p-4 rounded-lg space-y-3"> <div className="bg-gray-200 dark:bg-gray-900 p-4 rounded-lg space-y-3">
{[1, 2, 3, 4].map((i) => ( {[1, 2, 3, 4].map((i) => (
<div key={i} className="flex justify-between"> <div key={i} className="flex justify-between">
<SkeletonLoader className="h-4 w-24" variant="text" /> <SkeletonLoader className="h-4 w-24" variant="text" />
@ -73,7 +73,7 @@ export function MachineConnectionSkeleton() {
<SkeletonLoader className="h-4 w-16" variant="text" /> <SkeletonLoader className="h-4 w-16" variant="text" />
<SkeletonLoader className="h-8 w-32 rounded-lg" /> <SkeletonLoader className="h-8 w-32 rounded-lg" />
</div> </div>
<div className="bg-gray-50 dark:bg-gray-900 p-4 rounded-lg space-y-2"> <div className="bg-gray-200 dark:bg-gray-900 p-4 rounded-lg space-y-2">
<div className="flex justify-between"> <div className="flex justify-between">
<SkeletonLoader className="h-4 w-20" variant="text" /> <SkeletonLoader className="h-4 w-20" variant="text" />
<SkeletonLoader className="h-4 w-24" variant="text" /> <SkeletonLoader className="h-4 w-24" variant="text" />

View file

@ -320,11 +320,11 @@ export function WorkflowStepper() {
return ( return (
<div className="relative max-w-5xl mx-auto mt-2 lg:mt-4" role="navigation" aria-label="Workflow progress"> <div className="relative max-w-5xl mx-auto mt-2 lg:mt-4" role="navigation" aria-label="Workflow progress">
{/* Progress bar background */} {/* Progress bar background */}
<div className="absolute top-4 lg:top-5 left-0 right-0 h-0.5 lg:h-1 bg-blue-400/20 dark:bg-blue-600/20 rounded-full" style={{ left: '16px', right: '16px' }} /> <div className="absolute top-4 lg:top-5 left-0 right-0 h-0.5 lg:h-1 bg-primary-400/20 dark:bg-primary-600/20 rounded-full" style={{ left: '16px', right: '16px' }} />
{/* Progress bar fill */} {/* Progress bar fill */}
<div <div
className="absolute top-4 lg:top-5 left-0 h-0.5 lg: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" className="absolute top-4 lg:top-5 left-0 h-0.5 lg:h-1 bg-gradient-to-r from-success-500 to-primary-500 dark:from-success-600 dark:to-primary-600 transition-all duration-500 rounded-full"
style={{ style={{
left: '16px', left: '16px',
width: `calc(${((currentStep - 1) / (steps.length - 1)) * 100}% - 16px)` width: `calc(${((currentStep - 1) / (steps.length - 1)) * 100}% - 16px)`
@ -358,9 +358,9 @@ export function WorkflowStepper() {
className={` className={`
w-8 h-8 lg:w-10 lg:h-10 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-300 border-2 shadow-md w-8 h-8 lg:w-10 lg:h-10 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-300 border-2 shadow-md
${step.id <= currentStep ? 'cursor-pointer hover:scale-110' : 'cursor-not-allowed'} ${step.id <= currentStep ? 'cursor-pointer hover:scale-110' : 'cursor-not-allowed'}
${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' : ''} ${isComplete ? 'bg-success-500 dark:bg-success-600 border-success-400 dark:border-success-500 text-white shadow-success-500/30 dark:shadow-success-600/30' : ''}
${isCurrent ? 'bg-blue-600 dark:bg-blue-700 border-blue-500 dark:border-blue-600 text-white scale-105 lg: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' : ''} ${isCurrent ? 'bg-primary-600 dark:bg-primary-700 border-primary-500 dark:border-primary-600 text-white scale-105 lg:scale-110 shadow-primary-600/40 dark:shadow-primary-700/40 ring-2 ring-primary-300 dark:ring-primary-500 ring-offset-2 dark:ring-offset-gray-900' : ''}
${isUpcoming ? 'bg-blue-700 dark:bg-blue-800 border-blue-500/30 dark:border-blue-600/30 text-blue-200/70 dark:text-blue-300/70' : ''} ${isUpcoming ? 'bg-primary-700 dark:bg-primary-800 border-primary-500/30 dark:border-primary-600/30 text-primary-200/70 dark:text-primary-300/70' : ''}
${showPopover && popoverStep === step.id ? 'ring-4 ring-white dark:ring-gray-800' : ''} ${showPopover && popoverStep === step.id ? 'ring-4 ring-white dark:ring-gray-800' : ''}
`} `}
aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}. Click for details.`} aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}. Click for details.`}
@ -377,7 +377,7 @@ export function WorkflowStepper() {
{/* Step label */} {/* Step label */}
<div className="mt-1 lg:mt-2 text-center"> <div className="mt-1 lg: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 dark:text-green-300' : 'text-blue-300/70 dark:text-blue-400/70' isCurrent ? 'text-white' : isComplete ? 'text-success-200 dark:text-success-300' : 'text-primary-300/70 dark:text-primary-400/70'
}`}> }`}>
{step.label} {step.label}
</div> </div>
@ -400,35 +400,35 @@ export function WorkflowStepper() {
if (!content) return null; if (!content) return null;
const colorClasses = { const colorClasses = {
info: 'bg-blue-50 dark:bg-blue-900/95 border-blue-600 dark:border-blue-500', info: 'bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500',
success: 'bg-green-50 dark:bg-green-900/95 border-green-600 dark:border-green-500', success: 'bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500',
warning: 'bg-yellow-50 dark:bg-yellow-900/95 border-yellow-600 dark:border-yellow-500', warning: 'bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500',
error: 'bg-red-50 dark:bg-red-900/95 border-red-600 dark:border-red-500', error: 'bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500',
progress: 'bg-cyan-50 dark:bg-cyan-900/95 border-cyan-600 dark:border-cyan-500' progress: 'bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500'
}; };
const iconColorClasses = { const iconColorClasses = {
info: 'text-blue-600 dark:text-blue-400', info: 'text-info-600 dark:text-info-400',
success: 'text-green-600 dark:text-green-400', success: 'text-success-600 dark:text-success-400',
warning: 'text-yellow-600 dark:text-yellow-400', warning: 'text-warning-600 dark:text-warning-400',
error: 'text-red-600 dark:text-red-400', error: 'text-danger-600 dark:text-danger-400',
progress: 'text-cyan-600 dark:text-cyan-400' progress: 'text-info-600 dark:text-info-400'
}; };
const textColorClasses = { const textColorClasses = {
info: 'text-blue-900 dark:text-blue-200', info: 'text-info-900 dark:text-info-200',
success: 'text-green-900 dark:text-green-200', success: 'text-success-900 dark:text-success-200',
warning: 'text-yellow-900 dark:text-yellow-200', warning: 'text-warning-900 dark:text-warning-200',
error: 'text-red-900 dark:text-red-200', error: 'text-danger-900 dark:text-danger-200',
progress: 'text-cyan-900 dark:text-cyan-200' progress: 'text-info-900 dark:text-info-200'
}; };
const descColorClasses = { const descColorClasses = {
info: 'text-blue-800 dark:text-blue-300', info: 'text-info-800 dark:text-info-300',
success: 'text-green-800 dark:text-green-300', success: 'text-success-800 dark:text-success-300',
warning: 'text-yellow-800 dark:text-yellow-300', warning: 'text-warning-800 dark:text-warning-300',
error: 'text-red-800 dark:text-red-300', error: 'text-danger-800 dark:text-danger-300',
progress: 'text-cyan-800 dark:text-cyan-300' progress: 'text-info-800 dark:text-info-300'
}; };
const listColorClasses = { const listColorClasses = {

View file

@ -5,8 +5,15 @@ body {
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5; background-color: var(--color-gray-50);
color: #212529; color: var(--color-gray-900);
}
@media (prefers-color-scheme: dark) {
body {
background-color: var(--color-gray-900);
color: var(--color-gray-50);
}
} }
code { code {

View file

@ -1,135 +0,0 @@
/**
* Design Tokens - Semantic Color System
*
* These tokens provide meaningful names for colors used throughout the app.
* Instead of using arbitrary colors like "blue-600", use semantic names that
* describe the purpose: "primary", "success", "warning", etc.
*/
export const colors = {
// Primary - Main brand color for primary actions
primary: {
DEFAULT: '#2563eb', // blue-600
hover: '#1d4ed8', // blue-700
active: '#1e40af', // blue-800
light: '#dbeafe', // blue-50
border: '#93c5fd', // blue-300
},
// Success - Positive states, completion
success: {
DEFAULT: '#16a34a', // green-600
hover: '#15803d', // green-700
active: '#166534', // green-800
light: '#dcfce7', // green-50
border: '#86efac', // green-300
},
// Warning - Caution, waiting states
warning: {
DEFAULT: '#f59e0b', // amber-500
hover: '#d97706', // amber-600
active: '#b45309', // amber-700
light: '#fef3c7', // amber-50
border: '#fcd34d', // amber-300
},
// Danger - Errors, destructive actions
danger: {
DEFAULT: '#dc2626', // red-600
hover: '#b91c1c', // red-700
active: '#991b1b', // red-800
light: '#fee2e2', // red-50
border: '#fca5a5', // red-300
},
// Info - Informational states
info: {
DEFAULT: '#0891b2', // cyan-600
hover: '#0e7490', // cyan-700
active: '#155e75', // cyan-800
light: '#cffafe', // cyan-50
border: '#67e8f9', // cyan-300
},
// Neutral - Secondary actions, borders, backgrounds
neutral: {
DEFAULT: '#4b5563', // gray-600
hover: '#374151', // gray-700
active: '#1f2937', // gray-800
light: '#f9fafb', // gray-50
border: '#d1d5db', // gray-300
text: '#6b7280', // gray-500
},
};
/**
* Button Classes - Reusable button styles
*/
export const buttonClasses = {
// Base styles for all buttons
base: 'px-4 py-2.5 rounded-lg font-semibold text-sm transition-all duration-150 cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed',
// Primary button
primary: `bg-[${colors.primary.DEFAULT}] text-white hover:bg-[${colors.primary.hover}] active:bg-[${colors.primary.active}] hover:shadow-lg active:scale-[0.98] focus:ring-[${colors.primary.border}]`,
// Success button
success: `bg-[${colors.success.DEFAULT}] text-white hover:bg-[${colors.success.hover}] active:bg-[${colors.success.active}] hover:shadow-lg active:scale-[0.98] focus:ring-[${colors.success.border}]`,
// Warning button
warning: `bg-[${colors.warning.DEFAULT}] text-white hover:bg-[${colors.warning.hover}] active:bg-[${colors.warning.active}] hover:shadow-lg active:scale-[0.98] focus:ring-[${colors.warning.border}]`,
// Danger button
danger: `bg-[${colors.danger.DEFAULT}] text-white hover:bg-[${colors.danger.hover}] active:bg-[${colors.danger.active}] hover:shadow-lg active:scale-[0.98] focus:ring-[${colors.danger.border}]`,
// Secondary/Neutral button
secondary: `bg-[${colors.neutral.DEFAULT}] text-white hover:bg-[${colors.neutral.hover}] active:bg-[${colors.neutral.active}] hover:shadow-lg active:scale-[0.98] focus:ring-[${colors.neutral.border}]`,
};
/**
* Typography Scale
*/
export const typography = {
// Headings
h1: 'text-2xl font-bold',
h2: 'text-xl font-semibold',
h3: 'text-lg font-semibold',
h4: 'text-base font-semibold',
// Body text
body: 'text-sm', // 14px - standard body
bodyLarge: 'text-base', // 16px
bodySmall: 'text-xs', // 12px - minimum size
// Labels
label: 'text-xs font-medium text-gray-600',
// Values
value: 'text-sm font-semibold text-gray-900',
};
/**
* Spacing Scale
*/
export const spacing = {
xs: 'gap-2', // 8px
sm: 'gap-3', // 12px
md: 'gap-4', // 16px
lg: 'gap-6', // 24px
// Padding
paddingXs: 'p-2', // 8px
paddingSm: 'p-3', // 12px
paddingMd: 'p-4', // 16px
paddingLg: 'p-6', // 24px
};
/**
* Alert/Status Box Classes
*/
export const alertClasses = {
success: `bg-[${colors.success.light}] text-green-800 border border-[${colors.success.border}] px-3 py-2 rounded-lg text-sm`,
warning: `bg-[${colors.warning.light}] text-amber-800 border border-[${colors.warning.border}] px-3 py-2 rounded-lg text-sm`,
danger: `bg-[${colors.danger.light}] text-red-800 border border-[${colors.danger.border}] px-3 py-2 rounded-lg text-sm`,
info: `bg-[${colors.info.light}] text-cyan-800 border border-[${colors.info.border}] px-3 py-2 rounded-lg text-sm`,
};

19
src/utils/cssVariables.ts Normal file
View file

@ -0,0 +1,19 @@
/**
* Get CSS variable value from document root
*/
export function getCSSVariable(name: string): string {
return getComputedStyle(document.documentElement)
.getPropertyValue(name)
.trim();
}
/**
* Canvas color helpers
*/
export const canvasColors = {
grid: () => getCSSVariable('--color-canvas-grid'),
origin: () => getCSSVariable('--color-canvas-origin'),
hoop: () => getCSSVariable('--color-canvas-hoop'),
bounds: () => getCSSVariable('--color-canvas-bounds'),
position: () => getCSSVariable('--color-canvas-position'),
};