feature: Migrate PatternCanvas to shadcn/ui and rename placeholder

- Migrated PatternCanvas component to use shadcn Card components (Card, CardHeader, CardTitle, CardDescription, CardContent)
- Replaced custom zoom control buttons with shadcn Button component using outline variant and icon size
- Renamed PatternPreviewPlaceholder to PatternCanvasPlaceholder for consistency
- Updated all imports and references in App.tsx
- Maintained all existing functionality including Konva canvas rendering, zoom controls, and pattern positioning

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2025-12-20 20:12:40 +01:00
parent bb066d7775
commit 7cf4a5de17
4 changed files with 331 additions and 311 deletions

View file

@ -6,7 +6,7 @@ import { useUIStore } from "./stores/useUIStore";
import { AppHeader } from "./components/AppHeader"; import { AppHeader } from "./components/AppHeader";
import { LeftSidebar } from "./components/LeftSidebar"; import { LeftSidebar } from "./components/LeftSidebar";
import { PatternCanvas } from "./components/PatternCanvas"; import { PatternCanvas } from "./components/PatternCanvas";
import { PatternPreviewPlaceholder } from "./components/PatternPreviewPlaceholder"; import { PatternCanvasPlaceholder } from "./components/PatternCanvasPlaceholder";
import { BluetoothDevicePicker } from "./components/BluetoothDevicePicker"; import { BluetoothDevicePicker } from "./components/BluetoothDevicePicker";
import "./App.css"; import "./App.css";
@ -76,7 +76,7 @@ function App() {
{/* Right Column - Pattern Preview */} {/* Right Column - Pattern Preview */}
<div className="flex flex-col lg:overflow-hidden lg:h-full"> <div className="flex flex-col lg:overflow-hidden lg:h-full">
{pesData ? <PatternCanvas /> : <PatternPreviewPlaceholder />} {pesData ? <PatternCanvas /> : <PatternCanvasPlaceholder />}
</div> </div>
</div> </div>

View file

@ -22,6 +22,14 @@ import {
PatternBounds, PatternBounds,
CurrentPosition, CurrentPosition,
} from "./KonvaComponents"; } from "./KonvaComponents";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export function PatternCanvas() { export function PatternCanvas() {
// Machine store // Machine store
@ -252,126 +260,101 @@ export function PatternCanvas() {
: "text-gray-600 dark:text-gray-400"; : "text-gray-600 dark:text-gray-400";
return ( return (
<div <Card
className={`lg:h-full bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md border-l-4 ${borderColor} flex flex-col`} className={`p-0 gap-0 lg:h-full flex flex-col border-l-4 ${borderColor}`}
> >
<div className="flex items-start gap-3 mb-3 flex-shrink-0"> <CardHeader className="p-4 pb-3">
<PhotoIcon className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} /> <div className="flex items-start gap-3">
<div className="flex-1 min-w-0"> <PhotoIcon className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} />
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1"> <div className="flex-1 min-w-0">
Pattern Preview <CardTitle className="text-sm">Pattern Preview</CardTitle>
</h3> {pesData ? (
{pesData ? ( <CardDescription className="text-xs">
<p className="text-xs text-gray-600 dark:text-gray-400"> {((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)}{" "}
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} ×{" "} ×{" "}
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm {((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)}{" "}
</p> mm
) : ( </CardDescription>
<p className="text-xs text-gray-600 dark:text-gray-400"> ) : (
No pattern loaded <CardDescription className="text-xs">
</p> No pattern loaded
)} </CardDescription>
)}
</div>
</div> </div>
</div> </CardHeader>
<div <CardContent className="px-4 pt-0 pb-4 flex-1 flex flex-col">
className="relative w-full h-[400px] sm:h-[500px] lg:flex-1 lg:min-h-0 border border-gray-300 dark:border-gray-600 rounded bg-gray-200 dark:bg-gray-900 overflow-hidden" <div
ref={containerRef} className="relative w-full h-[400px] sm:h-[500px] lg:flex-1 lg:min-h-0 border border-gray-300 dark:border-gray-600 rounded bg-gray-200 dark:bg-gray-900 overflow-hidden"
> ref={containerRef}
{containerSize.width > 0 && ( >
<Stage {containerSize.width > 0 && (
width={containerSize.width} <Stage
height={containerSize.height} width={containerSize.width}
x={stagePos.x} height={containerSize.height}
y={stagePos.y} x={stagePos.x}
scaleX={stageScale} y={stagePos.y}
scaleY={stageScale} scaleX={stageScale}
draggable scaleY={stageScale}
onWheel={handleWheel} draggable
onDragStart={() => { onWheel={handleWheel}
if (stageRef.current) { onDragStart={() => {
stageRef.current.container().style.cursor = "grabbing"; if (stageRef.current) {
} stageRef.current.container().style.cursor = "grabbing";
}} }
onDragEnd={() => { }}
if (stageRef.current) { onDragEnd={() => {
stageRef.current.container().style.cursor = "grab"; if (stageRef.current) {
} stageRef.current.container().style.cursor = "grab";
}} }
ref={(node) => { }}
stageRef.current = node; ref={(node) => {
if (node) { stageRef.current = node;
node.container().style.cursor = "grab"; if (node) {
} node.container().style.cursor = "grab";
}} }
> }}
{/* Background layer: grid, origin, hoop */} >
<Layer> {/* Background layer: grid, origin, hoop */}
{pesData && ( <Layer>
<> {pesData && (
<Grid <>
gridSize={100} <Grid
bounds={pesData.bounds} gridSize={100}
machineInfo={machineInfo} bounds={pesData.bounds}
/> machineInfo={machineInfo}
<Origin /> />
{machineInfo && <Hoop machineInfo={machineInfo} />} <Origin />
</> {machineInfo && <Hoop machineInfo={machineInfo} />}
)} </>
</Layer> )}
</Layer>
{/* Pattern layer: draggable stitches and bounds */} {/* Pattern layer: draggable stitches and bounds */}
<Layer> <Layer>
{pesData && ( {pesData && (
<Group <Group
name="pattern-group" name="pattern-group"
draggable={!patternUploaded && !isUploading} draggable={!patternUploaded && !isUploading}
x={localPatternOffset.x} x={localPatternOffset.x}
y={localPatternOffset.y} y={localPatternOffset.y}
onDragEnd={handlePatternDragEnd} onDragEnd={handlePatternDragEnd}
onMouseEnter={(e) => { onMouseEnter={(e) => {
const stage = e.target.getStage(); const stage = e.target.getStage();
if (stage && !patternUploaded && !isUploading) if (stage && !patternUploaded && !isUploading)
stage.container().style.cursor = "move"; stage.container().style.cursor = "move";
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
const stage = e.target.getStage(); const stage = e.target.getStage();
if (stage && !patternUploaded && !isUploading) if (stage && !patternUploaded && !isUploading)
stage.container().style.cursor = "grab"; stage.container().style.cursor = "grab";
}} }}
> >
<Stitches <Stitches
stitches={pesData.penStitches.stitches.map(
(s, i): [number, number, number, number] => {
// Convert PEN stitch format {x, y, flags, isJump} to PES format [x, y, cmd, colorIndex]
const cmd = s.isJump ? 0x10 : 0; // MOVE flag if jump
const colorIndex =
pesData.penStitches.colorBlocks.find(
(b) => i >= b.startStitch && i <= b.endStitch,
)?.colorIndex ?? 0;
return [s.x, s.y, cmd, colorIndex];
},
)}
pesData={pesData}
currentStitchIndex={sewingProgress?.currentStitch || 0}
showProgress={patternUploaded || isUploading}
/>
<PatternBounds bounds={pesData.bounds} />
</Group>
)}
</Layer>
{/* Current position layer */}
<Layer>
{pesData &&
pesData.penStitches &&
sewingProgress &&
sewingProgress.currentStitch > 0 && (
<Group x={localPatternOffset.x} y={localPatternOffset.y}>
<CurrentPosition
currentStitchIndex={sewingProgress.currentStitch}
stitches={pesData.penStitches.stitches.map( stitches={pesData.penStitches.stitches.map(
(s, i): [number, number, number, number] => { (s, i): [number, number, number, number] => {
const cmd = s.isJump ? 0x10 : 0; // Convert PEN stitch format {x, y, flags, isJump} to PES format [x, y, cmd, colorIndex]
const cmd = s.isJump ? 0x10 : 0; // MOVE flag if jump
const colorIndex = const colorIndex =
pesData.penStitches.colorBlocks.find( pesData.penStitches.colorBlocks.find(
(b) => i >= b.startStitch && i <= b.endStitch, (b) => i >= b.startStitch && i <= b.endStitch,
@ -379,138 +362,175 @@ export function PatternCanvas() {
return [s.x, s.y, cmd, colorIndex]; return [s.x, s.y, cmd, colorIndex];
}, },
)} )}
pesData={pesData}
currentStitchIndex={sewingProgress?.currentStitch || 0}
showProgress={patternUploaded || isUploading}
/> />
<PatternBounds bounds={pesData.bounds} />
</Group> </Group>
)} )}
</Layer> </Layer>
</Stage>
)}
{/* Placeholder overlay when no pattern is loaded */} {/* Current position layer */}
{!pesData && ( <Layer>
<div className="flex items-center justify-center h-full text-gray-600 dark:text-gray-400 italic"> {pesData &&
Load a PES file to preview the pattern pesData.penStitches &&
</div> sewingProgress &&
)} sewingProgress.currentStitch > 0 && (
<Group x={localPatternOffset.x} y={localPatternOffset.y}>
<CurrentPosition
currentStitchIndex={sewingProgress.currentStitch}
stitches={pesData.penStitches.stitches.map(
(s, i): [number, number, number, number] => {
const cmd = s.isJump ? 0x10 : 0;
const colorIndex =
pesData.penStitches.colorBlocks.find(
(b) => i >= b.startStitch && i <= b.endStitch,
)?.colorIndex ?? 0;
return [s.x, s.y, cmd, colorIndex];
},
)}
/>
</Group>
)}
</Layer>
</Stage>
)}
{/* Pattern info overlays */} {/* Placeholder overlay when no pattern is loaded */}
{pesData && ( {!pesData && (
<> <div className="flex items-center justify-center h-full text-gray-600 dark:text-gray-400 italic">
{/* Thread Legend Overlay */} Load a PES file to preview the pattern
<div className="absolute top-2 sm:top-2.5 left-2 sm:left-2.5 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm p-2 sm:p-2.5 rounded-lg shadow-lg z-10 max-w-[150px] sm:max-w-[180px] lg:max-w-[200px]"> </div>
<h4 className="m-0 mb-1.5 sm:mb-2 text-xs font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-300 dark:border-gray-600 pb-1 sm:pb-1.5"> )}
Colors
</h4>
{pesData.uniqueColors.map((color, idx) => {
// Primary metadata: brand and catalog number
const primaryMetadata = [
color.brand,
color.catalogNumber ? `#${color.catalogNumber}` : null,
]
.filter(Boolean)
.join(" ");
// Secondary metadata: chart and description {/* Pattern info overlays */}
const secondaryMetadata = [color.chart, color.description] {pesData && (
.filter(Boolean) <>
.join(" "); {/* Thread Legend Overlay */}
<div className="absolute top-2 sm:top-2.5 left-2 sm:left-2.5 bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm p-2 sm:p-2.5 rounded-lg shadow-lg z-10 max-w-[150px] sm:max-w-[180px] lg:max-w-[200px]">
<h4 className="m-0 mb-1.5 sm:mb-2 text-xs font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-300 dark:border-gray-600 pb-1 sm:pb-1.5">
Colors
</h4>
{pesData.uniqueColors.map((color, idx) => {
// Primary metadata: brand and catalog number
const primaryMetadata = [
color.brand,
color.catalogNumber ? `#${color.catalogNumber}` : null,
]
.filter(Boolean)
.join(" ");
return ( // Secondary metadata: chart and description
<div const secondaryMetadata = [color.chart, color.description]
key={idx} .filter(Boolean)
className="flex items-start gap-1.5 sm:gap-2 mb-1 sm:mb-1.5 last:mb-0" .join(" ");
>
return (
<div <div
className="w-3 h-3 sm:w-4 sm:h-4 rounded border border-black dark:border-gray-300 flex-shrink-0 mt-0.5" key={idx}
style={{ backgroundColor: color.hex }} className="flex items-start gap-1.5 sm:gap-2 mb-1 sm:mb-1.5 last:mb-0"
/> >
<div className="flex-1 min-w-0"> <div
<div className="text-xs font-semibold text-gray-900 dark:text-gray-100"> className="w-3 h-3 sm:w-4 sm:h-4 rounded border border-black dark:border-gray-300 flex-shrink-0 mt-0.5"
Color {idx + 1} style={{ backgroundColor: color.hex }}
</div> />
{(primaryMetadata || secondaryMetadata) && ( <div className="flex-1 min-w-0">
<div className="text-xs text-gray-600 dark:text-gray-400 leading-tight mt-0.5 break-words"> <div className="text-xs font-semibold text-gray-900 dark:text-gray-100">
{primaryMetadata} Color {idx + 1}
{primaryMetadata && secondaryMetadata && (
<span className="mx-1"></span>
)}
{secondaryMetadata}
</div> </div>
)} {(primaryMetadata || secondaryMetadata) && (
<div className="text-xs text-gray-600 dark:text-gray-400 leading-tight mt-0.5 break-words">
{primaryMetadata}
{primaryMetadata && secondaryMetadata && (
<span className="mx-1"></span>
)}
{secondaryMetadata}
</div>
)}
</div>
</div> </div>
</div> );
); })}
})} </div>
</div>
{/* Pattern Offset Indicator */} {/* Pattern Offset Indicator */}
<div <div
className={`absolute bottom-16 sm:bottom-20 right-2 sm:right-5 backdrop-blur-sm p-2 sm:p-2.5 px-2.5 sm:px-3.5 rounded-lg shadow-lg z-[11] min-w-[160px] sm:min-w-[180px] transition-colors ${ className={`absolute bottom-16 sm:bottom-20 right-2 sm:right-5 backdrop-blur-sm p-2 sm:p-2.5 px-2.5 sm:px-3.5 rounded-lg shadow-lg z-[11] min-w-[160px] sm:min-w-[180px] transition-colors ${
patternUploaded patternUploaded
? "bg-amber-50/95 dark:bg-amber-900/80 border-2 border-amber-300 dark:border-amber-600" ? "bg-amber-50/95 dark:bg-amber-900/80 border-2 border-amber-300 dark:border-amber-600"
: "bg-white/95 dark:bg-gray-800/95" : "bg-white/95 dark:bg-gray-800/95"
}`} }`}
> >
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<div className="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider"> <div className="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">
Pattern Position: Pattern Position:
</div>
{patternUploaded && (
<div className="flex items-center gap-1 text-amber-600 dark:text-amber-400">
<LockClosedIcon className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
<span className="text-xs font-bold">LOCKED</span>
</div>
)}
</div>
<div className="text-sm font-semibold text-primary-600 dark:text-primary-400 mb-1">
X: {(localPatternOffset.x / 10).toFixed(1)}mm, Y:{" "}
{(localPatternOffset.y / 10).toFixed(1)}mm
</div>
<div className="text-xs text-gray-600 dark:text-gray-400 italic">
{patternUploaded
? "Pattern locked • Drag background to pan"
: "Drag pattern to move • Drag background to pan"}
</div> </div>
{patternUploaded && (
<div className="flex items-center gap-1 text-amber-600 dark:text-amber-400">
<LockClosedIcon className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
<span className="text-xs font-bold">LOCKED</span>
</div>
)}
</div> </div>
<div className="text-sm font-semibold text-primary-600 dark:text-primary-400 mb-1">
X: {(localPatternOffset.x / 10).toFixed(1)}mm, Y:{" "}
{(localPatternOffset.y / 10).toFixed(1)}mm
</div>
<div className="text-xs text-gray-600 dark:text-gray-400 italic">
{patternUploaded
? "Pattern locked • Drag background to pan"
: "Drag pattern to move • Drag background to pan"}
</div>
</div>
{/* Zoom Controls Overlay */} {/* Zoom Controls Overlay */}
<div className="absolute bottom-2 sm:bottom-5 right-2 sm:right-5 flex gap-1.5 sm:gap-2 items-center bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg shadow-lg z-10"> <div className="absolute bottom-2 sm:bottom-5 right-2 sm:right-5 flex gap-1.5 sm:gap-2 items-center bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg shadow-lg z-10">
<button <Button
className="w-7 h-7 sm:w-8 sm:h-8 p-1 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded cursor-pointer transition-all flex items-center justify-center hover:bg-primary-600 hover:text-white hover:border-primary-600 dark:hover:border-primary-600 hover:shadow-md hover:shadow-primary-600/30 disabled:opacity-50 disabled:cursor-not-allowed" variant="outline"
onClick={handleCenterPattern} size="icon"
disabled={!pesData || patternUploaded || isUploading} className="w-7 h-7 sm:w-8 sm:h-8"
title="Center Pattern in Hoop" onClick={handleCenterPattern}
> disabled={!pesData || patternUploaded || isUploading}
<ArrowsPointingInIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" /> title="Center Pattern in Hoop"
</button> >
<button <ArrowsPointingInIcon className="w-4 h-4 sm:w-5 sm:h-5" />
className="w-7 h-7 sm:w-8 sm:h-8 p-1 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded cursor-pointer transition-all flex items-center justify-center hover:bg-primary-600 hover:text-white hover:border-primary-600 dark:hover:border-primary-600 hover:shadow-md hover:shadow-primary-600/30 disabled:opacity-50 disabled:cursor-not-allowed" </Button>
onClick={handleZoomIn} <Button
title="Zoom In" variant="outline"
> size="icon"
<PlusIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" /> className="w-7 h-7 sm:w-8 sm:h-8"
</button> onClick={handleZoomIn}
<span className="min-w-[40px] sm:min-w-[50px] text-center text-sm font-semibold text-gray-900 dark:text-gray-100 select-none"> title="Zoom In"
{Math.round(stageScale * 100)}% >
</span> <PlusIcon className="w-4 h-4 sm:w-5 sm:h-5" />
<button </Button>
className="w-7 h-7 sm:w-8 sm:h-8 p-1 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded cursor-pointer transition-all flex items-center justify-center hover:bg-primary-600 hover:text-white hover:border-primary-600 dark:hover:border-primary-600 hover:shadow-md hover:shadow-primary-600/30 disabled:opacity-50 disabled:cursor-not-allowed" <span className="min-w-[40px] sm:min-w-[50px] text-center text-sm font-semibold text-gray-900 dark:text-gray-100 select-none">
onClick={handleZoomOut} {Math.round(stageScale * 100)}%
title="Zoom Out" </span>
> <Button
<MinusIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" /> variant="outline"
</button> size="icon"
<button className="w-7 h-7 sm:w-8 sm:h-8"
className="w-7 h-7 sm:w-8 sm:h-8 p-1 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 rounded cursor-pointer transition-all flex items-center justify-center hover:bg-primary-600 hover:text-white hover:border-primary-600 dark:hover:border-primary-600 hover:shadow-md hover:shadow-primary-600/30 disabled:opacity-50 disabled:cursor-not-allowed ml-1" onClick={handleZoomOut}
onClick={handleZoomReset} title="Zoom Out"
title="Reset Zoom" >
> <MinusIcon className="w-4 h-4 sm:w-5 sm:h-5" />
<ArrowPathIcon className="w-4 h-4 sm:w-5 sm:h-5 dark:text-gray-200" /> </Button>
</button> <Button
</div> variant="outline"
</> size="icon"
)} className="w-7 h-7 sm:w-8 sm:h-8 ml-1"
</div> onClick={handleZoomReset}
</div> title="Reset Zoom"
>
<ArrowPathIcon className="w-4 h-4 sm:w-5 sm:h-5" />
</Button>
</div>
</>
)}
</div>
</CardContent>
</Card>
); );
} }

View file

@ -0,0 +1,75 @@
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
export function PatternCanvasPlaceholder() {
return (
<Card className="p-0 gap-0 lg:h-full animate-fadeIn flex flex-col">
<CardHeader className="p-6 pb-4 border-b-2 border-gray-300 dark:border-gray-600">
<CardTitle className="text-base lg:text-lg">Pattern Preview</CardTitle>
</CardHeader>
<CardContent className="p-6 flex-1 flex flex-col">
<div className="h-[400px] sm:h-[500px] lg:flex-1 flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600 relative overflow-hidden">
{/* Decorative background pattern */}
<div className="absolute inset-0 opacity-5 dark:opacity-10">
<div className="absolute top-10 left-10 w-32 h-32 border-4 border-gray-400 dark:border-gray-500 rounded-full"></div>
<div className="absolute bottom-10 right-10 w-40 h-40 border-4 border-gray-400 dark:border-gray-500 rounded-full"></div>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-48 h-48 border-4 border-gray-400 dark:border-gray-500 rounded-full"></div>
</div>
<div className="text-center relative z-10">
<div className="relative inline-block mb-6">
<svg
className="w-28 h-28 mx-auto text-gray-300 dark:text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
<div className="absolute -top-2 -right-2 w-8 h-8 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
<svg
className="w-5 h-5 text-primary-600 dark:text-primary-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
</div>
</div>
<h3 className="text-gray-700 dark:text-gray-200 text-base lg:text-lg font-semibold mb-2">
No Pattern Loaded
</h3>
<p className="text-gray-500 dark:text-gray-400 text-sm mb-4 max-w-sm mx-auto">
Connect to your machine and choose a PES embroidery file to see
your design preview
</p>
<div className="flex items-center justify-center gap-6 text-xs text-gray-400 dark:text-gray-500">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-primary-400 dark:bg-primary-500 rounded-full"></div>
<span>Drag to Position</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-success-400 dark:bg-success-500 rounded-full"></div>
<span>Zoom & Pan</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-accent-400 dark:bg-accent-500 rounded-full"></div>
<span>Real-time Preview</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
}

View file

@ -1,75 +0,0 @@
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
export function PatternPreviewPlaceholder() {
return (
<Card className="p-0 gap-0 lg:h-full animate-fadeIn flex flex-col">
<CardHeader className="p-6 pb-4 border-b-2 border-gray-300 dark:border-gray-600">
<CardTitle className="text-base lg:text-lg">Pattern Preview</CardTitle>
</CardHeader>
<CardContent className="p-6 flex-1 flex flex-col">
<div className="h-[400px] sm:h-[500px] lg:flex-1 flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600 relative overflow-hidden">
{/* Decorative background pattern */}
<div className="absolute inset-0 opacity-5 dark:opacity-10">
<div className="absolute top-10 left-10 w-32 h-32 border-4 border-gray-400 dark:border-gray-500 rounded-full"></div>
<div className="absolute bottom-10 right-10 w-40 h-40 border-4 border-gray-400 dark:border-gray-500 rounded-full"></div>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-48 h-48 border-4 border-gray-400 dark:border-gray-500 rounded-full"></div>
</div>
<div className="text-center relative z-10">
<div className="relative inline-block mb-6">
<svg
className="w-28 h-28 mx-auto text-gray-300 dark:text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
<div className="absolute -top-2 -right-2 w-8 h-8 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
<svg
className="w-5 h-5 text-primary-600 dark:text-primary-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
</div>
</div>
<h3 className="text-gray-700 dark:text-gray-200 text-base lg:text-lg font-semibold mb-2">
No Pattern Loaded
</h3>
<p className="text-gray-500 dark:text-gray-400 text-sm mb-4 max-w-sm mx-auto">
Connect to your machine and choose a PES embroidery file to see your
design preview
</p>
<div className="flex items-center justify-center gap-6 text-xs text-gray-400 dark:text-gray-500">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-primary-400 dark:bg-primary-500 rounded-full"></div>
<span>Drag to Position</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-success-400 dark:bg-success-500 rounded-full"></div>
<span>Zoom & Pan</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-accent-400 dark:bg-accent-500 rounded-full"></div>
<span>Real-time Preview</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
}