mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
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:
parent
bb066d7775
commit
7cf4a5de17
4 changed files with 331 additions and 311 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
75
src/components/PatternCanvasPlaceholder.tsx
Normal file
75
src/components/PatternCanvasPlaceholder.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue