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,7 +60,8 @@
--radius: 0.5rem; --radius: 0.5rem;
} }
.dark { @media (prefers-color-scheme: dark) {
:root {
--background: 222.2 84% 4.9%; --background: 222.2 84% 4.9%;
--foreground: 210 40% 98%; --foreground: 210 40% 98%;
@ -71,7 +72,7 @@
--popover-foreground: 210 40% 98%; --popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%; /* blue-500 lighter for dark */ --primary: 217.2 91.2% 59.8%; /* blue-500 lighter for dark */
--primary-foreground: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%;
--secondary: 20.5 90.2% 48.2%; /* orange-600 for dark */ --secondary: 20.5 90.2% 48.2%; /* orange-600 for dark */
--secondary-foreground: 210 40% 98%; --secondary-foreground: 210 40% 98%;
@ -98,6 +99,7 @@
--input: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%; --ring: 224.3 76.3% 48%;
} }
}
} }
/* ============================================ /* ============================================

View file

@ -21,6 +21,12 @@ import {
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { createFileService } from "../platform"; import { createFileService } from "../platform";
import type { IFileService } from "../platform/interfaces/IFileService"; 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() { export function FileUpload() {
// Machine store // Machine store
@ -202,12 +208,11 @@ export function FileUpload() {
: "text-gray-600 dark:text-gray-400"; : "text-gray-600 dark:text-gray-400";
return ( return (
<div <Card className={cn("p-0 border-l-4", borderColor)}>
className={`bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 ${borderColor}`} <CardContent className="p-4 rounded-lg">
>
<div className="flex items-start gap-3 mb-3"> <div className="flex items-start gap-3 mb-3">
<DocumentTextIcon <DocumentTextIcon
className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} className={cn("w-6 h-6 flex-shrink-0 mt-0.5", iconColor)}
/> />
<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">
@ -253,40 +258,41 @@ export function FileUpload() {
className="hidden" className="hidden"
disabled={isLoading || patternUploaded || isUploading} disabled={isLoading || patternUploaded || isUploading}
/> />
<label <Button
htmlFor={fileService.hasNativeDialogs() ? undefined : "file-input"} asChild={!fileService.hasNativeDialogs()}
onClick={ onClick={
fileService.hasNativeDialogs() fileService.hasNativeDialogs()
? () => handleFileChange() ? () => handleFileChange()
: undefined : 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 ${ disabled={isLoading || patternUploaded || isUploading}
isLoading || patternUploaded || isUploading variant="outline"
? "opacity-50 cursor-not-allowed bg-gray-400 dark:bg-gray-600 text-white" className="flex-[2]"
: "cursor-pointer bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-600"
}`}
> >
{fileService.hasNativeDialogs() ? (
<>
{isLoading ? ( {isLoading ? (
<> <>
<svg <Loader2 className="w-3.5 h-3.5 animate-spin" />
className="w-3.5 h-3.5 animate-spin" <span>Loading...</span>
fill="none" </>
viewBox="0 0 24 24" ) : patternUploaded ? (
> <>
<circle <CheckCircleIcon className="w-3.5 h-3.5" />
className="opacity-25" <span>Locked</span>
cx="12" </>
cy="12" ) : (
r="10" <>
stroke="currentColor" <FolderOpenIcon className="w-3.5 h-3.5" />
strokeWidth="4" <span>Choose PES File</span>
></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" <label htmlFor="file-input" className="flex items-center gap-2">
></path> {isLoading ? (
</svg> <>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
<span>Loading...</span> <span>Loading...</span>
</> </>
) : patternUploaded ? ( ) : patternUploaded ? (
@ -301,15 +307,17 @@ export function FileUpload() {
</> </>
)} )}
</label> </label>
)}
</Button>
{pesData && {pesData &&
canUploadPattern(machineStatus) && canUploadPattern(machineStatus) &&
!patternUploaded && !patternUploaded &&
uploadProgress < 100 && ( uploadProgress < 100 && (
<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-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={ aria-label={
isUploading isUploading
? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete` ? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete`
@ -318,36 +326,18 @@ export function FileUpload() {
> >
{isUploading ? ( {isUploading ? (
<> <>
<svg <Loader2 className="w-3.5 h-3.5 animate-spin" />
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>
{uploadProgress > 0 {uploadProgress > 0
? uploadProgress.toFixed(0) + "%" ? uploadProgress.toFixed(0) + "%"
: "Uploading"} : "Uploading"}
</> </>
) : ( ) : (
<> <>
<ArrowUpTrayIcon className="w-3.5 h-3.5 inline mr-1" /> <ArrowUpTrayIcon className="w-3.5 h-3.5" />
Upload Upload
</> </>
)} )}
</button> </Button>
)} )}
</div> </div>
@ -364,12 +354,7 @@ export function FileUpload() {
{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"> <Progress value={pyodideProgress} className="h-2.5" />
<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>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1.5 italic"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1.5 italic">
{isLoading && !pyodideReady {isLoading && !pyodideReady
? "File dialog will open automatically when ready" ? "File dialog will open automatically when ready"
@ -393,15 +378,22 @@ export function FileUpload() {
}} }}
> >
{pesData && !canUploadPattern(machineStatus) && ( {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"> <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)} Cannot upload while {getMachineStateCategory(machineStatus)}
</div> </AlertDescription>
</Alert>
)} )}
{pesData && boundsCheck.error && ( {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"> <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} <strong>Pattern too large:</strong> {boundsCheck.error}
</div> </AlertDescription>
</Alert>
)} )}
</div> </div>
@ -417,14 +409,13 @@ export function FileUpload() {
: "Starting..."} : "Starting..."}
</span> </span>
</div> </div>
<div className="h-2.5 bg-gray-300 dark:bg-gray-600 rounded-full overflow-hidden shadow-inner relative"> <Progress
<div value={uploadProgress}
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" 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"
style={{ width: `${uploadProgress}%` }}
/> />
</div> </div>
</div>
)} )}
</div> </CardContent>
</Card>
); );
} }

View file

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