added text section design layout
This commit is contained in:
parent
976978aec4
commit
954ac950b0
21 changed files with 2156 additions and 1822 deletions
12
src/App.jsx
12
src/App.jsx
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
// import Canvas from "./components/Canvas";
|
// import Canvas from "./components/Canvas";
|
||||||
import WebFont from "webfontloader";
|
import WebFont from "webfontloader";
|
||||||
|
|
@ -8,12 +8,12 @@ import SheetLeftPanel from "./components/Layouts/SheetLeftPanel";
|
||||||
import CanvasCapture from "./components/CanvasCapture";
|
import CanvasCapture from "./components/CanvasCapture";
|
||||||
import { Toaster } from "./components/ui/toaster";
|
import { Toaster } from "./components/ui/toaster";
|
||||||
import { Sidebar } from "./components/Layouts/LeftSidebar";
|
import { Sidebar } from "./components/Layouts/LeftSidebar";
|
||||||
import RightPanel from "./components/Layouts/RightPanel";
|
|
||||||
import { EditorPanel } from "./components/Panel/EditorPanel";
|
|
||||||
import { Canvas } from "./components/Panel/Canvas";
|
import { Canvas } from "./components/Panel/Canvas";
|
||||||
import TextPanel from "./components/Panel/TextPanel";
|
import TextPanel from "./components/Panel/TextPanel";
|
||||||
import { TopBar } from "./components/Panel/TopBar";
|
import { TopBar } from "./components/Panel/TopBar";
|
||||||
import { ActionButtons } from "./components/ActionButtons";
|
import { ActionButtons } from "./components/ActionButtons";
|
||||||
|
import EditorPanel from "./components/Panel/EditorPanel";
|
||||||
|
import CanvasContext from "./components/Context/canvasContext/CanvasContext";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -74,7 +74,7 @@ function App() {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [selectedItem, setSelectedItem] = useState("text");
|
const { selectedPanel } = useContext(CanvasContext);
|
||||||
const [hasSelectedObject, setHasSelectedObject] = useState(true);
|
const [hasSelectedObject, setHasSelectedObject] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -93,8 +93,8 @@ function App() {
|
||||||
// </div>
|
// </div>
|
||||||
|
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
<Sidebar selectedItem={selectedItem} onItemSelect={setSelectedItem} />
|
<Sidebar />
|
||||||
{selectedItem === "text" && <TextPanel />}
|
{selectedPanel !== "" && <EditorPanel />}
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<TopBar isVisible={hasSelectedObject} />
|
<TopBar isVisible={hasSelectedObject} />
|
||||||
<ActionButtons />
|
<ActionButtons />
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,58 @@
|
||||||
import { ChevronDown } from "lucide-react";
|
import CanvasContext from "./Context/canvasContext/CanvasContext";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "./ui/select";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
const aspectRatios = [
|
||||||
|
{ value: "1:1", label: "Square (1:1)" },
|
||||||
|
{ value: "4:3", label: "Standard (4:3)" },
|
||||||
|
{ value: "3:2", label: "Classic (3:2)" },
|
||||||
|
{ value: "16:9", label: "Widescreen (16:9)" },
|
||||||
|
{ value: "9:16", label: "Portrait (9:16)" },
|
||||||
|
{ value: "21:9", label: "Ultrawide (21:9)" },
|
||||||
|
{ value: "32:9", label: "Super Ultrawide (32:9)" },
|
||||||
|
{ value: "1.85:1", label: "Cinema Standard (1.85:1)" },
|
||||||
|
{ value: "2.39:1", label: "Anamorphic Widescreen (2.39:1)" },
|
||||||
|
{ value: "2.76:1", label: "Ultra Panavision 70 (2.76:1)" },
|
||||||
|
{ value: "5:4", label: "Large Format (5:4)" },
|
||||||
|
{ value: "7:5", label: "Artistic Format (7:5)" },
|
||||||
|
{ value: "11:8.5", label: "Letter Size (11:8.5)" },
|
||||||
|
{ value: "3:4", label: "Portrait (3:4)" },
|
||||||
|
{ value: "1.91:1", label: "Facebook Ads (1.91:1)" },
|
||||||
|
];
|
||||||
|
|
||||||
export function ActionButtons() {
|
export function ActionButtons() {
|
||||||
|
const { setCanvasRatio, canvasRatio } = useContext(CanvasContext);
|
||||||
|
const handleRatioChange = (newRatio) => {
|
||||||
|
setCanvasRatio(newRatio);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-4 right-0 z-50 flex items-center gap-2 bg-white rounded-l-[16px]">
|
<div className="absolute top-4 right-0 z-50 flex items-center gap-2 bg-white rounded-l-[16px]">
|
||||||
<div className="px-2 py-2">
|
<div className="px-2 py-2">
|
||||||
<Button variant="ghost" className="gap-2 h-9 px-2">
|
<div className="w-full">
|
||||||
<svg
|
<Select onValueChange={handleRatioChange} value={canvasRatio}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<SelectTrigger className="w-full text-xs font-bold">
|
||||||
width="12"
|
<SelectValue placeholder="Select aspect ratio" />
|
||||||
height="12"
|
</SelectTrigger>
|
||||||
viewBox="0 0 12 12"
|
<SelectContent>
|
||||||
fill="none"
|
{aspectRatios.map((ratio) => (
|
||||||
|
<SelectItem
|
||||||
|
key={ratio.value}
|
||||||
|
value={ratio.value}
|
||||||
|
className="text-xs font-bold"
|
||||||
>
|
>
|
||||||
<g clipPath="url(#clip0_71_2678)">
|
{ratio.label}
|
||||||
<path d="M1 3.5L11 3.5" stroke="black" strokeLinecap="round" />
|
</SelectItem>
|
||||||
<path
|
))}
|
||||||
d="M1 8.5769L11 8.57691"
|
</SelectContent>
|
||||||
stroke="black"
|
</Select>
|
||||||
strokeLinecap="round"
|
</div>
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M9.0769 1L9.0769 11"
|
|
||||||
stroke="black"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M3.4231 1L3.4231 11"
|
|
||||||
stroke="black"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_71_2678">
|
|
||||||
<rect width="12" height="12" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
Resize <ChevronDown className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-5">
|
<div className="mr-5">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from "react";
|
||||||
import CanvasContext from './CanvasContext';
|
import CanvasContext from "./CanvasContext";
|
||||||
|
|
||||||
const CanvasContextProvider = ({ children }) => {
|
const CanvasContextProvider = ({ children }) => {
|
||||||
const canvasRef = useRef(null);
|
const canvasRef = useRef(null);
|
||||||
|
|
@ -7,13 +7,32 @@ const CanvasContextProvider = ({ children }) => {
|
||||||
const [canvasHeight, setCanvasHeight] = useState(0);
|
const [canvasHeight, setCanvasHeight] = useState(0);
|
||||||
const [canvasWidth, setCanvasWidth] = useState(0);
|
const [canvasWidth, setCanvasWidth] = useState(0);
|
||||||
const [screenWidth, setScreenWidth] = useState(0);
|
const [screenWidth, setScreenWidth] = useState(0);
|
||||||
|
const [canvasRatio, setCanvasRatio] = useState("4:3");
|
||||||
|
const [selectedPanel, setSelectedPanel] = useState("");
|
||||||
const fabricCanvasRef = useRef(null);
|
const fabricCanvasRef = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CanvasContext.Provider value={{ canvasRef, canvas, setCanvas, fabricCanvasRef, canvasHeight, setCanvasHeight, canvasWidth, setCanvasWidth, screenWidth, setScreenWidth }}>
|
<CanvasContext.Provider
|
||||||
|
value={{
|
||||||
|
canvasRef,
|
||||||
|
canvas,
|
||||||
|
setCanvas,
|
||||||
|
fabricCanvasRef,
|
||||||
|
canvasHeight,
|
||||||
|
canvasRatio,
|
||||||
|
setCanvasRatio,
|
||||||
|
selectedPanel,
|
||||||
|
setSelectedPanel,
|
||||||
|
setCanvasHeight,
|
||||||
|
canvasWidth,
|
||||||
|
setCanvasWidth,
|
||||||
|
screenWidth,
|
||||||
|
setScreenWidth,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</CanvasContext.Provider>
|
</CanvasContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CanvasContextProvider
|
export default CanvasContextProvider;
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,60 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { useContext, useRef, useState } from 'react';
|
import { useContext, useRef, useState } from "react";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import { fabric } from 'fabric';
|
import { fabric } from "fabric";
|
||||||
import Resizer from "react-image-file-resizer";
|
import Resizer from "react-image-file-resizer";
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
import { Slider } from '@/components/ui/slider';
|
Select,
|
||||||
import { HardDriveUpload, ImagePlus, Upload, Crop, RotateCw, FileType, ChevronUp, ChevronDown } from 'lucide-react';
|
SelectContent,
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
SelectItem,
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
SelectTrigger,
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
SelectValue,
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
} from "@/components/ui/select";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import {
|
||||||
|
HardDriveUpload,
|
||||||
|
ImagePlus,
|
||||||
|
Upload,
|
||||||
|
Crop,
|
||||||
|
RotateCw,
|
||||||
|
FileType,
|
||||||
|
ChevronUp,
|
||||||
|
ChevronDown,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{ icon: Upload, title: 'Auto-Resize', description: 'Images larger than 1080px are automatically resized' },
|
{
|
||||||
{ icon: Crop, title: 'Custom Dimensions', description: 'Set your preferred resize dimensions' },
|
icon: Upload,
|
||||||
{ icon: RotateCw, title: 'Quality & Rotation', description: 'Adjust image quality and rotation as needed' },
|
title: "Auto-Resize",
|
||||||
{ icon: FileType, title: 'Format Conversion', description: 'Convert between various image formats' },
|
description: "Images larger than 1080px are automatically resized",
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
icon: Crop,
|
||||||
|
title: "Custom Dimensions",
|
||||||
|
description: "Set your preferred resize dimensions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RotateCw,
|
||||||
|
title: "Quality & Rotation",
|
||||||
|
description: "Adjust image quality and rotation as needed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: FileType,
|
||||||
|
title: "Format Conversion",
|
||||||
|
description: "Convert between various image formats",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const AddImageIntoShape = () => {
|
const AddImageIntoShape = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
@ -29,7 +64,7 @@ const AddImageIntoShape = () => {
|
||||||
const [height, setHeight] = useState(1080);
|
const [height, setHeight] = useState(1080);
|
||||||
const [quality, setQuality] = useState(100);
|
const [quality, setQuality] = useState(100);
|
||||||
const [rotation, setRotation] = useState("0");
|
const [rotation, setRotation] = useState("0");
|
||||||
const [format, setFormat] = useState('JPEG');
|
const [format, setFormat] = useState("JPEG");
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
|
@ -44,7 +79,7 @@ const AddImageIntoShape = () => {
|
||||||
(resizedFile) => {
|
(resizedFile) => {
|
||||||
callback(resizedFile); // Pass the resized file to the callback
|
callback(resizedFile); // Pass the resized file to the callback
|
||||||
},
|
},
|
||||||
'file' // Output type
|
"file" // Output type
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -155,14 +190,14 @@ const AddImageIntoShape = () => {
|
||||||
top: activeObject.top,
|
top: activeObject.top,
|
||||||
clipPath: activeObject, // Apply clipPath to the image
|
clipPath: activeObject, // Apply clipPath to the image
|
||||||
originX: activeObject.originX, // Match origin point
|
originX: activeObject.originX, // Match origin point
|
||||||
originY: activeObject.originY // Match origin point
|
originY: activeObject.originY, // Match origin point
|
||||||
});
|
});
|
||||||
|
|
||||||
// Adjust position based on the clipPath's transformations
|
// Adjust position based on the clipPath's transformations
|
||||||
fabricImage.set({
|
fabricImage.set({
|
||||||
left: activeObject.left,
|
left: activeObject.left,
|
||||||
top: activeObject.top,
|
top: activeObject.top,
|
||||||
angle: activeObject.angle // Match rotation if any
|
angle: activeObject.angle, // Match rotation if any
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.add(fabricImage);
|
canvas.add(fabricImage);
|
||||||
|
|
@ -176,7 +211,6 @@ const AddImageIntoShape = () => {
|
||||||
const content = () => {
|
const content = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
{/* <Card className="my-2">
|
{/* <Card className="my-2">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<Alert>
|
<Alert>
|
||||||
|
|
@ -213,13 +247,21 @@ const AddImageIntoShape = () => {
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription className="mt-1">
|
<AlertDescription className="mt-1">
|
||||||
<p className="mb-1">
|
<p className="mb-1">
|
||||||
Insert and customize images within shapes. Adjust image position and clipping after insertion.
|
Insert and customize images within shapes. Adjust image
|
||||||
|
position and clipping after insertion.
|
||||||
</p>
|
</p>
|
||||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button variant="outline" className="w-full justify-between mt-2">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-between mt-2"
|
||||||
|
>
|
||||||
<span>Key Features</span>
|
<span>Key Features</span>
|
||||||
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
{isOpen ? (
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
|
|
@ -231,8 +273,12 @@ const AddImageIntoShape = () => {
|
||||||
>
|
>
|
||||||
<feature.icon className="h-6 w-6 mr-3 flex-shrink-0 text-primary" />
|
<feature.icon className="h-6 w-6 mr-3 flex-shrink-0 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold mb-1">{feature.title}</h3>
|
<h3 className="font-semibold mb-1">
|
||||||
<p className="text-sm text-gray-600">{feature.description}</p>
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -247,7 +293,7 @@ const AddImageIntoShape = () => {
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<p className="text-red-600 text-sm mt-2">{errorMessage}</p>
|
<p className="text-red-600 text-sm mt-2">{errorMessage}</p>
|
||||||
)}
|
)}
|
||||||
<div className='flex flex-col w-[100%]'>
|
<div className="flex flex-col w-[100%]">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{/* Width Slider */}
|
{/* Width Slider */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -274,8 +320,7 @@ const AddImageIntoShape = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quality Slider */}
|
{/* Quality Slider */}
|
||||||
{
|
{format === "JPEG" && (
|
||||||
format === "JPEG" &&
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">Quality: {quality}%</Label>
|
<Label className="text-xs">Quality: {quality}%</Label>
|
||||||
<Slider
|
<Slider
|
||||||
|
|
@ -285,14 +330,17 @@ const AddImageIntoShape = () => {
|
||||||
onValueChange={(value) => setQuality(value[0])}
|
onValueChange={(value) => setQuality(value[0])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-1 items-center'>
|
<div className="grid grid-cols-2 gap-1 items-center">
|
||||||
{/* Rotation */}
|
{/* Rotation */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">Rotation: {rotation}°</Label>
|
<Label className="text-xs">Rotation: {rotation}°</Label>
|
||||||
|
|
||||||
<Select value={rotation} onValueChange={(value) => setRotation(value)}>
|
<Select
|
||||||
|
value={rotation}
|
||||||
|
onValueChange={(value) => setRotation(value)}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select rotation in degree" />
|
<SelectValue placeholder="Select rotation in degree" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -309,7 +357,10 @@ const AddImageIntoShape = () => {
|
||||||
{/* Format Dropdown */}
|
{/* Format Dropdown */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">Format</Label>
|
<Label className="text-xs">Format</Label>
|
||||||
<Select value={format} onValueChange={(value) => setFormat(value)}>
|
<Select
|
||||||
|
value={format}
|
||||||
|
onValueChange={(value) => setFormat(value)}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select format" />
|
<SelectValue placeholder="Select format" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -331,15 +382,16 @@ const AddImageIntoShape = () => {
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
onChange={fileHandler} />
|
onChange={fileHandler}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex flex-col p-2">
|
<Card className="flex flex-col shadow-none border-0">
|
||||||
<CollapsibleComponent text={"Insert Image"}>
|
<CollapsibleComponent text={"Insert Image"}>
|
||||||
{content()}
|
{content()}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
|
|
@ -348,6 +400,3 @@ const AddImageIntoShape = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddImageIntoShape;
|
export default AddImageIntoShape;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { Button } from "@/components/ui/button";
|
import {
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
Collapsible,
|
||||||
import { ChevronUp, ChevronDown } from "lucide-react";
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
const CollapsibleComponent = ({ children, text }) => {
|
const CollapsibleComponent = ({ children, text }) => {
|
||||||
|
|
@ -10,27 +12,25 @@ const CollapsibleComponent = ({ children, text }) => {
|
||||||
// Check if the text prop is "Canvas Setting" and set isOpen to false
|
// Check if the text prop is "Canvas Setting" and set isOpen to false
|
||||||
if (text === "Canvas Setting") {
|
if (text === "Canvas Setting") {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
} else {
|
||||||
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
else {
|
}, [text]);
|
||||||
setIsOpen(true)
|
|
||||||
}
|
|
||||||
}, [text])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<div className={`flex items-center justify-between cursor-pointer ${!isOpen ? "my-2" : ""}`}>
|
<div
|
||||||
<h2 className='font-bold'>{text}</h2>
|
className={`flex items-center justify-between cursor-pointer ${
|
||||||
<Button variant={"outline"}>
|
!isOpen ? "my-2" : ""
|
||||||
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
}`}
|
||||||
</Button>
|
>
|
||||||
|
<h2 className="font-bold mb-2">{text}</h2>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>{children}</CollapsibleContent>
|
||||||
{children}
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CollapsibleComponent
|
export default CollapsibleComponent;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { Lock, Unlock } from "lucide-react";
|
import { Lock, Unlock } from "lucide-react";
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
||||||
const LockObject = () => {
|
const LockObject = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
@ -15,11 +15,10 @@ const LockObject = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject?.lockMovementX === false) {
|
if (activeObject?.lockMovementX === false) {
|
||||||
setIsLocked(false);
|
setIsLocked(false);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
setIsLocked(true);
|
setIsLocked(true);
|
||||||
}
|
}
|
||||||
}, [activeObject])
|
}, [activeObject]);
|
||||||
|
|
||||||
const toggleLock = () => {
|
const toggleLock = () => {
|
||||||
if (!canvas || !activeObject) {
|
if (!canvas || !activeObject) {
|
||||||
|
|
@ -27,7 +26,7 @@ const LockObject = () => {
|
||||||
title: "No object selected",
|
title: "No object selected",
|
||||||
description: "Please select an object to lock or unlock.",
|
description: "Please select an object to lock or unlock.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
})
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,13 +44,15 @@ const LockObject = () => {
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: newLockState ? "Object locked" : "Object unlocked",
|
title: newLockState ? "Object locked" : "Object unlocked",
|
||||||
description: newLockState ? "The object is now locked in place." : "The object can now be moved and resized.",
|
description: newLockState
|
||||||
})
|
? "The object is now locked in place."
|
||||||
}
|
: "The object can now be moved and resized.",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-2">
|
<Card className="shadow-none border-0">
|
||||||
<h2 className='font-bold'>{!isLocked ? "Lock" : "Unlock"} Object</h2>
|
<h2 className="font-bold">{!isLocked ? "Lock" : "Unlock"} Object</h2>
|
||||||
<Button
|
<Button
|
||||||
onClick={toggleLock}
|
onClick={toggleLock}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -59,10 +60,14 @@ const LockObject = () => {
|
||||||
disabled={!activeObject}
|
disabled={!activeObject}
|
||||||
title={isLocked ? "Unlock object" : "Lock object"}
|
title={isLocked ? "Unlock object" : "Lock object"}
|
||||||
>
|
>
|
||||||
{isLocked ? <Unlock className="h-4 w-4" /> : <Lock className="h-4 w-4" />}
|
{isLocked ? (
|
||||||
|
<Unlock className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Lock className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LockObject
|
export default LockObject;
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,54 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from "@/components/ui/label";
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, ChevronUp, ChevronDown } from 'lucide-react';
|
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
|
||||||
|
|
||||||
const PositionCustomization = () => {
|
const PositionCustomization = () => {
|
||||||
const { canvas } = useContext(CanvasContext)
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { activeObject } = useContext(ActiveObjectContext)
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
const [objectPosition, setObjectPosition] = useState({ left: 50, top: 50 })
|
const [objectPosition, setObjectPosition] = useState({ left: 50, top: 50 });
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject) {
|
if (activeObject) {
|
||||||
setObjectPosition({
|
setObjectPosition({
|
||||||
left: Math.round(activeObject.left || 0),
|
left: Math.round(activeObject.left || 0),
|
||||||
top: Math.round(activeObject.top || 0),
|
top: Math.round(activeObject.top || 0),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [activeObject])
|
}, [activeObject]);
|
||||||
|
|
||||||
const updateObjectPosition = (key, value) => {
|
const updateObjectPosition = (key, value) => {
|
||||||
const updatedPosition = { ...objectPosition, [key]: value }
|
const updatedPosition = { ...objectPosition, [key]: value };
|
||||||
setObjectPosition(updatedPosition)
|
setObjectPosition(updatedPosition);
|
||||||
|
|
||||||
if (canvas && activeObject) {
|
if (canvas && activeObject) {
|
||||||
activeObject.set(updatedPosition)
|
activeObject.set(updatedPosition);
|
||||||
canvas.renderAll()
|
canvas.renderAll();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleInputChange = (key, value) => {
|
const handleInputChange = (key, value) => {
|
||||||
const numValue = parseInt(value, 10)
|
const numValue = parseInt(value, 10);
|
||||||
if (!isNaN(numValue)) {
|
if (!isNaN(numValue)) {
|
||||||
updateObjectPosition(key, numValue)
|
updateObjectPosition(key, numValue);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSliderChange = (key, value) => {
|
const handleSliderChange = (key, value) => {
|
||||||
updateObjectPosition(key, value[0])
|
updateObjectPosition(key, value[0]);
|
||||||
}
|
};
|
||||||
|
|
||||||
const adjustPosition = (key, delta) => {
|
const adjustPosition = (key, delta) => {
|
||||||
const newValue = objectPosition[key] + delta
|
const newValue = objectPosition[key] + delta;
|
||||||
updateObjectPosition(key, newValue)
|
updateObjectPosition(key, newValue);
|
||||||
}
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -64,7 +61,7 @@ const PositionCustomization = () => {
|
||||||
id="position-left"
|
id="position-left"
|
||||||
type="number"
|
type="number"
|
||||||
value={objectPosition.left}
|
value={objectPosition.left}
|
||||||
onChange={(e) => handleInputChange('left', e.target.value)}
|
onChange={(e) => handleInputChange("left", e.target.value)}
|
||||||
className="w-20"
|
className="w-20"
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
|
|
@ -72,7 +69,7 @@ const PositionCustomization = () => {
|
||||||
max={canvas ? canvas.width : 1000}
|
max={canvas ? canvas.width : 1000}
|
||||||
step={1}
|
step={1}
|
||||||
value={[objectPosition.left]}
|
value={[objectPosition.left]}
|
||||||
onValueChange={(value) => handleSliderChange('left', value)}
|
onValueChange={(value) => handleSliderChange("left", value)}
|
||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -84,7 +81,7 @@ const PositionCustomization = () => {
|
||||||
id="position-top"
|
id="position-top"
|
||||||
type="number"
|
type="number"
|
||||||
value={objectPosition.top}
|
value={objectPosition.top}
|
||||||
onChange={(e) => handleInputChange('top', e.target.value)}
|
onChange={(e) => handleInputChange("top", e.target.value)}
|
||||||
className="w-20"
|
className="w-20"
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
|
|
@ -92,7 +89,7 @@ const PositionCustomization = () => {
|
||||||
max={canvas ? canvas.height : 1000}
|
max={canvas ? canvas.height : 1000}
|
||||||
step={1}
|
step={1}
|
||||||
value={[objectPosition.top]}
|
value={[objectPosition.top]}
|
||||||
onValueChange={(value) => handleSliderChange('top', value)}
|
onValueChange={(value) => handleSliderChange("top", value)}
|
||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -102,7 +99,7 @@ const PositionCustomization = () => {
|
||||||
<div className="w-32 h-32 grid grid-cols-3 gap-1 p-2 mx-auto">
|
<div className="w-32 h-32 grid grid-cols-3 gap-1 p-2 mx-auto">
|
||||||
<div className="col-start-2">
|
<div className="col-start-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => adjustPosition('top', -1)}
|
onClick={() => adjustPosition("top", -1)}
|
||||||
aria-label="Move up"
|
aria-label="Move up"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
@ -113,7 +110,7 @@ const PositionCustomization = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="col-start-1 row-start-2">
|
<div className="col-start-1 row-start-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => adjustPosition('left', -1)}
|
onClick={() => adjustPosition("left", -1)}
|
||||||
aria-label="Move left"
|
aria-label="Move left"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
@ -124,7 +121,7 @@ const PositionCustomization = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="col-start-3 row-start-2">
|
<div className="col-start-3 row-start-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => adjustPosition('left', 1)}
|
onClick={() => adjustPosition("left", 1)}
|
||||||
aria-label="Move right"
|
aria-label="Move right"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
@ -135,7 +132,7 @@ const PositionCustomization = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="col-start-2 row-start-3">
|
<div className="col-start-2 row-start-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => adjustPosition('top', 1)}
|
onClick={() => adjustPosition("top", 1)}
|
||||||
aria-label="Move down"
|
aria-label="Move down"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
@ -145,18 +142,17 @@ const PositionCustomization = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-2">
|
<Card className="shadow-none border-0">
|
||||||
<CollapsibleComponent text={"Position Control"}>
|
<CollapsibleComponent text={"Position Control"}>
|
||||||
{content()}
|
{content()}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PositionCustomization
|
export default PositionCustomization;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const ScaleObjects = () => {
|
||||||
setScaleX(activeObject?.scaleX);
|
setScaleX(activeObject?.scaleX);
|
||||||
setScaleY(activeObject?.scaleY);
|
setScaleY(activeObject?.scaleY);
|
||||||
}
|
}
|
||||||
}, [activeObject])
|
}, [activeObject]);
|
||||||
|
|
||||||
// Handle scaleX changes
|
// Handle scaleX changes
|
||||||
const handleScaleXChange = (value) => {
|
const handleScaleXChange = (value) => {
|
||||||
|
|
@ -79,11 +79,11 @@ const ScaleObjects = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="grid items-center gap-2 p-2">
|
<Card className="grid items-center gap-2 shadow-none border-0">
|
||||||
<CollapsibleComponent text={"Scale Control"}>
|
<CollapsibleComponent text={"Scale Control"}>
|
||||||
{content()}
|
{content()}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,35 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
import { useContext, useEffect, useRef, useState } from 'react'
|
Select,
|
||||||
import { Card, } from '@/components/ui/card';
|
SelectContent,
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
SelectItem,
|
||||||
import { Separator } from '@/components/ui/separator';
|
SelectTrigger,
|
||||||
import { fabric } from 'fabric';
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { fabric } from "fabric";
|
||||||
|
|
||||||
const SelectObjectFromGroup = () => {
|
const SelectObjectFromGroup = () => {
|
||||||
const [groupObjects, setGroupObjects] = useState([])
|
const [groupObjects, setGroupObjects] = useState([]);
|
||||||
const [selectedObject, setSelectedObject] = useState(null)
|
const [selectedObject, setSelectedObject] = useState(null);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext)
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
const { canvas } = useContext(CanvasContext)
|
const { canvas } = useContext(CanvasContext);
|
||||||
const previewCanvasRef = useRef(null)
|
const previewCanvasRef = useRef(null);
|
||||||
|
|
||||||
const activeObject = canvas?.getActiveObject()
|
const activeObject = canvas?.getActiveObject();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject?.type === 'group') {
|
if (activeObject?.type === "group") {
|
||||||
setGroupObjects(activeObject._objects || [])
|
setGroupObjects(activeObject._objects || []);
|
||||||
setSelectedObject(null)
|
setSelectedObject(null);
|
||||||
} else {
|
} else {
|
||||||
setGroupObjects([])
|
setGroupObjects([]);
|
||||||
setSelectedObject(null)
|
setSelectedObject(null);
|
||||||
}
|
}
|
||||||
}, [activeObject])
|
}, [activeObject]);
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// if (selectedObject && previewCanvasRef.current) {
|
// if (selectedObject && previewCanvasRef.current) {
|
||||||
|
|
@ -61,7 +67,6 @@ const SelectObjectFromGroup = () => {
|
||||||
// }
|
// }
|
||||||
// }, [selectedObject])
|
// }, [selectedObject])
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedObject && previewCanvasRef.current) {
|
if (selectedObject && previewCanvasRef.current) {
|
||||||
// Create a new static canvas
|
// Create a new static canvas
|
||||||
|
|
@ -69,7 +74,7 @@ const SelectObjectFromGroup = () => {
|
||||||
|
|
||||||
previewCanvas.setDimensions({
|
previewCanvas.setDimensions({
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100
|
height: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear previous objects (if any)
|
// Clear previous objects (if any)
|
||||||
|
|
@ -88,8 +93,8 @@ const SelectObjectFromGroup = () => {
|
||||||
clonedObject.set({
|
clonedObject.set({
|
||||||
left: 50,
|
left: 50,
|
||||||
top: 50,
|
top: 50,
|
||||||
originX: 'center',
|
originX: "center",
|
||||||
originY: 'center',
|
originY: "center",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add the cloned object to preview canvas
|
// Add the cloned object to preview canvas
|
||||||
|
|
@ -105,25 +110,28 @@ const SelectObjectFromGroup = () => {
|
||||||
}
|
}
|
||||||
}, [selectedObject, previewCanvasRef]);
|
}, [selectedObject, previewCanvasRef]);
|
||||||
|
|
||||||
|
|
||||||
const handleSelectObject = (value) => {
|
const handleSelectObject = (value) => {
|
||||||
const selected = groupObjects[parseInt(value)]
|
const selected = groupObjects[parseInt(value)];
|
||||||
setSelectedObject(selected)
|
setSelectedObject(selected);
|
||||||
setActiveObject(selected)
|
setActiveObject(selected);
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!activeObject || activeObject.type !== 'group') {
|
if (!activeObject || activeObject.type !== "group") {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card className="p-4">
|
<Card className="p-4 shadow-none border-0">
|
||||||
<h2 className='font-bold mb-4'>Group Objects</h2>
|
<h2 className="font-bold mb-4">Group Objects</h2>
|
||||||
<div className='flex flex-col items-center justify-center space-y-4'>
|
<div className="flex flex-col items-center justify-center space-y-4">
|
||||||
<Select
|
<Select
|
||||||
onValueChange={handleSelectObject}
|
onValueChange={handleSelectObject}
|
||||||
value={selectedObject ? groupObjects.indexOf(selectedObject).toString() : undefined}
|
value={
|
||||||
|
selectedObject
|
||||||
|
? groupObjects.indexOf(selectedObject).toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full max-w-xs">
|
<SelectTrigger className="w-full max-w-xs">
|
||||||
<SelectValue placeholder="Select object" />
|
<SelectValue placeholder="Select object" />
|
||||||
|
|
@ -138,7 +146,7 @@ const SelectObjectFromGroup = () => {
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{selectedObject && (
|
{selectedObject && (
|
||||||
<div className='w-[100px] h-[100px] bg-muted rounded-md overflow-hidden border border-gray-300'>
|
<div className="w-[100px] h-[100px] bg-muted rounded-md overflow-hidden border border-gray-300">
|
||||||
<canvas ref={previewCanvasRef} width={100} height={100} />
|
<canvas ref={previewCanvasRef} width={100} height={100} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -146,6 +154,6 @@ const SelectObjectFromGroup = () => {
|
||||||
</Card>
|
</Card>
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default SelectObjectFromGroup;
|
export default SelectObjectFromGroup;
|
||||||
|
|
@ -28,7 +28,7 @@ const ShadowCustomization = () => {
|
||||||
setBlur(activeObject?.shadow?.blur || 0.5);
|
setBlur(activeObject?.shadow?.blur || 0.5);
|
||||||
setOpacity(activeObject?.shadow?.opacity || 0.5);
|
setOpacity(activeObject?.shadow?.opacity || 0.5);
|
||||||
}
|
}
|
||||||
}, [activeObject])
|
}, [activeObject]);
|
||||||
|
|
||||||
const handleApplyShadow = () => {
|
const handleApplyShadow = () => {
|
||||||
if (activeObject && canvas) {
|
if (activeObject && canvas) {
|
||||||
|
|
@ -44,14 +44,14 @@ const ShadowCustomization = () => {
|
||||||
activeObject.dirty = true; // Mark the object as dirty for re-render
|
activeObject.dirty = true; // Mark the object as dirty for re-render
|
||||||
canvas.renderAll(); // Trigger canvas re-render
|
canvas.renderAll(); // Trigger canvas re-render
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDisableShadow = () => {
|
const handleDisableShadow = () => {
|
||||||
if (activeObject && canvas) {
|
if (activeObject && canvas) {
|
||||||
activeObject.set("shadow", null)
|
activeObject.set("shadow", null);
|
||||||
canvas.renderAll()
|
canvas.renderAll();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -116,22 +116,31 @@ const ShadowCustomization = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-32 h-32 bg-white rounded-md flex items-center justify-center mx-auto border-2 my-4" style={{
|
<div
|
||||||
boxShadow: `${offsetX}px ${offsetY}px ${blur}px ${shadowColor}${Math.round(opacity * 255).toString(16).padStart(2, '0')}`
|
className="w-32 h-32 bg-white rounded-md flex items-center justify-center mx-auto border-2 my-4"
|
||||||
}}>
|
style={{
|
||||||
|
boxShadow: `${offsetX}px ${offsetY}px ${blur}px ${shadowColor}${Math.round(
|
||||||
|
opacity * 255
|
||||||
|
)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span className="text-4xl">A</span>
|
<span className="text-4xl">A</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Button onClick={handleApplyShadow}>Apply Shadow</Button>
|
<Button onClick={handleApplyShadow}>Apply Shadow</Button>
|
||||||
<Button variant="outline" onClick={handleDisableShadow}>Disable Shadow</Button>
|
<Button variant="outline" onClick={handleDisableShadow}>
|
||||||
|
Disable Shadow
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className='p-2'>
|
<Card className="shadow-none border-0">
|
||||||
<CollapsibleComponent text={"Shadow Control"}>
|
<CollapsibleComponent text={"Shadow Control"}>
|
||||||
{content()}
|
{content()}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
|
|
@ -140,4 +149,3 @@ const ShadowCustomization = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShadowCustomization;
|
export default ShadowCustomization;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from "@/components/ui/card";
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from "@/components/ui/label";
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { useContext, useEffect, useState } from 'react'
|
import { useContext, useEffect, useState } from "react";
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
|
|
||||||
const SkewCustomization = () => {
|
const SkewCustomization = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
@ -17,7 +17,7 @@ const SkewCustomization = () => {
|
||||||
setSkewX(activeObject?.skewX);
|
setSkewX(activeObject?.skewX);
|
||||||
setSkewY(activeObject?.skewY);
|
setSkewY(activeObject?.skewY);
|
||||||
}
|
}
|
||||||
}, [activeObject])
|
}, [activeObject]);
|
||||||
|
|
||||||
// Update skewX directly
|
// Update skewX directly
|
||||||
const handleSkewXChange = (value) => {
|
const handleSkewXChange = (value) => {
|
||||||
|
|
@ -52,7 +52,7 @@ const SkewCustomization = () => {
|
||||||
step={1}
|
step={1}
|
||||||
value={[skewX]}
|
value={[skewX]}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
handleSkewXChange(value)
|
handleSkewXChange(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -65,21 +65,21 @@ const SkewCustomization = () => {
|
||||||
step={1}
|
step={1}
|
||||||
value={[skewY]}
|
value={[skewY]}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
handleSkewYChange(value)
|
handleSkewYChange(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-2">
|
<Card className="shadow-none border-0">
|
||||||
<CollapsibleComponent text={"Skew Control"}>
|
<CollapsibleComponent text={"Skew Control"}>
|
||||||
{content()}
|
{content()}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SkewCustomization
|
export default SkewCustomization;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,22 @@
|
||||||
import { useContext, useState, useRef, useEffect } from 'react'
|
import { useContext, useState, useRef, useEffect } from "react";
|
||||||
import { fabric } from 'fabric'
|
import { fabric } from "fabric";
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext'
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext'
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import {
|
||||||
import { Slider } from '@/components/ui/slider'
|
Select,
|
||||||
import { Button } from '@/components/ui/button'
|
SelectContent,
|
||||||
import { Paintbrush, ContrastIcon as Gradient } from 'lucide-react'
|
SelectItem,
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
SelectTrigger,
|
||||||
import CollapsibleComponent from './CollapsibleComponent'
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Paintbrush } from "lucide-react";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
|
|
||||||
const StrokeCustomization = () => {
|
const StrokeCustomization = () => {
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
|
|
@ -26,10 +32,8 @@ const StrokeCustomization = () => {
|
||||||
const [colorType, setColorType] = useState("color");
|
const [colorType, setColorType] = useState("color");
|
||||||
const [gradientDirection, setGradientDirection] = useState("to bottom");
|
const [gradientDirection, setGradientDirection] = useState("to bottom");
|
||||||
|
|
||||||
|
|
||||||
// Utility function to handle styles of objects
|
// Utility function to handle styles of objects
|
||||||
const handleObjectStyle = (object) => {
|
const handleObjectStyle = (object) => {
|
||||||
// Determine fill type (solid or gradient)
|
|
||||||
if (object.stroke) {
|
if (object.stroke) {
|
||||||
if (typeof object.stroke === "string") {
|
if (typeof object.stroke === "string") {
|
||||||
setColorType("color");
|
setColorType("color");
|
||||||
|
|
@ -42,7 +46,6 @@ const StrokeCustomization = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle stroke width
|
|
||||||
if (object.strokeWidth) {
|
if (object.strokeWidth) {
|
||||||
setStrokeWidth(object.strokeWidth || 0);
|
setStrokeWidth(object.strokeWidth || 0);
|
||||||
}
|
}
|
||||||
|
|
@ -74,10 +77,10 @@ const StrokeCustomization = () => {
|
||||||
if (!previewRef.current) return;
|
if (!previewRef.current) return;
|
||||||
|
|
||||||
const previewStyle = {
|
const previewStyle = {
|
||||||
width: '80px',
|
width: "80px",
|
||||||
height: '80px',
|
height: "80px",
|
||||||
border: `${strokeWidth}px solid`,
|
border: `${strokeWidth}px solid`,
|
||||||
borderRadius: '4px',
|
borderRadius: "4px",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (colorType === "color") {
|
if (colorType === "color") {
|
||||||
|
|
@ -91,7 +94,13 @@ const StrokeCustomization = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}, [strokeWidth, strokeColor, gradientStrokeColors, colorType, gradientDirection]);
|
}, [
|
||||||
|
strokeWidth,
|
||||||
|
strokeColor,
|
||||||
|
gradientStrokeColors,
|
||||||
|
colorType,
|
||||||
|
gradientDirection,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleStrokeWidthChange = (value) => {
|
const handleStrokeWidthChange = (value) => {
|
||||||
setStrokeWidth(value);
|
setStrokeWidth(value);
|
||||||
|
|
@ -106,7 +115,7 @@ const StrokeCustomization = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGradientColorChange = (key, e) => {
|
const handleGradientColorChange = (key, e) => {
|
||||||
setGradientStrokeColors(prev => ({ ...prev, [key]: e.target.value }));
|
setGradientStrokeColors((prev) => ({ ...prev, [key]: e.target.value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyStrokeStyle = () => {
|
const applyStrokeStyle = () => {
|
||||||
|
|
@ -161,6 +170,17 @@ const StrokeCustomization = () => {
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Automatically apply stroke styles on state change
|
||||||
|
useEffect(() => {
|
||||||
|
applyStrokeStyle();
|
||||||
|
}, [
|
||||||
|
strokeWidth,
|
||||||
|
strokeColor,
|
||||||
|
gradientStrokeColors,
|
||||||
|
colorType,
|
||||||
|
gradientDirection,
|
||||||
|
]);
|
||||||
|
|
||||||
const revertStroke = () => {
|
const revertStroke = () => {
|
||||||
if (!activeObject) return;
|
if (!activeObject) return;
|
||||||
|
|
||||||
|
|
@ -209,21 +229,26 @@ const StrokeCustomization = () => {
|
||||||
min={0}
|
min={0}
|
||||||
max={50}
|
max={50}
|
||||||
value={strokeWidth}
|
value={strokeWidth}
|
||||||
onChange={(e) => handleStrokeWidthChange(Number(e.target.value))}
|
onChange={(e) =>
|
||||||
|
handleStrokeWidthChange(Number(e.target.value))
|
||||||
|
}
|
||||||
className="w-16"
|
className="w-16"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Tabs value={colorType} onValueChange={(value) => handleColorTypeChange(value)}>
|
<Tabs
|
||||||
|
value={colorType}
|
||||||
|
onValueChange={(value) => handleColorTypeChange(value)}
|
||||||
|
>
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<TabsTrigger value="color">
|
<TabsTrigger value="color">
|
||||||
<Paintbrush className="w-4 h-4 mr-2" />
|
<Paintbrush className="w-4 h-4 mr-2" />
|
||||||
Solid
|
Solid
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="gradient">
|
<TabsTrigger value="gradient" className="flex gap-2">
|
||||||
<Gradient className="w-4 h-4 mr-2" />
|
<div className="h-4 w-4 rounded bg-gradient-to-r from-purple-500 to-pink-500" />
|
||||||
Gradient
|
Gradient
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
@ -241,7 +266,10 @@ const StrokeCustomization = () => {
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 pointer-events-none"
|
className="absolute inset-0 pointer-events-none"
|
||||||
style={{ backgroundColor: strokeColor, borderRadius: '0.375rem' }}
|
style={{
|
||||||
|
backgroundColor: strokeColor,
|
||||||
|
borderRadius: "0.375rem",
|
||||||
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -256,7 +284,10 @@ const StrokeCustomization = () => {
|
||||||
<TabsContent value="gradient" className="space-y-4">
|
<TabsContent value="gradient" className="space-y-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="gradient-direction">Gradient Direction</Label>
|
<Label htmlFor="gradient-direction">Gradient Direction</Label>
|
||||||
<Select value={gradientDirection} onValueChange={setGradientDirection}>
|
<Select
|
||||||
|
value={gradientDirection}
|
||||||
|
onValueChange={setGradientDirection}
|
||||||
|
>
|
||||||
<SelectTrigger id="gradient-direction">
|
<SelectTrigger id="gradient-direction">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
@ -277,18 +308,21 @@ const StrokeCustomization = () => {
|
||||||
id="gradient-color-1"
|
id="gradient-color-1"
|
||||||
type="color"
|
type="color"
|
||||||
value={gradientStrokeColors.color1}
|
value={gradientStrokeColors.color1}
|
||||||
onChange={(e) => handleGradientColorChange('color1', e)}
|
onChange={(e) => handleGradientColorChange("color1", e)}
|
||||||
className="w-10 h-10 p-1 rounded-md cursor-pointer"
|
className="w-10 h-10 p-1 rounded-md cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 pointer-events-none"
|
className="absolute inset-0 pointer-events-none"
|
||||||
style={{ backgroundColor: gradientStrokeColors.color1, borderRadius: '0.375rem' }}
|
style={{
|
||||||
|
backgroundColor: gradientStrokeColors.color1,
|
||||||
|
borderRadius: "0.375rem",
|
||||||
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={gradientStrokeColors.color1}
|
value={gradientStrokeColors.color1}
|
||||||
onChange={(e) => handleGradientColorChange('color1', e)}
|
onChange={(e) => handleGradientColorChange("color1", e)}
|
||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -302,18 +336,21 @@ const StrokeCustomization = () => {
|
||||||
id="gradient-color-2"
|
id="gradient-color-2"
|
||||||
type="color"
|
type="color"
|
||||||
value={gradientStrokeColors.color2}
|
value={gradientStrokeColors.color2}
|
||||||
onChange={(e) => handleGradientColorChange('color2', e)}
|
onChange={(e) => handleGradientColorChange("color2", e)}
|
||||||
className="w-10 h-10 p-1 rounded-md cursor-pointer"
|
className="w-10 h-10 p-1 rounded-md cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 pointer-events-none"
|
className="absolute inset-0 pointer-events-none"
|
||||||
style={{ backgroundColor: gradientStrokeColors.color2, borderRadius: '0.375rem' }}
|
style={{
|
||||||
|
backgroundColor: gradientStrokeColors.color2,
|
||||||
|
borderRadius: "0.375rem",
|
||||||
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={gradientStrokeColors.color2}
|
value={gradientStrokeColors.color2}
|
||||||
onChange={(e) => handleGradientColorChange('color2', e)}
|
onChange={(e) => handleGradientColorChange("color2", e)}
|
||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -324,13 +361,19 @@ const StrokeCustomization = () => {
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label>Preview</Label>
|
<Label>Preview</Label>
|
||||||
<div className="border rounded-md p-2 flex items-center justify-center" style={{ height: '120px' }}>
|
<div
|
||||||
<div ref={previewRef} style={{ width: '80px', height: '80px' }}></div>
|
className="border rounded-md p-2 flex items-center justify-center"
|
||||||
|
style={{ height: "120px" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={previewRef}
|
||||||
|
style={{ width: "80px", height: "80px" }}
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<Button onClick={applyStrokeStyle}>
|
<Button onClick={applyStrokeStyle} className="bg-[#FF2B85]">
|
||||||
Apply Stroke
|
Apply Stroke
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={revertStroke} variant="outline">
|
<Button onClick={revertStroke} variant="outline">
|
||||||
|
|
@ -339,11 +382,11 @@ const StrokeCustomization = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-2">
|
<Card className=" shadow-none border-0">
|
||||||
<CollapsibleComponent text={"Stroke Control"}>
|
<CollapsibleComponent text={"Stroke Control"}>
|
||||||
{content()}
|
{content()}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
|
|
@ -352,16 +395,3 @@ const StrokeCustomization = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StrokeCustomization;
|
export default StrokeCustomization;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,81 @@
|
||||||
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
import { Slider } from '@/components/ui/slider';
|
Select,
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
SelectContent,
|
||||||
import { useContext, useEffect, useState } from 'react'
|
SelectGroup,
|
||||||
import { AlignLeft, AlignCenter, AlignRight, Bold, Italic, Underline, Strikethrough } from 'lucide-react';
|
SelectItem,
|
||||||
import CollapsibleComponent from './CollapsibleComponent';
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
AlignLeft,
|
||||||
|
AlignCenter,
|
||||||
|
AlignRight,
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Underline,
|
||||||
|
Strikethrough,
|
||||||
|
} from "lucide-react";
|
||||||
|
import CollapsibleComponent from "./CollapsibleComponent";
|
||||||
|
|
||||||
const fonts = [
|
const fonts = [
|
||||||
'Roboto', 'Open Sans', 'Lato', 'Montserrat', 'Raleway', 'Poppins', 'Merriweather',
|
"Roboto",
|
||||||
'Playfair Display', 'Nunito', 'Oswald', 'Source Sans Pro', 'Ubuntu', 'Noto Sans',
|
"Open Sans",
|
||||||
'Work Sans', 'Bebas Neue', 'Arimo', 'PT Sans', 'PT Serif', 'Titillium Web',
|
"Lato",
|
||||||
'Fira Sans', 'Karla', 'Josefin Sans', 'Cairo', 'Rubik', 'Mulish', 'IBM Plex Sans',
|
"Montserrat",
|
||||||
'Quicksand', 'Cabin', 'Heebo', 'Exo 2', 'Manrope', 'Jost', 'Anton', 'Asap',
|
"Raleway",
|
||||||
'Baloo 2', 'Barlow', 'Cantarell', 'Chivo', 'Inter', 'Dosis', 'Crimson Text',
|
"Poppins",
|
||||||
'Amatic SC', 'ABeeZee', 'Raleway Dots', 'Pacifico', 'Orbitron', 'Varela Round',
|
"Merriweather",
|
||||||
'Acme', 'Teko',
|
"Playfair Display",
|
||||||
|
"Nunito",
|
||||||
|
"Oswald",
|
||||||
|
"Source Sans Pro",
|
||||||
|
"Ubuntu",
|
||||||
|
"Noto Sans",
|
||||||
|
"Work Sans",
|
||||||
|
"Bebas Neue",
|
||||||
|
"Arimo",
|
||||||
|
"PT Sans",
|
||||||
|
"PT Serif",
|
||||||
|
"Titillium Web",
|
||||||
|
"Fira Sans",
|
||||||
|
"Karla",
|
||||||
|
"Josefin Sans",
|
||||||
|
"Cairo",
|
||||||
|
"Rubik",
|
||||||
|
"Mulish",
|
||||||
|
"IBM Plex Sans",
|
||||||
|
"Quicksand",
|
||||||
|
"Cabin",
|
||||||
|
"Heebo",
|
||||||
|
"Exo 2",
|
||||||
|
"Manrope",
|
||||||
|
"Jost",
|
||||||
|
"Anton",
|
||||||
|
"Asap",
|
||||||
|
"Baloo 2",
|
||||||
|
"Barlow",
|
||||||
|
"Cantarell",
|
||||||
|
"Chivo",
|
||||||
|
"Inter",
|
||||||
|
"Dosis",
|
||||||
|
"Crimson Text",
|
||||||
|
"Amatic SC",
|
||||||
|
"ABeeZee",
|
||||||
|
"Raleway Dots",
|
||||||
|
"Pacifico",
|
||||||
|
"Orbitron",
|
||||||
|
"Varela Round",
|
||||||
|
"Acme",
|
||||||
|
"Teko",
|
||||||
];
|
];
|
||||||
|
|
||||||
const TextCustomization = () => {
|
const TextCustomization = () => {
|
||||||
|
|
@ -27,40 +83,40 @@ const TextCustomization = () => {
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState("");
|
||||||
const [fontFamily, setFontFamily] = useState('Arial');
|
const [fontFamily, setFontFamily] = useState("Arial");
|
||||||
const [fontSize, setFontSize] = useState(20);
|
const [fontSize, setFontSize] = useState(20);
|
||||||
const [fontStyle, setFontStyle] = useState('normal');
|
const [fontStyle, setFontStyle] = useState("normal");
|
||||||
const [fontWeight, setFontWeight] = useState('normal');
|
const [fontWeight, setFontWeight] = useState("normal");
|
||||||
const [lineHeight, setLineHeight] = useState(1.16);
|
const [lineHeight, setLineHeight] = useState(1.16);
|
||||||
const [charSpacing, setCharSpacing] = useState(0);
|
const [charSpacing, setCharSpacing] = useState(0);
|
||||||
const [underline, setUnderline] = useState(false);
|
const [underline, setUnderline] = useState(false);
|
||||||
const [linethrough, setLinethrough] = useState(false);
|
const [linethrough, setLinethrough] = useState(false);
|
||||||
const [textAlign, setTextAlign] = useState('left');
|
const [textAlign, setTextAlign] = useState("left");
|
||||||
const [previewText, setPreviewText] = useState('');
|
const [previewText, setPreviewText] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeObject?.type === "i-text") {
|
if (activeObject?.type === "i-text") {
|
||||||
setText(activeObject?.text || '');
|
setText(activeObject?.text || "");
|
||||||
setFontFamily(activeObject?.fontFamily || 'Arial');
|
setFontFamily(activeObject?.fontFamily || "Arial");
|
||||||
setFontSize(activeObject?.fontSize || 20);
|
setFontSize(activeObject?.fontSize || 20);
|
||||||
setFontStyle(activeObject?.fontStyle || 'normal');
|
setFontStyle(activeObject?.fontStyle || "normal");
|
||||||
setFontWeight(activeObject?.fontWeight || 'normal');
|
setFontWeight(activeObject?.fontWeight || "normal");
|
||||||
setLineHeight(activeObject?.lineHeight || 1.16);
|
setLineHeight(activeObject?.lineHeight || 1.16);
|
||||||
setCharSpacing(activeObject?.charSpacing || 0);
|
setCharSpacing(activeObject?.charSpacing || 0);
|
||||||
setUnderline(activeObject?.underline || false);
|
setUnderline(activeObject?.underline || false);
|
||||||
setLinethrough(activeObject?.linethrough || false);
|
setLinethrough(activeObject?.linethrough || false);
|
||||||
setTextAlign(activeObject?.textAlign || 'left');
|
setTextAlign(activeObject?.textAlign || "left");
|
||||||
setPreviewText(activeObject?.text || '');
|
setPreviewText(activeObject?.text || "");
|
||||||
}
|
}
|
||||||
}, [activeObject])
|
}, [activeObject]);
|
||||||
|
|
||||||
const updateActiveObject = (properties) => {
|
const updateActiveObject = (properties) => {
|
||||||
if (activeObject?.type === "i-text") {
|
if (activeObject?.type === "i-text") {
|
||||||
activeObject.set(properties)
|
activeObject.set(properties);
|
||||||
canvas?.renderAll()
|
canvas?.renderAll();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const applyChanges = () => {
|
const applyChanges = () => {
|
||||||
updateActiveObject({
|
updateActiveObject({
|
||||||
|
|
@ -73,69 +129,69 @@ const TextCustomization = () => {
|
||||||
charSpacing,
|
charSpacing,
|
||||||
underline,
|
underline,
|
||||||
linethrough,
|
linethrough,
|
||||||
textAlign
|
textAlign,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleTextChange = (newText) => {
|
const handleTextChange = (newText) => {
|
||||||
setText(newText);
|
setText(newText);
|
||||||
setPreviewText(newText);
|
setPreviewText(newText);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleFontFamilyChange = (newFontFamily) => {
|
const handleFontFamilyChange = (newFontFamily) => {
|
||||||
setFontFamily(newFontFamily);
|
setFontFamily(newFontFamily);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleFontSizeChange = (newFontSize) => {
|
const handleFontSizeChange = (newFontSize) => {
|
||||||
setFontSize(newFontSize)
|
setFontSize(newFontSize);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleTextAlignChange = (newTextAlign) => {
|
const handleTextAlignChange = (newTextAlign) => {
|
||||||
setTextAlign(newTextAlign)
|
setTextAlign(newTextAlign);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleFontStyleChange = () => {
|
const handleFontStyleChange = () => {
|
||||||
const newFontStyle = fontStyle === 'normal' ? 'italic' : 'normal'
|
const newFontStyle = fontStyle === "normal" ? "italic" : "normal";
|
||||||
setFontStyle(newFontStyle)
|
setFontStyle(newFontStyle);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleFontWeightChange = () => {
|
const handleFontWeightChange = () => {
|
||||||
const newFontWeight = fontWeight === 'normal' ? 'bold' : 'normal'
|
const newFontWeight = fontWeight === "normal" ? "bold" : "normal";
|
||||||
setFontWeight(newFontWeight)
|
setFontWeight(newFontWeight);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleLineHeightChange = (newLineHeight) => {
|
const handleLineHeightChange = (newLineHeight) => {
|
||||||
setLineHeight(newLineHeight)
|
setLineHeight(newLineHeight);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleCharSpacingChange = (newCharSpacing) => {
|
const handleCharSpacingChange = (newCharSpacing) => {
|
||||||
setCharSpacing(newCharSpacing)
|
setCharSpacing(newCharSpacing);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleUnderlineChange = () => {
|
const handleUnderlineChange = () => {
|
||||||
const newUnderline = !underline
|
const newUnderline = !underline;
|
||||||
setUnderline(newUnderline)
|
setUnderline(newUnderline);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleLinethroughChange = () => {
|
const handleLinethroughChange = () => {
|
||||||
const newLinethrough = !linethrough
|
const newLinethrough = !linethrough;
|
||||||
setLinethrough(newLinethrough)
|
setLinethrough(newLinethrough);
|
||||||
}
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!(activeObject?.type === "i-text")) {
|
if (!(activeObject?.type === "i-text")) {
|
||||||
return (
|
return (
|
||||||
<div className='mt-2'>
|
<div className="mt-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<p className="text-center text-gray-500">Select a text object to customize</p>
|
<p className="text-center text-gray-500">
|
||||||
|
Select a text object to customize
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
else {
|
|
||||||
return (
|
return (
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-2">
|
||||||
<Tabs defaultValue="text" className="w-full">
|
<Tabs defaultValue="text" className="w-full">
|
||||||
|
|
@ -155,14 +211,21 @@ const TextCustomization = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label>Font Family</Label>
|
<Label>Font Family</Label>
|
||||||
<Select value={fontFamily} onValueChange={handleFontFamilyChange}>
|
<Select
|
||||||
|
value={fontFamily}
|
||||||
|
onValueChange={handleFontFamilyChange}
|
||||||
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a font" />
|
<SelectValue placeholder="Select a font" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="h-[250px]">
|
<SelectContent className="h-[250px]">
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{fonts.map((font) => (
|
{fonts.map((font) => (
|
||||||
<SelectItem style={{ fontFamily: font }} key={font} value={font}>
|
<SelectItem
|
||||||
|
style={{ fontFamily: font }}
|
||||||
|
key={font}
|
||||||
|
value={font}
|
||||||
|
>
|
||||||
{font}
|
{font}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -184,7 +247,9 @@ const TextCustomization = () => {
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={fontSize}
|
value={fontSize}
|
||||||
onChange={(e) => handleFontSizeChange(parseInt(e.target.value, 10) || 16)}
|
onChange={(e) =>
|
||||||
|
handleFontSizeChange(parseInt(e.target.value, 10) || 16)
|
||||||
|
}
|
||||||
className="w-16"
|
className="w-16"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -195,23 +260,23 @@ const TextCustomization = () => {
|
||||||
<Label>Text Alignment</Label>
|
<Label>Text Alignment</Label>
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
<Button
|
<Button
|
||||||
variant={textAlign === 'left' ? 'default' : 'outline'}
|
variant={textAlign === "left" ? "default" : "outline"}
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => handleTextAlignChange('left')}
|
onClick={() => handleTextAlignChange("left")}
|
||||||
>
|
>
|
||||||
<AlignLeft className="h-4 w-4" />
|
<AlignLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={textAlign === 'center' ? 'default' : 'outline'}
|
variant={textAlign === "center" ? "default" : "outline"}
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => handleTextAlignChange('center')}
|
onClick={() => handleTextAlignChange("center")}
|
||||||
>
|
>
|
||||||
<AlignCenter className="h-4 w-4" />
|
<AlignCenter className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={textAlign === 'right' ? 'default' : 'outline'}
|
variant={textAlign === "right" ? "default" : "outline"}
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => handleTextAlignChange('right')}
|
onClick={() => handleTextAlignChange("right")}
|
||||||
>
|
>
|
||||||
<AlignRight className="h-4 w-4" />
|
<AlignRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -221,28 +286,28 @@ const TextCustomization = () => {
|
||||||
<Label>Text Style</Label>
|
<Label>Text Style</Label>
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
<Button
|
<Button
|
||||||
variant={fontWeight === 'bold' ? 'default' : 'outline'}
|
variant={fontWeight === "bold" ? "default" : "outline"}
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleFontWeightChange}
|
onClick={handleFontWeightChange}
|
||||||
>
|
>
|
||||||
<Bold className="h-4 w-4" />
|
<Bold className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={fontStyle === 'italic' ? 'default' : 'outline'}
|
variant={fontStyle === "italic" ? "default" : "outline"}
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleFontStyleChange}
|
onClick={handleFontStyleChange}
|
||||||
>
|
>
|
||||||
<Italic className="h-4 w-4" />
|
<Italic className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={underline ? 'default' : 'outline'}
|
variant={underline ? "default" : "outline"}
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleUnderlineChange}
|
onClick={handleUnderlineChange}
|
||||||
>
|
>
|
||||||
<Underline className="h-4 w-4" />
|
<Underline className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={linethrough ? 'default' : 'outline'}
|
variant={linethrough ? "default" : "outline"}
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handleLinethroughChange}
|
onClick={handleLinethroughChange}
|
||||||
>
|
>
|
||||||
|
|
@ -273,9 +338,9 @@ const TextCustomization = () => {
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
)
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-2">
|
<Card className="p-2">
|
||||||
|
|
@ -283,32 +348,35 @@ const TextCustomization = () => {
|
||||||
{content()}
|
{content()}
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
{
|
{previewText && (
|
||||||
previewText &&
|
|
||||||
<div className="p-4 border rounded-md overflow-hidden">
|
<div className="p-4 border rounded-md overflow-hidden">
|
||||||
<p className="font-bold mb-2">Preview:</p>
|
<p className="font-bold mb-2">Preview:</p>
|
||||||
<p style={{
|
<p
|
||||||
|
style={{
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize: `${fontSize}px`,
|
fontSize: `${fontSize}px`,
|
||||||
fontStyle,
|
fontStyle,
|
||||||
fontWeight,
|
fontWeight,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
letterSpacing: `${charSpacing}px`,
|
letterSpacing: `${charSpacing}px`,
|
||||||
textDecoration: `${underline ? 'underline' : ''} ${linethrough ? 'line-through' : ''}`,
|
textDecoration: `${underline ? "underline" : ""} ${
|
||||||
textAlign
|
linethrough ? "line-through" : ""
|
||||||
}} className='truncate'>
|
}`,
|
||||||
|
textAlign,
|
||||||
|
}}
|
||||||
|
className="truncate"
|
||||||
|
>
|
||||||
{previewText}
|
{previewText}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<Button onClick={applyChanges} className="w-full">
|
<Button onClick={applyChanges} className="w-full">
|
||||||
Apply Changes
|
Apply Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default TextCustomization
|
|
||||||
|
|
||||||
|
export default TextCustomization;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,28 @@
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Text, Square, Upload, Shield, Image, Folder } from "lucide-react";
|
import {
|
||||||
|
Type,
|
||||||
|
Text,
|
||||||
|
Shapes,
|
||||||
|
FolderUp,
|
||||||
|
Shield,
|
||||||
|
Image,
|
||||||
|
FolderKanban,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
|
||||||
const sidebarItems = [
|
const sidebarItems = [
|
||||||
{ id: "design", icon: Text, label: "Design" },
|
{ id: "design", icon: Text, label: "Design" },
|
||||||
{ id: "text", icon: Text, label: "Text" },
|
{ id: "text", icon: Type, label: "Text" },
|
||||||
{ id: "shape", icon: Square, label: "Shape" },
|
{ id: "shape", icon: Shapes, label: "Shape" },
|
||||||
{ id: "upload", icon: Upload, label: "Upload" },
|
{ id: "upload", icon: FolderUp, label: "Upload" },
|
||||||
{ id: "icon", icon: Shield, label: "Icon" },
|
{ id: "icon", icon: Shield, label: "Icon" },
|
||||||
{ id: "image", icon: Image, label: "Image" },
|
{ id: "image", icon: Image, label: "Image" },
|
||||||
{ id: "project", icon: Folder, label: "Project" },
|
{ id: "project", icon: FolderKanban, label: "Project" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar({ selectedItem, onItemSelect }) {
|
export function Sidebar() {
|
||||||
|
const { selectedPanel, setSelectedPanel } = useContext(CanvasContext);
|
||||||
return (
|
return (
|
||||||
<div className="w-20 border-r h-screen bg-background flex flex-col items-center py-4 gap-6">
|
<div className="w-20 border-r h-screen bg-background flex flex-col items-center py-4 gap-6">
|
||||||
{sidebarItems.map((item) => {
|
{sidebarItems.map((item) => {
|
||||||
|
|
@ -19,10 +30,12 @@ export function Sidebar({ selectedItem, onItemSelect }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => onItemSelect(item.id)}
|
onClick={() => {
|
||||||
|
setSelectedPanel(item.id);
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-center gap-1 p-2 rounded-lg w-16 hover:bg-accent",
|
"flex flex-col items-center gap-1 p-2 rounded-lg w-16 hover:bg-accent",
|
||||||
selectedItem === item.id && "bg-accent"
|
selectedPanel === item.id && "bg-accent"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { Button } from "../ui/button";
|
|
||||||
import DesignPanel from "../Panel/DesignPanel";
|
|
||||||
// import TextPanel from "../Panel/TextPanel";
|
|
||||||
import ShapePanel from "../Panel/ShapePanel";
|
|
||||||
|
|
||||||
const panels = {
|
|
||||||
design: DesignPanel,
|
|
||||||
// text: TextPanel,
|
|
||||||
shape: ShapePanel,
|
|
||||||
// Add other panels as needed
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RightPanel({ activePanel }) {
|
|
||||||
const PanelComponent = panels[activePanel] || (() => null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed right-0 top-0 h-screen w-72 border-l bg-background p-4">
|
|
||||||
<div className="flex items-center justify-between border-b pb-4">
|
|
||||||
<h2 className="text-lg font-semibold capitalize">{activePanel}</h2>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<PanelComponent />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,147 @@
|
||||||
|
import { useEffect, useContext } from "react";
|
||||||
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||||
|
import OpenContext from "../Context/openContext/OpenContext";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import { Card, CardContent } from "../ui/card";
|
||||||
|
|
||||||
export function Canvas() {
|
export function Canvas() {
|
||||||
|
const {
|
||||||
|
setLeftPanelOpen,
|
||||||
|
setRightPanelOpen,
|
||||||
|
setOpenSetting,
|
||||||
|
setOpenObjectPanel,
|
||||||
|
rightPanelOpen,
|
||||||
|
} = useContext(OpenContext);
|
||||||
|
|
||||||
|
const {
|
||||||
|
canvasRef,
|
||||||
|
canvas,
|
||||||
|
setCanvas,
|
||||||
|
fabricCanvasRef,
|
||||||
|
canvasRatio,
|
||||||
|
setCanvasHeight,
|
||||||
|
setCanvasWidth,
|
||||||
|
setScreenWidth,
|
||||||
|
} = useContext(CanvasContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
import("fabric").then((fabricModule) => {
|
||||||
|
window.fabric = fabricModule.fabric;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getRatioValue = (ratio) => {
|
||||||
|
const [width, height] = ratio.split(":").map(Number);
|
||||||
|
return width / height;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateCanvasSize = () => {
|
||||||
|
if (canvasRef.current && canvas) {
|
||||||
|
// Update canvas dimensions
|
||||||
|
const newWidth = canvasRef?.current?.offsetWidth;
|
||||||
|
const newHeight = canvasRef?.current?.offsetHeight;
|
||||||
|
|
||||||
|
canvas.setWidth(newWidth);
|
||||||
|
canvas.setHeight(newHeight);
|
||||||
|
setCanvasWidth(newWidth);
|
||||||
|
setCanvasHeight(newHeight);
|
||||||
|
|
||||||
|
// Adjust the background image to fit the updated canvas size
|
||||||
|
const bgImage = canvas.backgroundImage;
|
||||||
|
if (bgImage) {
|
||||||
|
// Calculate scaling factors for width and height
|
||||||
|
const scaleX = newWidth / bgImage.width;
|
||||||
|
const scaleY = newHeight / bgImage.height;
|
||||||
|
|
||||||
|
// Use the larger scale to cover the entire canvas
|
||||||
|
const scale = Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
|
// Apply scale and position the image
|
||||||
|
bgImage.scaleX = scale;
|
||||||
|
bgImage.scaleY = scale;
|
||||||
|
bgImage.left = 0; // Align left
|
||||||
|
bgImage.top = 0; // Align top
|
||||||
|
|
||||||
|
// Update the background image
|
||||||
|
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
|
||||||
|
} else {
|
||||||
|
// Render the canvas if no background image
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setScreenWidth(document.getElementById("root").offsetWidth);
|
||||||
|
|
||||||
|
// Handle responsive behavior for panels
|
||||||
|
if (document.getElementById("root").offsetWidth <= 640) {
|
||||||
|
setLeftPanelOpen(false);
|
||||||
|
setRightPanelOpen(false);
|
||||||
|
}
|
||||||
|
if (document.getElementById("root").offsetWidth > 640) {
|
||||||
|
setOpenObjectPanel(false);
|
||||||
|
setOpenSetting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Initial setup
|
||||||
|
updateCanvasSize();
|
||||||
|
|
||||||
|
// Listen for window resize
|
||||||
|
window.addEventListener("resize", updateCanvasSize);
|
||||||
|
|
||||||
|
// Cleanup listener on unmount
|
||||||
|
return () => window.removeEventListener("resize", updateCanvasSize);
|
||||||
|
}, [
|
||||||
|
setCanvasWidth,
|
||||||
|
setCanvasHeight,
|
||||||
|
canvasRatio,
|
||||||
|
canvasRef,
|
||||||
|
canvas,
|
||||||
|
setLeftPanelOpen,
|
||||||
|
setOpenObjectPanel,
|
||||||
|
setRightPanelOpen,
|
||||||
|
rightPanelOpen,
|
||||||
|
setScreenWidth,
|
||||||
|
setOpenSetting,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.fabric) {
|
||||||
|
if (fabricCanvasRef?.current) {
|
||||||
|
fabricCanvasRef?.current.dispose();
|
||||||
|
}
|
||||||
|
// Set styles directly on the canvas element
|
||||||
|
const canvasElement = document.getElementById("fabric-canvas");
|
||||||
|
if (canvasElement) {
|
||||||
|
canvasElement.classList.add("fabric-canvas-container"); // Add the CSS class
|
||||||
|
}
|
||||||
|
|
||||||
|
fabricCanvasRef.current = new window.fabric.Canvas("fabric-canvas", {
|
||||||
|
width: canvasRef?.current?.offsetWidth,
|
||||||
|
height: canvasRef?.current?.offsetWidth,
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
});
|
||||||
|
|
||||||
|
setCanvas(fabricCanvasRef?.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 h-[88vh] bg-[#F5F0FF] p-8 flex flex-col mt-20">
|
<Card className="w-full max-w-3xl p-2 my-4 overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-track-background rounded-none flex-1 flex flex-col mt-24 mx-auto bg-white pl-5 pb-5 pt-5 border-0 shadow-none">
|
||||||
{/* Main Canvas Area */}
|
<CardContent className="p-0 space-y-2">
|
||||||
<div className="flex-1 bg-white rounded-3xl shadow-sm mb-4 flex items-center justify-center">
|
<AspectRatio
|
||||||
<div className="w-32 h-32 bg-muted rounded-lg" />
|
ratio={getRatioValue(canvasRatio)}
|
||||||
</div>
|
className="overflow-y-scroll shadow-red-200 overflow-x-hidden shadow-lg rounded-lg border-2 border-primary/10 transition-all duration-300 ease-in-out hover:shadow-xl scrollbar-hide"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={canvasRef}
|
||||||
|
className="w-full h-full flex items-center justify-center bg-white rounded-md shadow-lg"
|
||||||
|
id="canvas-ref"
|
||||||
|
>
|
||||||
|
<canvas id="fabric-canvas" />
|
||||||
</div>
|
</div>
|
||||||
|
</AspectRatio>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
src/components/Panel/CommonPanel.jsx
Normal file
65
src/components/Panel/CommonPanel.jsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import SelectObjectFromGroup from "../EachComponent/Customization/SelectObjectFromGroup";
|
||||||
|
import StrokeCustomization from "../EachComponent/Customization/StrokeCustomization";
|
||||||
|
import PositionCustomization from "../EachComponent/Customization/PositionCustomization";
|
||||||
|
import { Card } from "../ui/card";
|
||||||
|
import CollapsibleComponent from "../EachComponent/Customization/CollapsibleComponent";
|
||||||
|
import OpacityCustomization from "../EachComponent/Customization/OpacityCustomization";
|
||||||
|
import FlipCustomization from "../EachComponent/Customization/FlipCustomization";
|
||||||
|
import RotateCustomization from "../EachComponent/Customization/RotateCustomization";
|
||||||
|
import SkewCustomization from "../EachComponent/Customization/SkewCustomization";
|
||||||
|
import ScaleObjects from "../EachComponent/Customization/ScaleObjects";
|
||||||
|
import ShadowCustomization from "../EachComponent/Customization/ShadowCustomization";
|
||||||
|
import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape";
|
||||||
|
|
||||||
|
const CommonPanel = () => {
|
||||||
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
const activeObject = canvas?.getActiveObject();
|
||||||
|
const customClipPath = activeObject?.isClipPath;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<SelectObjectFromGroup />
|
||||||
|
|
||||||
|
{/* Apply stroke and stroke color */}
|
||||||
|
{!customClipPath && (
|
||||||
|
<>
|
||||||
|
<StrokeCustomization />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeObject?.type !== "group" && (
|
||||||
|
<>
|
||||||
|
<PositionCustomization />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Controls for opacity, flip, and rotation */}
|
||||||
|
<Card className="shadow-none border-0">
|
||||||
|
<CollapsibleComponent text={"Opacity, Flip, Rotate Control"}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<OpacityCustomization />
|
||||||
|
<FlipCustomization />
|
||||||
|
<RotateCustomization />
|
||||||
|
</div>
|
||||||
|
</CollapsibleComponent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Skew Customization */}
|
||||||
|
<SkewCustomization />
|
||||||
|
|
||||||
|
{/* Scale Objects */}
|
||||||
|
<ScaleObjects />
|
||||||
|
|
||||||
|
{/* Shadow Customization */}
|
||||||
|
<ShadowCustomization />
|
||||||
|
|
||||||
|
{/* Add image into shape */}
|
||||||
|
<AddImageIntoShape />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommonPanel;
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { Label } from "../ui/label";
|
|
||||||
import { Slider } from "../ui/slider";
|
|
||||||
|
|
||||||
export default function DesignPanel() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 pt-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Shadow Style</Label>
|
|
||||||
<div className="grid grid-cols-4 gap-2">
|
|
||||||
{Array.from({ length: 4 }).map((_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="aspect-square rounded-lg bg-muted hover:bg-muted/80"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Offset X</Label>
|
|
||||||
<Slider defaultValue={[0]} max={100} step={1} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Offset Y</Label>
|
|
||||||
<Slider defaultValue={[0]} max={100} step={1} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Blur</Label>
|
|
||||||
<Slider defaultValue={[0]} max={100} step={1} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Opacity</Label>
|
|
||||||
<Slider defaultValue={[100]} max={100} step={1} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +1,28 @@
|
||||||
import { Plus, Search, Upload } from "lucide-react";
|
import { useContext } from "react";
|
||||||
import { Button } from "../ui/button";
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import { Input } from "../ui/input";
|
import TextPanel from "./TextPanel";
|
||||||
|
|
||||||
export function EditorPanel({ type }) {
|
const EditorPanel = () => {
|
||||||
if (!type) return null;
|
const { selectedPanel } = useContext(CanvasContext);
|
||||||
|
|
||||||
const panelContent = {
|
const renderPanel = () => {
|
||||||
text: (
|
switch (selectedPanel) {
|
||||||
<>
|
case "text":
|
||||||
<h2 className="text-lg font-semibold mb-4">Text</h2>
|
return <TextPanel />;
|
||||||
<Button className="w-full">
|
default:
|
||||||
<Plus className="mr-2 h-4 w-4" /> Add Text
|
return;
|
||||||
</Button>
|
}
|
||||||
<div className="mt-4 space-y-2">
|
|
||||||
<Input placeholder="Add a Heading" />
|
|
||||||
<Input placeholder="Add a Subheading" />
|
|
||||||
<Input placeholder="Add body text" />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
shape: (
|
|
||||||
<>
|
|
||||||
<h2 className="text-lg font-semibold mb-4">Shape</h2>
|
|
||||||
<div className="relative mb-4">
|
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input placeholder="Search shapes" className="pl-8" />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{[...Array(6)].map((_, i) => (
|
|
||||||
<div key={i} className="aspect-square bg-muted rounded-md" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
image: (
|
|
||||||
<>
|
|
||||||
<h2 className="text-lg font-semibold mb-4">Image</h2>
|
|
||||||
<Button className="w-full">
|
|
||||||
<Upload className="mr-2 h-4 w-4" /> Upload Image
|
|
||||||
</Button>
|
|
||||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
|
||||||
{[...Array(4)].map((_, i) => (
|
|
||||||
<div key={i} className="aspect-square bg-muted rounded-md" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 bg-background border-r border-border p-4 overflow-y-auto">
|
<>
|
||||||
{panelContent[type]}
|
{selectedPanel !== "" && (
|
||||||
|
<div className="w-80 h-[calc(100vh-32px)] bg-background rounded-xl shadow-lg mx-4 my-4">
|
||||||
|
{renderPanel()}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default EditorPanel;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { Input } from "../ui/input";
|
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
|
||||||
|
|
||||||
export default function ShapePanel() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 pt-4">
|
|
||||||
<Input placeholder="Search with project name" />
|
|
||||||
<ScrollArea className="h-[calc(100vh-8rem)]">
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{Array.from({ length: 9 }).map((_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="aspect-square rounded-md bg-muted hover:bg-muted/80"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +1,92 @@
|
||||||
import { Button } from "../ui/Button";
|
import { Button } from "../ui/Button";
|
||||||
import { Input } from "../ui/Input";
|
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
|
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||||
|
import { fabric } from "fabric";
|
||||||
|
import CommonPanel from "./CommonPanel";
|
||||||
|
import TextCustomization from "../EachComponent/Customization/TextCustomization";
|
||||||
|
|
||||||
export default function TextPanel() {
|
export default function TextPanel() {
|
||||||
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
const activeObject = canvas?.getActiveObject();
|
||||||
|
const activeObjectType = activeObject?.type;
|
||||||
|
const hasClipPath = !!activeObject?.clipPath;
|
||||||
|
const customClipPath = activeObject?.isClipPath;
|
||||||
|
|
||||||
|
const addText = () => {
|
||||||
|
if (canvas) {
|
||||||
|
const text = new fabric.IText("Editable Text", {
|
||||||
|
left: 100,
|
||||||
|
top: 100,
|
||||||
|
fontFamily: "Poppins",
|
||||||
|
fontSize: 16,
|
||||||
|
});
|
||||||
|
// Add the text to the canvas and re-render
|
||||||
|
canvas.add(text);
|
||||||
|
// canvas.clipPath = text;
|
||||||
|
canvas.setActiveObject(text);
|
||||||
|
setActiveObject(text);
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 h-[calc(100vh-32px)] bg-background rounded-3xl shadow-lg mx-4 my-4">
|
<div>
|
||||||
<ScrollArea className="h-[calc(100vh-32px)] px-4 py-4">
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h2 className="text-lg font-semibold">Text</h2>
|
<h2 className="text-lg font-semibold">Text</h2>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button className="w-full bg-[#FF4D8D] hover:bg-[#FF3D7D] text-white rounded-2xl mb-6 h-12 text-base font-medium">
|
<ScrollArea className="h-[calc(100vh-115px)] px-4 py-4">
|
||||||
|
<Button
|
||||||
|
className="w-full bg-[#FF2B85] hover:bg-[#FF2B85] text-white rounded-[10px] mb-6 h-12 font-medium text-xl"
|
||||||
|
onClick={() => {
|
||||||
|
addText();
|
||||||
|
}}
|
||||||
|
>
|
||||||
Add Text
|
Add Text
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.5 18.3333H12.5C16.6667 18.3333 18.3333 16.6666 18.3333 12.5V7.49996C18.3333 3.33329 16.6667 1.66663 12.5 1.66663H7.5C3.33333 1.66663 1.66667 3.33329 1.66667 7.49996V12.5C1.66667 16.6666 3.33333 18.3333 7.5 18.3333Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.83333 7.40837C8.45833 6.10004 11.5417 6.10004 14.1667 7.40837"
|
||||||
|
stroke="white"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10 13.5833V6.60828"
|
||||||
|
stroke="white"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
{activeObject ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<CommonPanel />
|
||||||
<p className="text-sm text-muted-foreground mb-2">
|
<TextCustomization />
|
||||||
Default text style
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm font-semibold text-center">
|
||||||
|
No active object found
|
||||||
</p>
|
</p>
|
||||||
<Input
|
)}
|
||||||
placeholder="Add a Heading H1"
|
|
||||||
className="mb-2 rounded-2xl h-12"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder="Add a Subheading H2"
|
|
||||||
className="mb-2 rounded-2xl h-12"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder="Add Some body text"
|
|
||||||
className="rounded-2xl h-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<p className="text-sm text-muted-foreground">Text Design</p>
|
|
||||||
<Button variant="link" size="sm">
|
|
||||||
See all
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{[...Array(6)].map((_, i) => (
|
|
||||||
<div key={i} className="aspect-square bg-muted rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue