mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
fix: Run prettier formatting on all components
- Applied prettier auto-formatting to all component and utility files - Fixed semicolons, commas, and indentation throughout codebase - No functional changes, only code style improvements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7cf4a5de17
commit
3ca5edf4dc
17 changed files with 577 additions and 564 deletions
|
|
@ -167,7 +167,7 @@ export function AppHeader() {
|
||||||
"gap-1.5 flex-shrink-0",
|
"gap-1.5 flex-shrink-0",
|
||||||
machineErrorMessage || pyodideError
|
machineErrorMessage || pyodideError
|
||||||
? "animate-pulse hover:animate-none"
|
? "animate-pulse hover:animate-none"
|
||||||
: "invisible pointer-events-none"
|
: "invisible pointer-events-none",
|
||||||
)}
|
)}
|
||||||
title="Click to view error details"
|
title="Click to view error details"
|
||||||
aria-label="View error details"
|
aria-label="View error details"
|
||||||
|
|
|
||||||
|
|
@ -233,188 +233,188 @@ export function FileUpload() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{resumeAvailable && resumeFileName && (
|
{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">
|
<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">
|
<p className="text-xs text-success-800 dark:text-success-200">
|
||||||
<strong>Cached:</strong> "{resumeFileName}"
|
<strong>Cached:</strong> "{resumeFileName}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isLoading && <PatternInfoSkeleton />}
|
|
||||||
|
|
||||||
{!isLoading && pesData && (
|
|
||||||
<div className="mb-3">
|
|
||||||
<PatternInfo pesData={pesData} showThreadBlocks />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-2 mb-3">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".pes"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
id="file-input"
|
|
||||||
className="hidden"
|
|
||||||
disabled={isLoading || patternUploaded || isUploading}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
asChild={!fileService.hasNativeDialogs()}
|
|
||||||
onClick={
|
|
||||||
fileService.hasNativeDialogs()
|
|
||||||
? () => handleFileChange()
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
disabled={isLoading || patternUploaded || isUploading}
|
|
||||||
variant="outline"
|
|
||||||
className="flex-[2]"
|
|
||||||
>
|
|
||||||
{fileService.hasNativeDialogs() ? (
|
|
||||||
<>
|
|
||||||
{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 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>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{pesData &&
|
|
||||||
canUploadPattern(machineStatus) &&
|
|
||||||
!patternUploaded &&
|
|
||||||
uploadProgress < 100 && (
|
|
||||||
<Button
|
|
||||||
onClick={handleUpload}
|
|
||||||
disabled={!isConnected || isUploading || !boundsCheck.fits}
|
|
||||||
className="flex-1"
|
|
||||||
aria-label={
|
|
||||||
isUploading
|
|
||||||
? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete`
|
|
||||||
: boundsCheck.error || "Upload pattern to machine"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isUploading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
||||||
{uploadProgress > 0
|
|
||||||
? uploadProgress.toFixed(0) + "%"
|
|
||||||
: "Uploading"}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowUpTrayIcon className="w-3.5 h-3.5" />
|
|
||||||
Upload
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pyodide initialization progress indicator - shown when initializing or waiting */}
|
|
||||||
{!pyodideReady && pyodideProgress > 0 && (
|
|
||||||
<div className="mb-3">
|
|
||||||
<div className="flex justify-between items-center mb-1.5">
|
|
||||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
{isLoading && !pyodideReady
|
|
||||||
? "Please wait - initializing Python environment..."
|
|
||||||
: pyodideLoadingStep || "Initializing Python environment..."}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs font-bold text-primary-600 dark:text-primary-400">
|
|
||||||
{pyodideProgress.toFixed(0)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</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"
|
|
||||||
: "This only happens once on first use"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error/warning messages with smooth transition - placed after buttons */}
|
|
||||||
<div
|
|
||||||
className="transition-all duration-200 ease-in-out overflow-hidden"
|
|
||||||
style={{
|
|
||||||
maxHeight:
|
|
||||||
pesData && (boundsCheck.error || !canUploadPattern(machineStatus))
|
|
||||||
? "200px"
|
|
||||||
: "0px",
|
|
||||||
marginTop:
|
|
||||||
pesData && (boundsCheck.error || !canUploadPattern(machineStatus))
|
|
||||||
? "12px"
|
|
||||||
: "0px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{pesData && !canUploadPattern(machineStatus) && (
|
|
||||||
<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 && (
|
{isLoading && <PatternInfoSkeleton />}
|
||||||
<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>
|
|
||||||
|
|
||||||
{isUploading && uploadProgress < 100 && (
|
{!isLoading && pesData && (
|
||||||
<div className="mt-3">
|
<div className="mb-3">
|
||||||
<div className="flex justify-between items-center mb-1.5">
|
<PatternInfo pesData={pesData} showThreadBlocks />
|
||||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Uploading
|
|
||||||
</span>
|
|
||||||
<span className="text-xs font-bold text-secondary-600 dark:text-secondary-400">
|
|
||||||
{uploadProgress > 0
|
|
||||||
? uploadProgress.toFixed(1) + "%"
|
|
||||||
: "Starting..."}
|
|
||||||
</span>
|
|
||||||
</div>
|
</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 className="flex gap-2 mb-3">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".pes"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
id="file-input"
|
||||||
|
className="hidden"
|
||||||
|
disabled={isLoading || patternUploaded || isUploading}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
asChild={!fileService.hasNativeDialogs()}
|
||||||
|
onClick={
|
||||||
|
fileService.hasNativeDialogs()
|
||||||
|
? () => handleFileChange()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
disabled={isLoading || patternUploaded || isUploading}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-[2]"
|
||||||
|
>
|
||||||
|
{fileService.hasNativeDialogs() ? (
|
||||||
|
<>
|
||||||
|
{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 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>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{pesData &&
|
||||||
|
canUploadPattern(machineStatus) &&
|
||||||
|
!patternUploaded &&
|
||||||
|
uploadProgress < 100 && (
|
||||||
|
<Button
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={!isConnected || isUploading || !boundsCheck.fits}
|
||||||
|
className="flex-1"
|
||||||
|
aria-label={
|
||||||
|
isUploading
|
||||||
|
? `Uploading pattern: ${uploadProgress.toFixed(0)}% complete`
|
||||||
|
: boundsCheck.error || "Upload pattern to machine"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
{uploadProgress > 0
|
||||||
|
? uploadProgress.toFixed(0) + "%"
|
||||||
|
: "Uploading"}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowUpTrayIcon className="w-3.5 h-3.5" />
|
||||||
|
Upload
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{/* Pyodide initialization progress indicator - shown when initializing or waiting */}
|
||||||
|
{!pyodideReady && pyodideProgress > 0 && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex justify-between items-center mb-1.5">
|
||||||
|
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
{isLoading && !pyodideReady
|
||||||
|
? "Please wait - initializing Python environment..."
|
||||||
|
: pyodideLoadingStep || "Initializing Python environment..."}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-bold text-primary-600 dark:text-primary-400">
|
||||||
|
{pyodideProgress.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</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"
|
||||||
|
: "This only happens once on first use"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error/warning messages with smooth transition - placed after buttons */}
|
||||||
|
<div
|
||||||
|
className="transition-all duration-200 ease-in-out overflow-hidden"
|
||||||
|
style={{
|
||||||
|
maxHeight:
|
||||||
|
pesData && (boundsCheck.error || !canUploadPattern(machineStatus))
|
||||||
|
? "200px"
|
||||||
|
: "0px",
|
||||||
|
marginTop:
|
||||||
|
pesData && (boundsCheck.error || !canUploadPattern(machineStatus))
|
||||||
|
? "12px"
|
||||||
|
: "0px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pesData && !canUploadPattern(machineStatus) && (
|
||||||
|
<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 && (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{isUploading && uploadProgress < 100 && (
|
||||||
|
<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-secondary-600 dark:text-secondary-400">
|
||||||
|
{uploadProgress > 0
|
||||||
|
? uploadProgress.toFixed(1) + "%"
|
||||||
|
: "Starting..."}
|
||||||
|
</span>
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,18 @@ export function PatternInfo({
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded cursor-help">
|
<div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded cursor-help">
|
||||||
<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(
|
||||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
1,
|
||||||
|
)}{" "}
|
||||||
|
x{" "}
|
||||||
|
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(
|
||||||
|
1,
|
||||||
|
)}{" "}
|
||||||
|
mm
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
@ -45,7 +53,8 @@ export function PatternInfo({
|
||||||
{pesData.penStitches?.stitches.length.toLocaleString() ||
|
{pesData.penStitches?.stitches.length.toLocaleString() ||
|
||||||
pesData.stitchCount.toLocaleString()}
|
pesData.stitchCount.toLocaleString()}
|
||||||
{pesData.penStitches &&
|
{pesData.penStitches &&
|
||||||
pesData.penStitches.stitches.length !== pesData.stitchCount && (
|
pesData.penStitches.stitches.length !==
|
||||||
|
pesData.stitchCount && (
|
||||||
<span
|
<span
|
||||||
className="text-gray-500 dark:text-gray-500 font-normal ml-1"
|
className="text-gray-500 dark:text-gray-500 font-normal ml-1"
|
||||||
title="Input stitch count from PES file (lock stitches were added for machine compatibility)"
|
title="Input stitch count from PES file (lock stitches were added for machine compatibility)"
|
||||||
|
|
@ -99,64 +108,64 @@ export function PatternInfo({
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{pesData.uniqueColors.slice(0, 8).map((color, idx) => {
|
{pesData.uniqueColors.slice(0, 8).map((color, idx) => {
|
||||||
// Primary metadata: brand and catalog number
|
// Primary metadata: brand and catalog number
|
||||||
const primaryMetadata = [
|
const primaryMetadata = [
|
||||||
color.brand,
|
color.brand,
|
||||||
color.catalogNumber ? `#${color.catalogNumber}` : null,
|
color.catalogNumber ? `#${color.catalogNumber}` : null,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
// Secondary metadata: chart and description
|
// Secondary metadata: chart and description
|
||||||
const secondaryMetadata = [color.chart, color.description]
|
const secondaryMetadata = [color.chart, color.description]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
const metadata = [primaryMetadata, secondaryMetadata]
|
const metadata = [primaryMetadata, secondaryMetadata]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" • ");
|
.join(" • ");
|
||||||
|
|
||||||
// Show which thread blocks use this color in PatternSummaryCard
|
// Show which thread blocks use this color in PatternSummaryCard
|
||||||
const threadNumbers = color.threadIndices
|
const threadNumbers = color.threadIndices
|
||||||
.map((i) => i + 1)
|
.map((i) => i + 1)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
const tooltipText = showThreadBlocks
|
const tooltipText = showThreadBlocks
|
||||||
? metadata
|
? metadata
|
||||||
? `Color ${idx + 1}: ${color.hex} - ${metadata}`
|
? `Color ${idx + 1}: ${color.hex} - ${metadata}`
|
||||||
: `Color ${idx + 1}: ${color.hex}`
|
: `Color ${idx + 1}: ${color.hex}`
|
||||||
: metadata
|
: metadata
|
||||||
? `Color ${idx + 1}: ${color.hex}\n${metadata}\nUsed in thread blocks: ${threadNumbers}`
|
? `Color ${idx + 1}: ${color.hex}\n${metadata}\nUsed in thread blocks: ${threadNumbers}`
|
||||||
: `Color ${idx + 1}: ${color.hex}\nUsed in thread blocks: ${threadNumbers}`;
|
: `Color ${idx + 1}: ${color.hex}\nUsed in thread blocks: ${threadNumbers}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={idx}>
|
<Tooltip key={idx}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600 cursor-help"
|
||||||
|
style={{ backgroundColor: color.hex }}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-xs">
|
||||||
|
<p className="text-xs whitespace-pre-line">{tooltipText}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{pesData.uniqueColors.length > 8 && (
|
||||||
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300 leading-none cursor-help">
|
||||||
className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600 cursor-help"
|
+{pesData.uniqueColors.length - 8}
|
||||||
style={{ backgroundColor: color.hex }}
|
</div>
|
||||||
/>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent>
|
||||||
<p className="text-xs whitespace-pre-line">{tooltipText}</p>
|
<p className="text-xs">
|
||||||
|
{pesData.uniqueColors.length - 8} more{" "}
|
||||||
|
{pesData.uniqueColors.length - 8 === 1 ? "color" : "colors"}
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
)}
|
||||||
})}
|
|
||||||
{pesData.uniqueColors.length > 8 && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300 leading-none cursor-help">
|
|
||||||
+{pesData.uniqueColors.length - 8}
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p className="text-xs">
|
|
||||||
{pesData.uniqueColors.length - 8} more{" "}
|
|
||||||
{pesData.uniqueColors.length - 8 === 1 ? "color" : "colors"}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,13 @@ import {
|
||||||
canResumeSewing,
|
canResumeSewing,
|
||||||
} from "../utils/machineStateHelpers";
|
} from "../utils/machineStateHelpers";
|
||||||
import { calculatePatternTime } from "../utils/timeCalculation";
|
import { calculatePatternTime } from "../utils/timeCalculation";
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
|
||||||
|
|
@ -170,246 +176,246 @@ export function ProgressMonitor() {
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-4 pt-0 pb-4 flex-1 flex flex-col lg:overflow-hidden">
|
<CardContent className="px-4 pt-0 pb-4 flex-1 flex flex-col lg:overflow-hidden">
|
||||||
|
{/* 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-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">
|
|
||||||
{totalStitches.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{totalMinutes} min
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{patternInfo.speed} spm
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Progress Bar */}
|
|
||||||
{sewingProgress && (
|
|
||||||
<div className="mb-3">
|
|
||||||
<Progress
|
|
||||||
value={progressPercent}
|
|
||||||
className="h-3 mb-2 [&>div]:bg-gradient-to-r [&>div]:from-accent-600 [&>div]:to-accent-700 dark:[&>div]:from-accent-600 dark:[&>div]:to-accent-800"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
|
|
||||||
<div className="bg-gray-200 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
|
Total Stitches
|
||||||
</span>
|
</span>
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{sewingProgress.currentStitch.toLocaleString()} /{" "}
|
|
||||||
{totalStitches.toLocaleString()}
|
{totalStitches.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-200 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
|
Total Time
|
||||||
</span>
|
</span>
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{elapsedMinutes} / {totalMinutes} min
|
{totalMinutes} min
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{patternInfo.speed} spm
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Color Blocks */}
|
{/* Progress Bar */}
|
||||||
{colorBlocks.length > 0 && (
|
{sewingProgress && (
|
||||||
<div className="mb-3 lg:flex-1 lg:min-h-0 flex flex-col">
|
<div className="mb-3">
|
||||||
<h4 className="text-xs font-semibold mb-2 text-gray-700 dark:text-gray-300 flex-shrink-0">
|
<Progress
|
||||||
Color Blocks
|
value={progressPercent}
|
||||||
</h4>
|
className="h-3 mb-2 [&>div]:bg-gradient-to-r [&>div]:from-accent-600 [&>div]:to-accent-700 dark:[&>div]:from-accent-600 dark:[&>div]:to-accent-800"
|
||||||
<div className="relative lg:flex-1 lg:min-h-0">
|
/>
|
||||||
<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-primary-600 dark:[&::-webkit-scrollbar-thumb]:bg-primary-500 [&::-webkit-scrollbar-thumb]:rounded-full"
|
|
||||||
>
|
|
||||||
{colorBlocks.map((block, index) => {
|
|
||||||
const isCompleted = currentStitch >= block.endStitch;
|
|
||||||
const isCurrent = index === currentBlockIndex;
|
|
||||||
|
|
||||||
// Calculate progress within current block
|
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
|
||||||
let blockProgress = 0;
|
<div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
|
||||||
if (isCurrent) {
|
<span className="text-gray-600 dark:text-gray-400 block">
|
||||||
blockProgress =
|
Current Stitch
|
||||||
((currentStitch - block.startStitch) / block.stitchCount) *
|
</span>
|
||||||
100;
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
} else if (isCompleted) {
|
{sewingProgress.currentStitch.toLocaleString()} /{" "}
|
||||||
blockProgress = 100;
|
{totalStitches.toLocaleString()}
|
||||||
}
|
</span>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{elapsedMinutes} / {totalMinutes} min
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
return (
|
{/* Color Blocks */}
|
||||||
<div
|
{colorBlocks.length > 0 && (
|
||||||
key={index}
|
<div className="mb-3 lg:flex-1 lg:min-h-0 flex flex-col">
|
||||||
ref={isCurrent ? currentBlockRef : null}
|
<h4 className="text-xs font-semibold mb-2 text-gray-700 dark:text-gray-300 flex-shrink-0">
|
||||||
className={`p-2.5 rounded-lg border-2 transition-all duration-300 ${
|
Color Blocks
|
||||||
isCompleted
|
</h4>
|
||||||
? "border-success-600 bg-success-50 dark:bg-success-900/20"
|
<div className="relative lg:flex-1 lg:min-h-0">
|
||||||
: isCurrent
|
<div
|
||||||
? "border-gray-400 dark:border-gray-500 bg-white dark:bg-gray-700"
|
ref={colorBlocksScrollRef}
|
||||||
: "border-gray-200 dark:border-gray-600 bg-gray-100 dark:bg-gray-800/50 opacity-70"
|
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-primary-600 dark:[&::-webkit-scrollbar-thumb]:bg-primary-500 [&::-webkit-scrollbar-thumb]:rounded-full"
|
||||||
role="listitem"
|
>
|
||||||
aria-label={`Thread ${block.colorIndex + 1}, ${block.stitchCount} stitches, ${isCompleted ? "completed" : isCurrent ? "in progress" : "pending"}`}
|
{colorBlocks.map((block, index) => {
|
||||||
>
|
const isCompleted = currentStitch >= block.endStitch;
|
||||||
<div className="flex items-center gap-2.5">
|
const isCurrent = index === currentBlockIndex;
|
||||||
{/* Color swatch */}
|
|
||||||
<div
|
|
||||||
className="w-7 h-7 rounded-lg border-2 border-gray-300 dark:border-gray-600 shadow-md flex-shrink-0"
|
|
||||||
style={{
|
|
||||||
backgroundColor: block.threadHex,
|
|
||||||
}}
|
|
||||||
title={`Thread color: ${block.threadHex}`}
|
|
||||||
aria-label={`Thread color ${block.threadHex}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Thread info */}
|
// Calculate progress within current block
|
||||||
<div className="flex-1 min-w-0">
|
let blockProgress = 0;
|
||||||
<div className="font-semibold text-xs text-gray-900 dark:text-gray-100">
|
if (isCurrent) {
|
||||||
Thread {block.colorIndex + 1}
|
blockProgress =
|
||||||
{(block.threadBrand ||
|
((currentStitch - block.startStitch) /
|
||||||
block.threadChart ||
|
block.stitchCount) *
|
||||||
block.threadDescription ||
|
100;
|
||||||
block.threadCatalogNumber) && (
|
} else if (isCompleted) {
|
||||||
<span className="font-normal text-gray-600 dark:text-gray-400">
|
blockProgress = 100;
|
||||||
{" "}
|
}
|
||||||
(
|
|
||||||
{(() => {
|
|
||||||
// Primary metadata: brand and catalog number
|
|
||||||
const primaryMetadata = [
|
|
||||||
block.threadBrand,
|
|
||||||
block.threadCatalogNumber
|
|
||||||
? `#${block.threadCatalogNumber}`
|
|
||||||
: null,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
// Secondary metadata: chart and description
|
return (
|
||||||
const secondaryMetadata = [
|
<div
|
||||||
block.threadChart,
|
key={index}
|
||||||
block.threadDescription,
|
ref={isCurrent ? currentBlockRef : null}
|
||||||
]
|
className={`p-2.5 rounded-lg border-2 transition-all duration-300 ${
|
||||||
.filter(Boolean)
|
isCompleted
|
||||||
.join(" ");
|
? "border-success-600 bg-success-50 dark:bg-success-900/20"
|
||||||
|
: isCurrent
|
||||||
|
? "border-gray-400 dark:border-gray-500 bg-white dark:bg-gray-700"
|
||||||
|
: "border-gray-200 dark:border-gray-600 bg-gray-100 dark:bg-gray-800/50 opacity-70"
|
||||||
|
}`}
|
||||||
|
role="listitem"
|
||||||
|
aria-label={`Thread ${block.colorIndex + 1}, ${block.stitchCount} stitches, ${isCompleted ? "completed" : isCurrent ? "in progress" : "pending"}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
{/* Color swatch */}
|
||||||
|
<div
|
||||||
|
className="w-7 h-7 rounded-lg border-2 border-gray-300 dark:border-gray-600 shadow-md flex-shrink-0"
|
||||||
|
style={{
|
||||||
|
backgroundColor: block.threadHex,
|
||||||
|
}}
|
||||||
|
title={`Thread color: ${block.threadHex}`}
|
||||||
|
aria-label={`Thread color ${block.threadHex}`}
|
||||||
|
/>
|
||||||
|
|
||||||
return [primaryMetadata, secondaryMetadata]
|
{/* Thread info */}
|
||||||
.filter(Boolean)
|
<div className="flex-1 min-w-0">
|
||||||
.join(" • ");
|
<div className="font-semibold text-xs text-gray-900 dark:text-gray-100">
|
||||||
})()}
|
Thread {block.colorIndex + 1}
|
||||||
)
|
{(block.threadBrand ||
|
||||||
</span>
|
block.threadChart ||
|
||||||
)}
|
block.threadDescription ||
|
||||||
</div>
|
block.threadCatalogNumber) && (
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-400 mt-0.5">
|
<span className="font-normal text-gray-600 dark:text-gray-400">
|
||||||
{block.stitchCount.toLocaleString()} stitches
|
{" "}
|
||||||
|
(
|
||||||
|
{(() => {
|
||||||
|
// Primary metadata: brand and catalog number
|
||||||
|
const primaryMetadata = [
|
||||||
|
block.threadBrand,
|
||||||
|
block.threadCatalogNumber
|
||||||
|
? `#${block.threadCatalogNumber}`
|
||||||
|
: null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
// Secondary metadata: chart and description
|
||||||
|
const secondaryMetadata = [
|
||||||
|
block.threadChart,
|
||||||
|
block.threadDescription,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return [primaryMetadata, secondaryMetadata]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" • ");
|
||||||
|
})()}
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-600 dark:text-gray-400 mt-0.5">
|
||||||
|
{block.stitchCount.toLocaleString()} stitches
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Status icon */}
|
||||||
|
{isCompleted ? (
|
||||||
|
<CheckCircleIcon
|
||||||
|
className="w-5 h-5 text-success-600 flex-shrink-0"
|
||||||
|
aria-label="Completed"
|
||||||
|
/>
|
||||||
|
) : isCurrent ? (
|
||||||
|
<ArrowRightIcon
|
||||||
|
className="w-5 h-5 text-gray-600 dark:text-gray-400 flex-shrink-0 animate-pulse"
|
||||||
|
aria-label="In progress"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CircleStackIcon
|
||||||
|
className="w-5 h-5 text-gray-400 flex-shrink-0"
|
||||||
|
aria-label="Pending"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status icon */}
|
{/* Progress bar for current block */}
|
||||||
{isCompleted ? (
|
{isCurrent && (
|
||||||
<CheckCircleIcon
|
<Progress
|
||||||
className="w-5 h-5 text-success-600 flex-shrink-0"
|
value={blockProgress}
|
||||||
aria-label="Completed"
|
className="mt-2 h-1.5 [&>div]:bg-gray-600 dark:[&>div]:bg-gray-500"
|
||||||
/>
|
aria-label={`${Math.round(blockProgress)}% complete`}
|
||||||
) : isCurrent ? (
|
|
||||||
<ArrowRightIcon
|
|
||||||
className="w-5 h-5 text-gray-600 dark:text-gray-400 flex-shrink-0 animate-pulse"
|
|
||||||
aria-label="In progress"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CircleStackIcon
|
|
||||||
className="w-5 h-5 text-gray-400 flex-shrink-0"
|
|
||||||
aria-label="Pending"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{/* Progress bar for current block */}
|
})}
|
||||||
{isCurrent && (
|
</div>
|
||||||
<Progress
|
{/* Gradient overlay to indicate more content below - only on desktop and when not at bottom */}
|
||||||
value={blockProgress}
|
{showGradient && (
|
||||||
className="mt-2 h-1.5 [&>div]:bg-gray-600 dark:[&>div]:bg-gray-500"
|
<div className="hidden lg:block absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-white dark:from-gray-800 to-transparent pointer-events-none" />
|
||||||
aria-label={`${Math.round(blockProgress)}% complete`}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
{/* Gradient overlay to indicate more content below - only on desktop and when not at bottom */}
|
|
||||||
{showGradient && (
|
|
||||||
<div className="hidden lg:block absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-white dark:from-gray-800 to-transparent pointer-events-none" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex gap-2 flex-shrink-0">
|
||||||
|
{/* Resume has highest priority when available */}
|
||||||
|
{canResumeSewing(machineStatus) && (
|
||||||
|
<Button
|
||||||
|
onClick={resumeSewing}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="flex-1"
|
||||||
|
aria-label="Resume sewing the current pattern"
|
||||||
|
>
|
||||||
|
<PlayIcon className="w-3.5 h-3.5" />
|
||||||
|
Resume Sewing
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Start Sewing - primary action, takes more space */}
|
||||||
|
{canStartSewing(machineStatus) && !canResumeSewing(machineStatus) && (
|
||||||
|
<Button
|
||||||
|
onClick={startSewing}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="flex-[2]"
|
||||||
|
aria-label="Start sewing the pattern"
|
||||||
|
>
|
||||||
|
<PlayIcon className="w-3.5 h-3.5" />
|
||||||
|
Start Sewing
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Start Mask Trace - secondary action */}
|
||||||
|
{canStartMaskTrace(machineStatus) && (
|
||||||
|
<Button
|
||||||
|
onClick={startMaskTrace}
|
||||||
|
disabled={isDeleting}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
aria-label={
|
||||||
|
isMaskTraceComplete
|
||||||
|
? "Start mask trace again"
|
||||||
|
: "Start mask trace"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ArrowPathIcon className="w-3.5 h-3.5" />
|
||||||
|
{isMaskTraceComplete ? "Trace Again" : "Start Mask Trace"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action buttons */}
|
|
||||||
<div className="flex gap-2 flex-shrink-0">
|
|
||||||
{/* Resume has highest priority when available */}
|
|
||||||
{canResumeSewing(machineStatus) && (
|
|
||||||
<Button
|
|
||||||
onClick={resumeSewing}
|
|
||||||
disabled={isDeleting}
|
|
||||||
className="flex-1"
|
|
||||||
aria-label="Resume sewing the current pattern"
|
|
||||||
>
|
|
||||||
<PlayIcon className="w-3.5 h-3.5" />
|
|
||||||
Resume Sewing
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Start Sewing - primary action, takes more space */}
|
|
||||||
{canStartSewing(machineStatus) && !canResumeSewing(machineStatus) && (
|
|
||||||
<Button
|
|
||||||
onClick={startSewing}
|
|
||||||
disabled={isDeleting}
|
|
||||||
className="flex-[2]"
|
|
||||||
aria-label="Start sewing the pattern"
|
|
||||||
>
|
|
||||||
<PlayIcon className="w-3.5 h-3.5" />
|
|
||||||
Start Sewing
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Start Mask Trace - secondary action */}
|
|
||||||
{canStartMaskTrace(machineStatus) && (
|
|
||||||
<Button
|
|
||||||
onClick={startMaskTrace}
|
|
||||||
disabled={isDeleting}
|
|
||||||
variant="outline"
|
|
||||||
className="flex-1"
|
|
||||||
aria-label={
|
|
||||||
isMaskTraceComplete
|
|
||||||
? "Start mask trace again"
|
|
||||||
: "Start mask trace"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ArrowPathIcon className="w-3.5 h-3.5" />
|
|
||||||
{isMaskTraceComplete ? "Trace Again" : "Start Mask Trace"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@ export function SkeletonLoader({
|
||||||
circle: "rounded-full",
|
circle: "rounded-full",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <Skeleton className={cn(variantClasses[variant], className)} />;
|
||||||
<Skeleton className={cn(variantClasses[variant], className)} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PatternCanvasSkeleton() {
|
export function PatternCanvasSkeleton() {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
function AlertDialog({
|
function AlertDialog({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogTrigger({
|
function AlertDialogTrigger({
|
||||||
|
|
@ -15,7 +15,7 @@ function AlertDialogTrigger({
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogPortal({
|
function AlertDialogPortal({
|
||||||
|
|
@ -23,7 +23,7 @@ function AlertDialogPortal({
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogOverlay({
|
function AlertDialogOverlay({
|
||||||
|
|
@ -35,11 +35,11 @@ function AlertDialogOverlay({
|
||||||
data-slot="alert-dialog-overlay"
|
data-slot="alert-dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogContent({
|
function AlertDialogContent({
|
||||||
|
|
@ -53,12 +53,12 @@ function AlertDialogContent({
|
||||||
data-slot="alert-dialog-content"
|
data-slot="alert-dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</AlertDialogPortal>
|
</AlertDialogPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogHeader({
|
function AlertDialogHeader({
|
||||||
|
|
@ -71,7 +71,7 @@ function AlertDialogHeader({
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogFooter({
|
function AlertDialogFooter({
|
||||||
|
|
@ -83,11 +83,11 @@ function AlertDialogFooter({
|
||||||
data-slot="alert-dialog-footer"
|
data-slot="alert-dialog-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogTitle({
|
function AlertDialogTitle({
|
||||||
|
|
@ -100,7 +100,7 @@ function AlertDialogTitle({
|
||||||
className={cn("text-lg font-semibold", className)}
|
className={cn("text-lg font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogDescription({
|
function AlertDialogDescription({
|
||||||
|
|
@ -113,7 +113,7 @@ function AlertDialogDescription({
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogAction({
|
function AlertDialogAction({
|
||||||
|
|
@ -125,7 +125,7 @@ function AlertDialogAction({
|
||||||
className={cn(buttonVariants(), className)}
|
className={cn(buttonVariants(), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDialogCancel({
|
function AlertDialogCancel({
|
||||||
|
|
@ -137,7 +137,7 @@ function AlertDialogCancel({
|
||||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -152,4 +152,4 @@ export {
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
|
@ -16,8 +16,8 @@ const alertVariants = cva(
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Alert({
|
function Alert({
|
||||||
className,
|
className,
|
||||||
|
|
@ -31,7 +31,7 @@ function Alert({
|
||||||
className={cn(alertVariants({ variant }), className)}
|
className={cn(alertVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -40,11 +40,11 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="alert-title"
|
data-slot="alert-title"
|
||||||
className={cn(
|
className={cn(
|
||||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDescription({
|
function AlertDescription({
|
||||||
|
|
@ -56,11 +56,11 @@ function AlertDescription({
|
||||||
data-slot="alert-description"
|
data-slot="alert-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
|
@ -22,8 +22,8 @@ const badgeVariants = cva(
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Badge({
|
function Badge({
|
||||||
className,
|
className,
|
||||||
|
|
@ -32,7 +32,7 @@ function Badge({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span"> &
|
}: React.ComponentProps<"span"> &
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "span"
|
const Comp = asChild ? Slot : "span";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
|
@ -40,7 +40,7 @@ function Badge({
|
||||||
className={cn(badgeVariants({ variant }), className)}
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
export { Badge, badgeVariants };
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
|
@ -33,8 +33,8 @@ const buttonVariants = cva(
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
|
|
@ -44,9 +44,9 @@ function Button({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: React.ComponentProps<"button"> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
|
@ -56,7 +56,7 @@ function Button({
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants };
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("leading-none font-semibold", className)}
|
className={cn("leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="card-action"
|
data-slot="card-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("px-6", className)}
|
className={cn("px-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -78,7 +78,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -89,4 +89,4 @@ export {
|
||||||
CardAction,
|
CardAction,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,31 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTrigger({
|
function DialogTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogPortal({
|
function DialogPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogClose({
|
function DialogClose({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogOverlay({
|
function DialogOverlay({
|
||||||
|
|
@ -37,11 +37,11 @@ function DialogOverlay({
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogContent({
|
function DialogContent({
|
||||||
|
|
@ -50,7 +50,7 @@ function DialogContent({
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
|
@ -59,7 +59,7 @@ function DialogContent({
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -75,7 +75,7 @@ function DialogContent({
|
||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -85,7 +85,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -94,11 +94,11 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTitle({
|
function DialogTitle({
|
||||||
|
|
@ -111,7 +111,7 @@ function DialogTitle({
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDescription({
|
function DialogDescription({
|
||||||
|
|
@ -124,7 +124,7 @@ function DialogDescription({
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -138,4 +138,4 @@ export {
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Popover({
|
function Popover({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverTrigger({
|
function PopoverTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverContent({
|
function PopoverContent({
|
||||||
|
|
@ -31,18 +31,18 @@ function PopoverContent({
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverAnchor({
|
function PopoverAnchor({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Progress({
|
function Progress({
|
||||||
className,
|
className,
|
||||||
|
|
@ -15,7 +15,7 @@ function Progress({
|
||||||
data-slot="progress"
|
data-slot="progress"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -25,7 +25,7 @@ function Progress({
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
/>
|
/>
|
||||||
</ProgressPrimitive.Root>
|
</ProgressPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Progress }
|
export { Progress };
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Separator({
|
function Separator({
|
||||||
className,
|
className,
|
||||||
|
|
@ -16,11 +16,11 @@ function Separator({
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Separator }
|
export { Separator };
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Skeleton }
|
export { Skeleton };
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function TooltipProvider({
|
function TooltipProvider({
|
||||||
delayDuration = 0,
|
delayDuration = 0,
|
||||||
|
|
@ -13,7 +13,7 @@ function TooltipProvider({
|
||||||
delayDuration={delayDuration}
|
delayDuration={delayDuration}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tooltip({
|
function Tooltip({
|
||||||
|
|
@ -23,13 +23,13 @@ function Tooltip({
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TooltipTrigger({
|
function TooltipTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TooltipContent({
|
function TooltipContent({
|
||||||
|
|
@ -45,7 +45,7 @@ function TooltipContent({
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -53,7 +53,7 @@ function TooltipContent({
|
||||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
</TooltipPrimitive.Content>
|
</TooltipPrimitive.Content>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue