feature: Migrate FileUpload to shadcn/ui and fix dark mode

Migrated FileUpload component to use shadcn/ui Card, Button, Alert, and Progress components. Updated dark mode CSS variables to use media query approach for automatic system theme detection. Fixed Card component padding and button styling for better visual consistency.

Changes:
- Replaced custom div with shadcn Card and CardContent components
- Migrated buttons to shadcn Button component with outline variant for Choose File
- Replaced custom alerts with shadcn Alert components
- Replaced custom progress bars with shadcn Progress component
- Fixed Card padding by adding p-0 to Card and rounded-lg to CardContent
- Changed dark mode from .dark class to @media (prefers-color-scheme: dark)
- Fixed primary-foreground color in dark mode for proper white text contrast

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2025-12-20 19:38:27 +01:00
parent bd80e95004
commit ed3950b5d0
3 changed files with 125 additions and 132 deletions

View file

@ -60,43 +60,45 @@
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
@media (prefers-color-scheme: dark) {
:root {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%; /* blue-500 lighter for dark */
--primary-foreground: 222.2 47.4% 11.2%;
--primary: 217.2 91.2% 59.8%; /* blue-500 lighter for dark */
--primary-foreground: 210 40% 98%;
--secondary: 20.5 90.2% 48.2%; /* orange-600 for dark */
--secondary-foreground: 210 40% 98%;
--secondary: 20.5 90.2% 48.2%; /* orange-600 for dark */
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 263.4 70% 50.4%; /* purple-600 for dark */
--accent-foreground: 210 40% 98%;
--accent: 263.4 70% 50.4%; /* purple-600 for dark */
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%; /* red-900 */
--destructive-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%; /* red-900 */
--destructive-foreground: 210 40% 98%;
--success: 142.1 70.6% 45.3%; /* green-500 for dark */
--success-foreground: 210 40% 98%;
--success: 142.1 70.6% 45.3%; /* green-500 for dark */
--success-foreground: 210 40% 98%;
--warning: 47.9 95.8% 53.1%; /* amber-400 for dark */
--warning-foreground: 26 83.3% 14.1%;
--warning: 47.9 95.8% 53.1%; /* amber-400 for dark */
--warning-foreground: 26 83.3% 14.1%;
--info: 188.7 85.7% 53.3%; /* cyan-500 */
--info-foreground: 210 40% 98%;
--info: 188.7 85.7% 53.3%; /* cyan-500 */
--info-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
}
}

View file

@ -21,6 +21,12 @@ import {
} from "@heroicons/react/24/solid";
import { createFileService } from "../platform";
import type { IFileService } from "../platform/interfaces/IFileService";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Progress } from "@/components/ui/progress";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
export function FileUpload() {
// Machine store
@ -202,31 +208,30 @@ export function FileUpload() {
: "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}`}
>
<div className="flex items-start gap-3 mb-3">
<DocumentTextIcon
className={`w-6 h-6 ${iconColor} 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">
Pattern File
</h3>
{pesData && displayFileName ? (
<p
className="text-xs text-gray-600 dark:text-gray-400 truncate"
title={displayFileName}
>
{displayFileName}
</p>
) : (
<p className="text-xs text-gray-600 dark:text-gray-400">
No pattern loaded
</p>
)}
<Card className={cn("p-0 border-l-4", borderColor)}>
<CardContent className="p-4 rounded-lg">
<div className="flex items-start gap-3 mb-3">
<DocumentTextIcon
className={cn("w-6 h-6 flex-shrink-0 mt-0.5", iconColor)}
/>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">
Pattern File
</h3>
{pesData && displayFileName ? (
<p
className="text-xs text-gray-600 dark:text-gray-400 truncate"
title={displayFileName}
>
{displayFileName}
</p>
) : (
<p className="text-xs text-gray-600 dark:text-gray-400">
No pattern loaded
</p>
)}
</div>
</div>
</div>
{resumeAvailable && resumeFileName && (
<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">
@ -253,63 +258,66 @@ export function FileUpload() {
className="hidden"
disabled={isLoading || patternUploaded || isUploading}
/>
<label
htmlFor={fileService.hasNativeDialogs() ? undefined : "file-input"}
<Button
asChild={!fileService.hasNativeDialogs()}
onClick={
fileService.hasNativeDialogs()
? () => handleFileChange()
: undefined
}
className={`flex-[2] flex items-center justify-center gap-2 px-3 py-2.5 sm:py-2 rounded font-semibold text-sm transition-all ${
isLoading || patternUploaded || isUploading
? "opacity-50 cursor-not-allowed bg-gray-400 dark:bg-gray-600 text-white"
: "cursor-pointer bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-600"
}`}
disabled={isLoading || patternUploaded || isUploading}
variant="outline"
className="flex-[2]"
>
{isLoading ? (
{fileService.hasNativeDialogs() ? (
<>
<svg
className="w-3.5 h-3.5 animate-spin"
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>
<span>Loading...</span>
</>
) : patternUploaded ? (
<>
<CheckCircleIcon className="w-3.5 h-3.5" />
<span>Locked</span>
{isLoading ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
<span>Loading...</span>
</>
) : patternUploaded ? (
<>
<CheckCircleIcon className="w-3.5 h-3.5" />
<span>Locked</span>
</>
) : (
<>
<FolderOpenIcon className="w-3.5 h-3.5" />
<span>Choose PES File</span>
</>
)}
</>
) : (
<>
<FolderOpenIcon className="w-3.5 h-3.5" />
<span>Choose PES File</span>
</>
<label htmlFor="file-input" className="flex items-center gap-2">
{isLoading ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
<span>Loading...</span>
</>
) : patternUploaded ? (
<>
<CheckCircleIcon className="w-3.5 h-3.5" />
<span>Locked</span>
</>
) : (
<>
<FolderOpenIcon className="w-3.5 h-3.5" />
<span>Choose PES File</span>
</>
)}
</label>
)}
</label>
</Button>
{pesData &&
canUploadPattern(machineStatus) &&
!patternUploaded &&
uploadProgress < 100 && (
<button
<Button
onClick={handleUpload}
disabled={!isConnected || isUploading || !boundsCheck.fits}
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"
className="flex-1"
aria-label={
isUploading
? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete`
@ -318,36 +326,18 @@ export function FileUpload() {
>
{isUploading ? (
<>
<svg
className="w-3.5 h-3.5 animate-spin inline mr-1"
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>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
{uploadProgress > 0
? uploadProgress.toFixed(0) + "%"
: "Uploading"}
</>
) : (
<>
<ArrowUpTrayIcon className="w-3.5 h-3.5 inline mr-1" />
<ArrowUpTrayIcon className="w-3.5 h-3.5" />
Upload
</>
)}
</button>
</Button>
)}
</div>
@ -364,12 +354,7 @@ export function FileUpload() {
{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-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>
<Progress value={pyodideProgress} className="h-2.5" />
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1.5 italic">
{isLoading && !pyodideReady
? "File dialog will open automatically when ready"
@ -393,15 +378,22 @@ export function FileUpload() {
}}
>
{pesData && !canUploadPattern(machineStatus) && (
<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>
<Alert className="bg-warning-100 dark:bg-warning-900/20 border-warning-200 dark:border-warning-800">
<AlertDescription className="text-warning-800 dark:text-warning-200 text-sm">
Cannot upload while {getMachineStateCategory(machineStatus)}
</AlertDescription>
</Alert>
)}
{pesData && boundsCheck.error && (
<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>
<Alert
variant="destructive"
className="bg-danger-100 dark:bg-danger-900/20 border-danger-200 dark:border-danger-800"
>
<AlertDescription className="text-danger-800 dark:text-danger-200 text-sm">
<strong>Pattern too large:</strong> {boundsCheck.error}
</AlertDescription>
</Alert>
)}
</div>
@ -417,14 +409,13 @@ export function FileUpload() {
: "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-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>
<Progress
value={uploadProgress}
className="h-2.5 [&>div]:bg-gradient-to-r [&>div]:from-secondary-500 [&>div]:via-secondary-600 [&>div]:to-secondary-700 dark:[&>div]:from-secondary-600 dark:[&>div]:via-secondary-700 dark:[&>div]:to-secondary-800"
/>
</div>
)}
</div>
</CardContent>
</Card>
);
}

View file

@ -30,8 +30,8 @@ export function PatternSummaryCard() {
const canDelete = canDeletePattern(machineStatus);
return (
<Card className="border-l-4 border-primary-600 dark:border-primary-500">
<CardContent className="p-4">
<Card className="p-0 border-l-4 border-primary-600 dark:border-primary-500">
<CardContent className="p-4 rounded-lg">
<div className="flex items-start gap-3 mb-3">
<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">