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";
/* 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 {
0% {
transform: translateX(-100%);
@ -44,13 +167,13 @@
}
}
/* Pulse glow effect */
/* Pulse glow effect - uses primary-600 */
@keyframes pulseGlow {
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% {
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]);
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 />
<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]);
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">
{/* Machine Connection Status - Responsive width column */}
<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="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h1 className="text-lg lg:text-xl font-bold text-white leading-tight">Respira</h1>
{isConnected && machineInfo?.serialNumber && (
<span
className="text-xs text-blue-200 cursor-help"
className="text-xs text-primary-200 cursor-help"
title={`Serial: ${machineInfo.serialNumber}${
machineInfo.macAddress
? `\nMAC: ${machineInfo.macAddress}`
@ -116,7 +116,7 @@ export function AppHeader() {
</span>
)}
{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 className="flex items-center gap-2 mt-1 min-h-[32px]">
@ -124,7 +124,7 @@ export function AppHeader() {
<>
<button
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"
aria-label="Disconnect from machine"
>
@ -137,7 +137,7 @@ export function AppHeader() {
</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 */}
@ -145,7 +145,7 @@ export function AppHeader() {
<button
ref={errorButtonRef}
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)
? 'cursor-pointer animate-pulse hover:animate-none'
: 'invisible pointer-events-none'

View file

@ -59,7 +59,7 @@ export function BluetoothDevicePicker() {
return (
<div className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-[1000]" onClick={handleCancel}>
<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()}
role="dialog"
aria-labelledby="bluetooth-picker-title"
@ -73,7 +73,7 @@ export function BluetoothDevicePicker() {
<div className="p-6">
{isScanning && devices.length === 0 ? (
<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>
<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>
@ -89,7 +89,7 @@ export function BluetoothDevicePicker() {
<button
key={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}`}
>
<div className="font-semibold text-gray-900 dark:text-white">{device.deviceName}</div>

View file

@ -40,7 +40,7 @@ export function ConfirmDialog({
return (
<div className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-[1000]" onClick={onCancel}>
<div
className={`bg-white 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()}
role="dialog"
aria-labelledby="dialog-title"
@ -65,8 +65,8 @@ export function ConfirmDialog({
onClick={onConfirm}
className={
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-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-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-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}`}
>

View file

@ -26,7 +26,7 @@ export function ConnectionPrompt() {
</div>
<button
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
</button>
@ -35,17 +35,17 @@ export function ConnectionPrompt() {
}
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">
<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">
<h3 className="text-base font-semibold text-amber-900 dark:text-amber-100 mb-2">Browser Not Supported</h3>
<p className="text-sm text-amber-800 dark:text-amber-200 mb-3">
<h3 className="text-base font-semibold text-warning-900 dark:text-warning-100 mb-2">Browser Not Supported</h3>
<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.
</p>
<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>
<ul className="text-sm text-amber-800 dark:text-amber-200 space-y-1.5 ml-4 list-disc">
<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-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>
Download the Desktop app from{' '}
@ -53,7 +53,7 @@ export function ConnectionPrompt() {
href="https://github.com/jhbruhn/respira/releases/latest"
target="_blank"
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
</a>

View file

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

View file

@ -171,8 +171,8 @@ export function FileUpload() {
const boundsCheck = checkPatternFitsInHoop();
const borderColor = pesData ? 'border-orange-600 dark:border-orange-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 borderColor = pesData ? 'border-secondary-600 dark:border-secondary-500' : 'border-gray-400 dark:border-gray-600';
const iconColor = pesData ? 'text-secondary-600 dark:text-secondary-400' : 'text-gray-600 dark:text-gray-400';
return (
<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>
{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">
<p className="text-xs text-green-800 dark:text-green-200">
<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-success-800 dark:text-success-200">
<strong>Cached:</strong> "{resumeFileName}"
</p>
</div>
@ -249,7 +249,7 @@ export function FileUpload() {
<button
onClick={handleUpload}
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'}
>
{isUploading ? (
@ -279,13 +279,13 @@ export function FileUpload() {
? 'Please wait - initializing Python environment...'
: pyodideLoadingStep || 'Initializing Python environment...'}
</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)}%
</span>
</div>
<div className="h-2.5 bg-gray-300 dark:bg-gray-600 rounded-full overflow-hidden shadow-inner relative">
<div
className="h-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 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}%` }}
/>
</div>
@ -303,13 +303,13 @@ export function FileUpload() {
marginTop: (pesData && (boundsCheck.error || !canUploadPattern(machineStatus))) ? '12px' : '0px'
}}>
{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)}
</div>
)}
{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}
</div>
)}
@ -319,13 +319,13 @@ export function FileUpload() {
<div className="mt-3">
<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-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...'}
</span>
</div>
<div className="h-2.5 bg-gray-300 dark:bg-gray-600 rounded-full overflow-hidden shadow-inner relative">
<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}%` }}
/>
</div>

View file

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

View file

@ -61,16 +61,16 @@ export function MachineConnection({
};
const statusBadgeColors = {
idle: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300',
info: 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300',
active: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300',
waiting: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300',
warning: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300',
complete: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300',
success: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300',
interrupted: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300',
error: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300',
danger: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300',
idle: 'bg-info-100 dark:bg-info-900/30 text-info-800 dark:text-info-300',
info: 'bg-info-100 dark:bg-info-900/30 text-info-800 dark:text-info-300',
active: 'bg-warning-100 dark:bg-warning-900/30 text-warning-800 dark:text-warning-300',
waiting: 'bg-warning-100 dark:bg-warning-900/30 text-warning-800 dark:text-warning-300',
warning: 'bg-warning-100 dark:bg-warning-900/30 text-warning-800 dark:text-warning-300',
complete: 'bg-success-100 dark:bg-success-900/30 text-success-800 dark:text-success-300',
success: 'bg-success-100 dark:bg-success-900/30 text-success-800 dark:text-success-300',
interrupted: 'bg-danger-100 dark:bg-danger-900/30 text-danger-800 dark:text-danger-300',
error: 'bg-danger-100 dark:bg-danger-900/30 text-danger-800 dark:text-danger-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
@ -90,15 +90,15 @@ export function MachineConnection({
<button
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
</button>
</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">
<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">
<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">
@ -110,21 +110,21 @@ export function MachineConnection({
{/* Error/Info Display */}
{errorInfo && (
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">
<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="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 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">
<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="font-semibold text-red-900 dark:text-red-200 text-xs mb-1">{errorInfo.title}</div>
<div className="text-xs text-red-700 dark:text-red-300 font-mono">
<div className="font-semibold text-danger-900 dark:text-danger-200 text-xs mb-1">{errorInfo.title}</div>
<div className="text-xs text-danger-700 dark:text-danger-300 font-mono">
Error Code: 0x{machineError.toString(16).toUpperCase().padStart(2, '0')}
</div>
</div>
@ -148,14 +148,14 @@ export function MachineConnection({
{/* Machine Info */}
{machineInfo && (
<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="font-semibold text-gray-900 dark:text-gray-100">
{(machineInfo.maxWidth / 10).toFixed(1)} × {(machineInfo.maxHeight / 10).toFixed(1)} mm
</span>
</div>
{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="font-semibold text-gray-900 dark:text-gray-100">
{machineInfo.totalCount.toLocaleString()}

View file

@ -213,8 +213,8 @@ export function PatternCanvas() {
setPatternOffset(newOffset.x, newOffset.y);
}, [setPatternOffset]);
const borderColor = pesData ? 'border-teal-600 dark:border-teal-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 borderColor = pesData ? 'border-tertiary-600 dark:border-tertiary-500' : 'border-gray-400 dark:border-gray-600';
const iconColor = pesData ? 'text-tertiary-600 dark:text-tertiary-400' : 'text-gray-600 dark:text-gray-400';
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`}>
@ -231,7 +231,7 @@ export function PatternCanvas() {
)}
</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 && (
<Stage
width={containerSize.width}
@ -392,7 +392,7 @@ export function PatternCanvas() {
</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
</div>
<div className="text-xs text-gray-600 dark:text-gray-400 italic">
@ -402,17 +402,17 @@ export function PatternCanvas() {
{/* 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">
<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" />
</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" />
</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>
<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" />
</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" />
</button>
</div>

View file

@ -9,14 +9,14 @@ export function PatternInfo({ pesData, showThreadBlocks = false }: PatternInfoPr
return (
<>
<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="font-semibold text-gray-900 dark:text-gray-100">
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{' '}
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
</span>
</div>
<div className="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="font-semibold text-gray-900 dark:text-gray-100">
{pesData.penStitches?.stitches.length.toLocaleString() || pesData.stitchCount.toLocaleString()}
@ -30,7 +30,7 @@ export function PatternInfo({ pesData, showThreadBlocks = false }: PatternInfoPr
)}
</span>
</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">
{showThreadBlocks ? 'Colors / Blocks' : 'Colors'}
</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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<div className="absolute -top-2 -right-2 w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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-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" />
</svg>
</div>
@ -27,15 +27,15 @@ export function PatternPreviewPlaceholder() {
</p>
<div className="flex items-center justify-center gap-6 text-xs text-gray-400 dark:text-gray-500">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-blue-400 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>
</div>
<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>
</div>
<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>
</div>
</div>

View file

@ -34,9 +34,9 @@ export function PatternSummaryCard() {
const canDelete = canDeletePattern(machineStatus);
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">
<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">
<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}>
@ -51,7 +51,7 @@ export function PatternSummaryCard() {
<button
onClick={deletePattern}
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 ? (
<>

View file

@ -154,22 +154,22 @@ export function ProgressMonitor() {
}, [colorBlocks]);
const stateIndicatorColors = {
idle: "bg-blue-50 dark:bg-blue-900/20 border-blue-600",
info: "bg-blue-50 dark:bg-blue-900/20 border-blue-600",
active: "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500",
waiting: "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500",
warning: "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500",
complete: "bg-green-50 dark:bg-green-900/20 border-green-600",
success: "bg-green-50 dark:bg-green-900/20 border-green-600",
interrupted: "bg-red-50 dark:bg-red-900/20 border-red-600",
error: "bg-red-50 dark:bg-red-900/20 border-red-600",
danger: "bg-red-50 dark:bg-red-900/20 border-red-600",
idle: "bg-info-50 dark:bg-info-900/20 border-info-600",
info: "bg-info-50 dark:bg-info-900/20 border-info-600",
active: "bg-warning-50 dark:bg-warning-900/20 border-warning-500",
waiting: "bg-warning-50 dark:bg-warning-900/20 border-warning-500",
warning: "bg-warning-50 dark:bg-warning-900/20 border-warning-500",
complete: "bg-success-50 dark:bg-success-900/20 border-success-600",
success: "bg-success-50 dark:bg-success-900/20 border-success-600",
interrupted: "bg-danger-50 dark:bg-danger-900/20 border-danger-600",
error: "bg-danger-50 dark:bg-danger-900/20 border-danger-600",
danger: "bg-danger-50 dark:bg-danger-900/20 border-danger-600",
};
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">
<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">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">
Sewing Progress
@ -185,7 +185,7 @@ export function ProgressMonitor() {
{/* Pattern Info */}
{patternInfo && (
<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">
Total Stitches
</span>
@ -193,7 +193,7 @@ export function ProgressMonitor() {
{totalStitches.toLocaleString()}
</span>
</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">
Total Time
</span>
@ -201,7 +201,7 @@ export function ProgressMonitor() {
{totalMinutes} min
</span>
</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">
Speed
</span>
@ -217,13 +217,13 @@ export function ProgressMonitor() {
<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-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}%` }}
/>
</div>
<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">
Current Stitch
</span>
@ -232,7 +232,7 @@ export function ProgressMonitor() {
{totalStitches.toLocaleString()}
</span>
</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">
Time
</span>
@ -249,22 +249,22 @@ export function ProgressMonitor() {
(() => {
const iconMap = {
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: (
<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: (
<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: (
<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: (
<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: (
<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
ref={colorBlocksScrollRef}
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) => {
const isCompleted = currentStitch >= block.endStitch;
@ -319,10 +319,10 @@ export function ProgressMonitor() {
ref={isCurrent ? currentBlockRef : null}
className={`p-2.5 rounded-lg border-2 transition-all duration-300 ${
isCompleted
? "border-green-600 bg-green-50 dark:bg-green-900/20"
? "border-success-600 bg-success-50 dark:bg-success-900/20"
: isCurrent
? "border-purple-600 bg-purple-50 dark:bg-purple-900/20 shadow-lg shadow-purple-600/20 animate-pulseGlow"
: "border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50 opacity-70"
? "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-300 dark:bg-gray-800/50 opacity-70"
}`}
role="listitem"
aria-label={`Thread ${block.colorIndex + 1}, ${block.stitchCount} stitches, ${isCompleted ? "completed" : isCurrent ? "in progress" : "pending"}`}
@ -374,12 +374,12 @@ export function ProgressMonitor() {
{/* Status icon */}
{isCompleted ? (
<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"
/>
) : isCurrent ? (
<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"
/>
) : (
@ -394,7 +394,7 @@ export function ProgressMonitor() {
{isCurrent && (
<div className="mt-2 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<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}%` }}
role="progressbar"
aria-valuenow={Math.round(blockProgress)}
@ -423,7 +423,7 @@ export function ProgressMonitor() {
<button
onClick={resumeSewing}
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"
>
<PlayIcon className="w-3.5 h-3.5" />
@ -436,7 +436,7 @@ export function ProgressMonitor() {
<button
onClick={startSewing}
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"
>
<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">
<SkeletonLoader className="h-7 w-40" variant="text" />
</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="text-center space-y-4">
<div className="relative w-24 h-24 mx-auto">
@ -50,7 +50,7 @@ export function PatternInfoSkeleton() {
return (
<div className="mt-4">
<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) => (
<div key={i} className="flex justify-between">
<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-8 w-32 rounded-lg" />
</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">
<SkeletonLoader className="h-4 w-20" variant="text" />
<SkeletonLoader className="h-4 w-24" variant="text" />

View file

@ -320,11 +320,11 @@ export function WorkflowStepper() {
return (
<div className="relative max-w-5xl mx-auto mt-2 lg:mt-4" role="navigation" aria-label="Workflow progress">
{/* 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 */}
<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={{
left: '16px',
width: `calc(${((currentStep - 1) / (steps.length - 1)) * 100}% - 16px)`
@ -358,9 +358,9 @@ export function WorkflowStepper() {
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
${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' : ''}
${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' : ''}
${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' : ''}
${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-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-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' : ''}
`}
aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}. Click for details.`}
@ -377,7 +377,7 @@ export function WorkflowStepper() {
{/* Step label */}
<div className="mt-1 lg:mt-2 text-center">
<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}
</div>
@ -400,35 +400,35 @@ export function WorkflowStepper() {
if (!content) return null;
const colorClasses = {
info: 'bg-blue-50 dark:bg-blue-900/95 border-blue-600 dark:border-blue-500',
success: 'bg-green-50 dark:bg-green-900/95 border-green-600 dark:border-green-500',
warning: 'bg-yellow-50 dark:bg-yellow-900/95 border-yellow-600 dark:border-yellow-500',
error: 'bg-red-50 dark:bg-red-900/95 border-red-600 dark:border-red-500',
progress: 'bg-cyan-50 dark:bg-cyan-900/95 border-cyan-600 dark:border-cyan-500'
info: 'bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500',
success: 'bg-success-50 dark:bg-success-900/95 border-success-600 dark:border-success-500',
warning: 'bg-warning-50 dark:bg-warning-900/95 border-warning-600 dark:border-warning-500',
error: 'bg-danger-50 dark:bg-danger-900/95 border-danger-600 dark:border-danger-500',
progress: 'bg-info-50 dark:bg-info-900/95 border-info-600 dark:border-info-500'
};
const iconColorClasses = {
info: 'text-blue-600 dark:text-blue-400',
success: 'text-green-600 dark:text-green-400',
warning: 'text-yellow-600 dark:text-yellow-400',
error: 'text-red-600 dark:text-red-400',
progress: 'text-cyan-600 dark:text-cyan-400'
info: 'text-info-600 dark:text-info-400',
success: 'text-success-600 dark:text-success-400',
warning: 'text-warning-600 dark:text-warning-400',
error: 'text-danger-600 dark:text-danger-400',
progress: 'text-info-600 dark:text-info-400'
};
const textColorClasses = {
info: 'text-blue-900 dark:text-blue-200',
success: 'text-green-900 dark:text-green-200',
warning: 'text-yellow-900 dark:text-yellow-200',
error: 'text-red-900 dark:text-red-200',
progress: 'text-cyan-900 dark:text-cyan-200'
info: 'text-info-900 dark:text-info-200',
success: 'text-success-900 dark:text-success-200',
warning: 'text-warning-900 dark:text-warning-200',
error: 'text-danger-900 dark:text-danger-200',
progress: 'text-info-900 dark:text-info-200'
};
const descColorClasses = {
info: 'text-blue-800 dark:text-blue-300',
success: 'text-green-800 dark:text-green-300',
warning: 'text-yellow-800 dark:text-yellow-300',
error: 'text-red-800 dark:text-red-300',
progress: 'text-cyan-800 dark:text-cyan-300'
info: 'text-info-800 dark:text-info-300',
success: 'text-success-800 dark:text-success-300',
warning: 'text-warning-800 dark:text-warning-300',
error: 'text-danger-800 dark:text-danger-300',
progress: 'text-info-800 dark:text-info-300'
};
const listColorClasses = {

View file

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