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",
|
||||
machineErrorMessage || pyodideError
|
||||
? "animate-pulse hover:animate-none"
|
||||
: "invisible pointer-events-none"
|
||||
: "invisible pointer-events-none",
|
||||
)}
|
||||
title="Click to view error details"
|
||||
aria-label="View error details"
|
||||
|
|
|
|||
|
|
@ -233,188 +233,188 @@ export function FileUpload() {
|
|||
</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">
|
||||
<p className="text-xs text-success-800 dark:text-success-200">
|
||||
<strong>Cached:</strong> "{resumeFileName}"
|
||||
</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>
|
||||
{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">
|
||||
<p className="text-xs text-success-800 dark:text-success-200">
|
||||
<strong>Cached:</strong> "{resumeFileName}"
|
||||
</p>
|
||||
</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>
|
||||
{isLoading && <PatternInfoSkeleton />}
|
||||
|
||||
{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>
|
||||
{!isLoading && pesData && (
|
||||
<div className="mb-3">
|
||||
<PatternInfo pesData={pesData} showThreadBlocks />
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,10 +23,18 @@ export function PatternInfo({
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<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">
|
||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{" "}
|
||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
|
||||
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(
|
||||
1,
|
||||
)}{" "}
|
||||
x{" "}
|
||||
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(
|
||||
1,
|
||||
)}{" "}
|
||||
mm
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -45,7 +53,8 @@ export function PatternInfo({
|
|||
{pesData.penStitches?.stitches.length.toLocaleString() ||
|
||||
pesData.stitchCount.toLocaleString()}
|
||||
{pesData.penStitches &&
|
||||
pesData.penStitches.stitches.length !== pesData.stitchCount && (
|
||||
pesData.penStitches.stitches.length !==
|
||||
pesData.stitchCount && (
|
||||
<span
|
||||
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)"
|
||||
|
|
@ -99,64 +108,64 @@ export function PatternInfo({
|
|||
<TooltipProvider>
|
||||
<div className="flex gap-1">
|
||||
{pesData.uniqueColors.slice(0, 8).map((color, idx) => {
|
||||
// Primary metadata: brand and catalog number
|
||||
const primaryMetadata = [
|
||||
color.brand,
|
||||
color.catalogNumber ? `#${color.catalogNumber}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
// Primary metadata: brand and catalog number
|
||||
const primaryMetadata = [
|
||||
color.brand,
|
||||
color.catalogNumber ? `#${color.catalogNumber}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
// Secondary metadata: chart and description
|
||||
const secondaryMetadata = [color.chart, color.description]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
// Secondary metadata: chart and description
|
||||
const secondaryMetadata = [color.chart, color.description]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
const metadata = [primaryMetadata, secondaryMetadata]
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
const metadata = [primaryMetadata, secondaryMetadata]
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
|
||||
// Show which thread blocks use this color in PatternSummaryCard
|
||||
const threadNumbers = color.threadIndices
|
||||
.map((i) => i + 1)
|
||||
.join(", ");
|
||||
const tooltipText = showThreadBlocks
|
||||
? metadata
|
||||
? `Color ${idx + 1}: ${color.hex} - ${metadata}`
|
||||
: `Color ${idx + 1}: ${color.hex}`
|
||||
: metadata
|
||||
? `Color ${idx + 1}: ${color.hex}\n${metadata}\nUsed in thread blocks: ${threadNumbers}`
|
||||
: `Color ${idx + 1}: ${color.hex}\nUsed in thread blocks: ${threadNumbers}`;
|
||||
// Show which thread blocks use this color in PatternSummaryCard
|
||||
const threadNumbers = color.threadIndices
|
||||
.map((i) => i + 1)
|
||||
.join(", ");
|
||||
const tooltipText = showThreadBlocks
|
||||
? metadata
|
||||
? `Color ${idx + 1}: ${color.hex} - ${metadata}`
|
||||
: `Color ${idx + 1}: ${color.hex}`
|
||||
: metadata
|
||||
? `Color ${idx + 1}: ${color.hex}\n${metadata}\nUsed in thread blocks: ${threadNumbers}`
|
||||
: `Color ${idx + 1}: ${color.hex}\nUsed in thread blocks: ${threadNumbers}`;
|
||||
|
||||
return (
|
||||
<Tooltip key={idx}>
|
||||
return (
|
||||
<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>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600 cursor-help"
|
||||
style={{ backgroundColor: color.hex }}
|
||||
/>
|
||||
<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 className="max-w-xs">
|
||||
<p className="text-xs whitespace-pre-line">{tooltipText}</p>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">
|
||||
{pesData.uniqueColors.length - 8} more{" "}
|
||||
{pesData.uniqueColors.length - 8 === 1 ? "color" : "colors"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</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>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,13 @@ import {
|
|||
canResumeSewing,
|
||||
} from "../utils/machineStateHelpers";
|
||||
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 { Progress } from "@/components/ui/progress";
|
||||
|
||||
|
|
@ -170,246 +176,246 @@ export function ProgressMonitor() {
|
|||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pt-0 pb-4 flex-1 flex flex-col lg:overflow-hidden">
|
||||
|
||||
{/* Pattern Info */}
|
||||
{patternInfo && (
|
||||
<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">
|
||||
{/* Pattern Info */}
|
||||
{patternInfo && (
|
||||
<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">
|
||||
Current Stitch
|
||||
Total Stitches
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{sewingProgress.currentStitch.toLocaleString()} /{" "}
|
||||
{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
|
||||
Total Time
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Color Blocks */}
|
||||
{colorBlocks.length > 0 && (
|
||||
<div className="mb-3 lg:flex-1 lg:min-h-0 flex flex-col">
|
||||
<h4 className="text-xs font-semibold mb-2 text-gray-700 dark:text-gray-300 flex-shrink-0">
|
||||
Color Blocks
|
||||
</h4>
|
||||
<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;
|
||||
{/* 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"
|
||||
/>
|
||||
|
||||
// Calculate progress within current block
|
||||
let blockProgress = 0;
|
||||
if (isCurrent) {
|
||||
blockProgress =
|
||||
((currentStitch - block.startStitch) / block.stitchCount) *
|
||||
100;
|
||||
} else if (isCompleted) {
|
||||
blockProgress = 100;
|
||||
}
|
||||
<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">
|
||||
<span className="text-gray-600 dark:text-gray-400 block">
|
||||
Current Stitch
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{sewingProgress.currentStitch.toLocaleString()} /{" "}
|
||||
{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 (
|
||||
<div
|
||||
key={index}
|
||||
ref={isCurrent ? currentBlockRef : null}
|
||||
className={`p-2.5 rounded-lg border-2 transition-all duration-300 ${
|
||||
isCompleted
|
||||
? "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}`}
|
||||
/>
|
||||
{/* Color Blocks */}
|
||||
{colorBlocks.length > 0 && (
|
||||
<div className="mb-3 lg:flex-1 lg:min-h-0 flex flex-col">
|
||||
<h4 className="text-xs font-semibold mb-2 text-gray-700 dark:text-gray-300 flex-shrink-0">
|
||||
Color Blocks
|
||||
</h4>
|
||||
<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;
|
||||
|
||||
{/* Thread info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-xs text-gray-900 dark:text-gray-100">
|
||||
Thread {block.colorIndex + 1}
|
||||
{(block.threadBrand ||
|
||||
block.threadChart ||
|
||||
block.threadDescription ||
|
||||
block.threadCatalogNumber) && (
|
||||
<span className="font-normal text-gray-600 dark:text-gray-400">
|
||||
{" "}
|
||||
(
|
||||
{(() => {
|
||||
// Primary metadata: brand and catalog number
|
||||
const primaryMetadata = [
|
||||
block.threadBrand,
|
||||
block.threadCatalogNumber
|
||||
? `#${block.threadCatalogNumber}`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
// Calculate progress within current block
|
||||
let blockProgress = 0;
|
||||
if (isCurrent) {
|
||||
blockProgress =
|
||||
((currentStitch - block.startStitch) /
|
||||
block.stitchCount) *
|
||||
100;
|
||||
} else if (isCompleted) {
|
||||
blockProgress = 100;
|
||||
}
|
||||
|
||||
// Secondary metadata: chart and description
|
||||
const secondaryMetadata = [
|
||||
block.threadChart,
|
||||
block.threadDescription,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
ref={isCurrent ? currentBlockRef : null}
|
||||
className={`p-2.5 rounded-lg border-2 transition-all duration-300 ${
|
||||
isCompleted
|
||||
? "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]
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
})()}
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 mt-0.5">
|
||||
{block.stitchCount.toLocaleString()} stitches
|
||||
{/* Thread info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-xs text-gray-900 dark:text-gray-100">
|
||||
Thread {block.colorIndex + 1}
|
||||
{(block.threadBrand ||
|
||||
block.threadChart ||
|
||||
block.threadDescription ||
|
||||
block.threadCatalogNumber) && (
|
||||
<span className="font-normal text-gray-600 dark:text-gray-400">
|
||||
{" "}
|
||||
(
|
||||
{(() => {
|
||||
// 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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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"
|
||||
{/* Progress bar for current block */}
|
||||
{isCurrent && (
|
||||
<Progress
|
||||
value={blockProgress}
|
||||
className="mt-2 h-1.5 [&>div]:bg-gray-600 dark:[&>div]:bg-gray-500"
|
||||
aria-label={`${Math.round(blockProgress)}% complete`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress bar for current block */}
|
||||
{isCurrent && (
|
||||
<Progress
|
||||
value={blockProgress}
|
||||
className="mt-2 h-1.5 [&>div]:bg-gray-600 dark:[&>div]:bg-gray-500"
|
||||
aria-label={`${Math.round(blockProgress)}% complete`}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ export function SkeletonLoader({
|
|||
circle: "rounded-full",
|
||||
};
|
||||
|
||||
return (
|
||||
<Skeleton className={cn(variantClasses[variant], className)} />
|
||||
);
|
||||
return <Skeleton className={cn(variantClasses[variant], className)} />;
|
||||
}
|
||||
|
||||
export function PatternCanvasSkeleton() {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
|
|
@ -15,7 +15,7 @@ function AlertDialogTrigger({
|
|||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
|
|
@ -23,7 +23,7 @@ function AlertDialogPortal({
|
|||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
|
|
@ -35,11 +35,11 @@ function AlertDialogOverlay({
|
|||
data-slot="alert-dialog-overlay"
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
|
|
@ -53,12 +53,12 @@ function AlertDialogContent({
|
|||
data-slot="alert-dialog-content"
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
|
|
@ -71,7 +71,7 @@ function AlertDialogHeader({
|
|||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
|
|
@ -83,11 +83,11 @@ function AlertDialogFooter({
|
|||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
|
|
@ -100,7 +100,7 @@ function AlertDialogTitle({
|
|||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
|
|
@ -113,7 +113,7 @@ function AlertDialogDescription({
|
|||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
|
|
@ -125,7 +125,7 @@ function AlertDialogAction({
|
|||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
|
|
@ -137,7 +137,7 @@ function AlertDialogCancel({
|
|||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -152,4 +152,4 @@ export {
|
|||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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",
|
||||
|
|
@ -16,8 +16,8 @@ const alertVariants = cva(
|
|||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
|
|
@ -31,7 +31,7 @@ function Alert({
|
|||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -40,11 +40,11 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
|
|
@ -56,11 +56,11 @@ function AlertDescription({
|
|||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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",
|
||||
|
|
@ -22,8 +22,8 @@ const badgeVariants = cva(
|
|||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
|
|
@ -32,7 +32,7 @@ function Badge({
|
|||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -40,7 +40,7 @@ function Badge({
|
|||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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",
|
||||
|
|
@ -33,8 +33,8 @@ const buttonVariants = cva(
|
|||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
|
|
@ -44,9 +44,9 @@ function Button({
|
|||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
|
@ -56,7 +56,7 @@ function Button({
|
|||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...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">) {
|
||||
return (
|
||||
|
|
@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-header"
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -89,4 +89,4 @@ export {
|
|||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
|
|
@ -37,11 +37,11 @@ function DialogOverlay({
|
|||
data-slot="dialog-overlay"
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
|
|
@ -50,7 +50,7 @@ function DialogContent({
|
|||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
|
|
@ -59,7 +59,7 @@ function DialogContent({
|
|||
data-slot="dialog-content"
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -75,7 +75,7 @@ function DialogContent({
|
|||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
|
@ -94,11 +94,11 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
|
|
@ -111,7 +111,7 @@ function DialogTitle({
|
|||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
|
|
@ -124,7 +124,7 @@ function DialogDescription({
|
|||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -138,4 +138,4 @@ export {
|
|||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
|
|
@ -31,18 +31,18 @@ function PopoverContent({
|
|||
sideOffset={sideOffset}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: 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 ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
|
|
@ -15,7 +15,7 @@ function Progress({
|
|||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -25,7 +25,7 @@ function Progress({
|
|||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
export { Progress };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
|
|
@ -16,11 +16,11 @@ function Separator({
|
|||
orientation={orientation}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...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">) {
|
||||
return (
|
||||
|
|
@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export { Skeleton };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
|
|
@ -13,7 +13,7 @@ function TooltipProvider({
|
|||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
|
|
@ -23,13 +23,13 @@ function Tooltip({
|
|||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
|
|
@ -45,7 +45,7 @@ function TooltipContent({
|
|||
sideOffset={sideOffset}
|
||||
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",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...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.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue