diff --git a/app/(tools)/background-snippets/page.tsx b/app/(tools)/background-snippets/page.tsx index 8acb861..077a53c 100644 --- a/app/(tools)/background-snippets/page.tsx +++ b/app/(tools)/background-snippets/page.tsx @@ -2,6 +2,7 @@ import BackgroundSnippetsGenerator from "@/components/view/background-snippets"; import { siteConfig } from "@/lib/utils"; import type { Metadata } from "next"; import React from "react"; +import { ToolPlaygroundShell } from "@/components/common/tool-playground-shell"; export const metadata: Metadata = { title: "Background Snippets Generator", description: @@ -56,11 +57,18 @@ export const metadata: Metadata = { }, }; function page() { - return ( - <> - - - ); + return ( + + + + ); } -export default page; +export default page; \ No newline at end of file diff --git a/app/(tools)/clip-paths/page.tsx b/app/(tools)/clip-paths/page.tsx index a8f080b..99a8df1 100644 --- a/app/(tools)/clip-paths/page.tsx +++ b/app/(tools)/clip-paths/page.tsx @@ -51,11 +51,7 @@ export const metadata: Metadata = { }; function page() { - return ( - <> - - - ); + return ; } export default page; diff --git a/app/(tools)/color-lab/page.tsx b/app/(tools)/color-lab/page.tsx index 982627a..0a7e766 100644 --- a/app/(tools)/color-lab/page.tsx +++ b/app/(tools)/color-lab/page.tsx @@ -3,6 +3,7 @@ import ColorConverter from "@/components/view/colors"; import { siteConfig } from "@/lib/utils"; import type { Metadata } from "next"; import React, { Suspense } from "react"; +import { ToolPlaygroundShell } from "@/components/common/tool-playground-shell"; export const metadata: Metadata = { title: "Color Lab – Generate Color Palettes, Convert Codes & Build Shadcn Themes", @@ -85,12 +86,17 @@ const PageLoading = () => { }; function page() { return ( - <> - }> - - - + + + ); } -export default page; +export default page; \ No newline at end of file diff --git a/app/(tools)/layout.tsx b/app/(tools)/layout.tsx index a96df1a..97e13ae 100644 --- a/app/(tools)/layout.tsx +++ b/app/(tools)/layout.tsx @@ -1,14 +1,29 @@ +"use client"; + import ToolsHeader from "@/components/common/tools-header"; +import { usePathname } from "next/navigation"; import type React from "react"; function Toolslayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const isFullPlayground = + pathname === "/svg-line-draw" || + pathname === "/shadows" || + pathname === "/clip-paths"; + return ( - <> - -
+
+ {!isFullPlayground && } +
{children}
- +
); } diff --git a/app/(tools)/mesh-gradients/page.tsx b/app/(tools)/mesh-gradients/page.tsx index 2e76fa3..34f9496 100644 --- a/app/(tools)/mesh-gradients/page.tsx +++ b/app/(tools)/mesh-gradients/page.tsx @@ -2,6 +2,7 @@ import { ShaderGradientGenerator } from "@/components/view/mesh-gradient"; import { siteConfig } from "@/lib/utils"; import type { Metadata } from "next"; import React from "react"; +import { ToolPlaygroundShell } from "@/components/common/tool-playground-shell"; export const metadata: Metadata = { title: "Mesh-Gradient Generator", description: @@ -52,10 +53,17 @@ export const metadata: Metadata = { function page() { return ( - <> + - + ); } -export default page; +export default page; \ No newline at end of file diff --git a/app/(tools)/shadows/page.tsx b/app/(tools)/shadows/page.tsx index 593a017..96bb07a 100644 --- a/app/(tools)/shadows/page.tsx +++ b/app/(tools)/shadows/page.tsx @@ -2,6 +2,7 @@ import ShadowGenerator from "@/components/view/shadow"; import { siteConfig } from "@/lib/utils"; import type { Metadata } from "next"; import React from "react"; + export const metadata: Metadata = { title: "Shadows Generator", description: @@ -52,11 +53,7 @@ export const metadata: Metadata = { }; function page() { - return ( - <> - - - ); + return ; } export default page; diff --git a/app/(tools)/svg-line-draw/page.tsx b/app/(tools)/svg-line-draw/page.tsx index 1727e1c..cbd44ce 100644 --- a/app/(tools)/svg-line-draw/page.tsx +++ b/app/(tools)/svg-line-draw/page.tsx @@ -2,6 +2,7 @@ import SVGLineDrawGenerator from "@/components/view/svg-line-draw"; import { siteConfig } from "@/lib/utils"; import type { Metadata } from "next"; import React, { Suspense } from "react"; + export const metadata: Metadata = { title: "SVG Line Draw – Sketch & Animate Hand-Drawn Lines for the Web", description: @@ -38,7 +39,7 @@ export const metadata: Metadata = { siteName: siteConfig.name, images: [ { - url: siteConfig.lineDrawOgImage, // Replace with relevant OG image + url: siteConfig.lineDrawOgImage, width: 1200, height: 630, alt: `SVG Line Draw by ${siteConfig.name}`, @@ -54,6 +55,7 @@ export const metadata: Metadata = { creator: "@naymur_dev", }, }; + const PageLoading = () => { return ( <> @@ -69,13 +71,12 @@ const PageLoading = () => { ); }; + function page() { return ( - <> - }> - - - + }> + + ); } diff --git a/components/common/tool-playground-shell.tsx b/components/common/tool-playground-shell.tsx new file mode 100644 index 0000000..7bab65b --- /dev/null +++ b/components/common/tool-playground-shell.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { cn } from "@/lib/utils"; +import { Copy, Download, EllipsisVertical, RotateCcw } from "lucide-react"; +import { useState } from "react"; +import toast from "react-hot-toast"; + +interface ToolPlaygroundShellProps { + title: string; + description: string; + examples: string[]; + docs: string; + exportLabel: string; + exportCode: string; + children: React.ReactNode; +} + +export function ToolPlaygroundShell({ + title, + description, + examples, + docs, + exportLabel, + exportCode, + children, +}: ToolPlaygroundShellProps) { + const [topTab, setTopTab] = useState("playground"); + const [drawerOpen, setDrawerOpen] = useState(false); + const [filter, setFilter] = useState(""); + + const copyCode = async () => { + await navigator.clipboard.writeText(exportCode); + toast.success("Copied"); + }; + + return ( +
+
+
+

{title}

+

{description}

+
+ + + + Playground + Examples + Docs + + + +
+ + + +
+ + + + + + + setDrawerOpen(true)}> + Export code + + Copy + location.reload()}>Reset + + +
+ +
+ + +
+
+
{children}
+
+
+
+ +
+
+

Export

+ +
+
+

{exportLabel}

+
{exportCode}
+
+ + +
+
+
+
+ ); +} diff --git a/components/view/clip-path/index.tsx b/components/view/clip-path/index.tsx index 195e302..4fa2da7 100644 --- a/components/view/clip-path/index.tsx +++ b/components/view/clip-path/index.tsx @@ -6,14 +6,36 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import CopyToClipboard from "@/components/ui/copy-to-clipboard"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Switch } from "@/components/ui/switch"; -import { Tabs, TabsContent, TabsList } from "@/components/ui/tabs"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; import { useMediaQuery } from "@/components/ui/use-media-query"; import { cn } from "@/lib/utils"; import { parseSvgPath, pointsToSvgPath } from "@/lib/utils"; import { useClipPathStore } from "@/store/clipPath-storage"; -import { TabsTrigger } from "@radix-ui/react-tabs"; -import { ChevronsDown } from "lucide-react"; +import { + Bookmark, + Layers, + Moon, + PanelsTopLeft, + Settings2, + SidebarClose, + SidebarOpen, + Sun, +} from "lucide-react"; +import { useTheme } from "next-themes"; +import { usePathname, useRouter } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { CodeOutput } from "./code-output"; import { CustomShapeForm } from "./custom-shape-form"; @@ -22,513 +44,444 @@ import { EditControls } from "./edit-controls"; import { ImageSelector } from "./image-selector"; import { ShapeGrid } from "./shape-grid"; import { ShapePreview } from "./shape-preview"; +import ThemeSwitch from "@/components/theme-switcher"; // Sample images for preview const SAMPLE_IMAGES = [ - "https://images.unsplash.com/photo-1635776062127-d379bfcba9f8?q=80&w=1932&auto=format&fit=crop", - "https://images.unsplash.com/photo-1624115773145-9b77fe912897?q=80&w=2070&auto=format&fit=crop", - "https://images.unsplash.com/photo-1600619030925-569b3b964418?q=80&w=2127&auto=format&fit=crop", + "https://images.unsplash.com/photo-1635776062127-d379bfcba9f8?q=80&w=1932&auto=format&fit=crop", + "https://images.unsplash.com/photo-1624115773145-9b77fe912897?q=80&w=2070&auto=format&fit=crop", + "https://images.unsplash.com/photo-1600619030925-569b3b964418?q=80&w=2127&auto=format&fit=crop", ]; export default function ClipPathGenerator() { - const isTab = useMediaQuery("(max-width:1024px)"); - const isMobile = useMediaQuery("(max-width:768px)"); - - const { - selectedShapeId, - setSelectedShapeId, - customPath, - customName, - addCustomShape, - addEditedShape, - deleteShape, - setCustomPath, - setCustomName, - customShapes, - editedShapes, - } = useClipPathStore(); - - const [viewAll, setViewAll] = useState(false); - const [selectedImage, setSelectedImage] = useState(SAMPLE_IMAGES[0]); - const [uploadedImages, setUploadedImages] = useState([]); - const [editClipPathId, _setEditClipPathId] = useState("edit-clip-path"); - const [editMode, setEditMode] = useState(false); - const [controlPoints, setControlPoints] = useState< - { x: number; y: number; command: string; isControl: boolean }[] - >([]); - const [_selectedPoint, setSelectedPoint] = useState(null); - const [zoomLevel, setZoomLevel] = useState(0.8); - const [previewOffset, setPreviewOffset] = useState({ x: 0, y: 0 }); - const [currentEditPath, setCurrentEditPath] = useState(""); - const [editingShapeKey, setEditingShapeKey] = useState(null); - const [controlPointsHistory, setControlPointsHistory] = useState< - { x: number; y: number; command: string; isControl: boolean }[][] - >([]); - const [historyIndex, setHistoryIndex] = useState(-1); - const [draggedPointIndex, setDraggedPointIndex] = useState(-1); - - const editorRef = useRef(null); - - const allShapes = [...INITIAL_CLIP_PATHS, ...editedShapes, ...customShapes]; - const selectedShape = - allShapes.find((shape) => shape.id === selectedShapeId) || - INITIAL_CLIP_PATHS[0]; - - useEffect(() => { - if (INITIAL_CLIP_PATHS.length > 0 && !selectedShapeId) { - setSelectedShapeId(INITIAL_CLIP_PATHS[0].id); - } - }, [selectedShapeId, setSelectedShapeId]); - - useEffect(() => { - if (controlPoints.length > 0) { - const path = pointsToSvgPath(controlPoints); - setCurrentEditPath(path); - } - }, [controlPoints]); - - const handleImageUpload = (e: React.ChangeEvent) => { - const files = e.target.files; - if (files) { - const newUploadedImages: string[] = []; // Ensure this is initialized outside the loop - - for (const file of Array.from(files)) { - const reader = new FileReader(); - reader.onload = (event) => { - const uploadedImage = event.target?.result as string; - newUploadedImages.push(uploadedImage); - - setUploadedImages((prev) => [...prev, uploadedImage]); - setSelectedImage(uploadedImage); - }; - reader.readAsDataURL(file); - } - } - }; - - const enterEditMode = (shapeId?: string) => { - const id = shapeId || selectedShapeId; - if (!id) return; - - setEditingShapeKey(id); - - const shape = allShapes.find((s) => s.id === id); - if (!shape) return; - - const pathData = shape.path; - - const points = parseSvgPath(pathData); - - setSelectedShapeId(id); - setControlPoints(points); - setCurrentEditPath(pathData); - setEditMode(true); - - setControlPointsHistory([points]); - setHistoryIndex(0); - }; - - const exitEditMode = (save = true) => { - if (save && controlPoints.length > 0 && editingShapeKey) { - const newPath = pointsToSvgPath(controlPoints); - const shape = allShapes.find((s) => s.id === editingShapeKey); - const className = shape?.className || ""; - addEditedShape(editingShapeKey, newPath, className); - } - - setEditMode(false); - setSelectedPoint(null); - setEditingShapeKey(null); - }; - - const startDrag = (index: number) => { - setDraggedPointIndex(index); - }; - - const handleMouseMove = (e: React.MouseEvent) => { - if (draggedPointIndex === -1 || !editorRef.current) return; - - const svg = editorRef.current; - const ctm = svg.getScreenCTM(); - if (!ctm) return; - - const newX = (e.clientX - ctm.e) / ctm.a; - const newY = (e.clientY - ctm.f) / ctm.d; - - if (Number.isNaN(newX) || Number.isNaN(newY)) return; - - setControlPoints((prev) => { - const newPoints = [...prev]; - if (newPoints[draggedPointIndex]) { - newPoints[draggedPointIndex] = { - ...newPoints[draggedPointIndex], - x: newX, - y: newY, - }; - } - return newPoints; - }); - }; - - const endDrag = () => { - if (draggedPointIndex !== -1) { - setControlPointsHistory((prev) => { - const newHistory = prev.slice(0, historyIndex + 1); - return [...newHistory, [...controlPoints]]; - }); - setHistoryIndex((prev) => prev + 1); - } - setDraggedPointIndex(-1); - }; - - const handleUndo = useCallback(() => { - if (historyIndex > 0) { - const newIndex = historyIndex - 1; - setHistoryIndex(newIndex); - setControlPoints(controlPointsHistory[newIndex]); - } - }, [historyIndex, controlPointsHistory]); - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "+" || e.key === "=") { - setZoomLevel((prev) => Math.min(5, prev + 0.25)); - } else if (e.key === "-" || e.key === "_") { - setZoomLevel((prev) => Math.max(0.25, prev - 0.25)); - } else if (e.key === "0") { - setZoomLevel(0.8); - setPreviewOffset({ x: 0, y: 0 }); - } else if (e.key === "z" && (e.ctrlKey || e.metaKey) && editMode) { - e.preventDefault(); - handleUndo(); // safely call the memoized function - } - }; - - // Handle mouse wheel for zoom - const handleWheel = (e: WheelEvent) => { - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - - const delta = e.deltaY > 0 ? -0.1 : 0.1; - setZoomLevel((prev) => Math.max(0.25, Math.min(5, prev + delta))); - } - }; - - window.addEventListener("keydown", handleKeyDown); - document.addEventListener("wheel", handleWheel, { passive: false }); - - return () => { - window.removeEventListener("keydown", handleKeyDown); - document.removeEventListener("wheel", handleWheel); - }; - }, [editMode, handleUndo]); - if (allShapes.length === 0 || !selectedShape) { - return ( -
- Loading shapes... -
- ); - } - - return ( - <> - - -
- {!viewAll && ( -
- )} - - {(viewAll ? INITIAL_CLIP_PATHS : INITIAL_CLIP_PATHS.slice(0, 10)).map( - (shape) => ( -
setSelectedShapeId(shape.id)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - setSelectedShapeId(shape.id); - } - }} - > - -
- ), - )} -
- {isMobile && ( -

- Please use a desktop/laptop to view the Editor. -

- )} - -
- {/* Left column: Controls */} - {!isMobile && ( - - - - - - Shapes - - - Edited - - - Custom - - - - - - - - - {!editMode && ( - - )} - - {editMode && ( - exitEditMode(true)} - onCancel={() => exitEditMode(false)} - /> - )} - - - - {editedShapes.length > 0 ? ( - <> - - - - - {editMode && ( - exitEditMode(true)} - onCancel={() => exitEditMode(false)} - /> - )} - - ) : ( -

No Edited Shapes Found

- )} -
- - - -
- - addCustomShape(customPath, customName) - } - disabled={editMode} - /> -
- - {/* Custom shapes list */} - {customShapes.length > 0 && ( - <> -

- Custom Shapes -

- - - - {editMode && ( - exitEditMode(true)} - onCancel={() => exitEditMode(false)} - /> - )} - - )} -
-
-
-
-
- )} - - {/* Middle column: Preview */} - - - -
- - - - - {zoomLevel > 1 && ( -

- Click and drag to pan the zoomed view -

- )} -

- Use mouse wheel + Ctrl to zoom in/out, or press + and - keys -

-
- {isTab && ( - - )} -
-
-
- - {/* Right column: Code output */} - {!isTab && ( - - - - - - - - )} -
- - ); + const pathname = usePathname(); + const router = useRouter(); + const isTab = useMediaQuery("(max-width:1024px)"); + const isMobile = useMediaQuery("(max-width:768px)"); + + const { + selectedShapeId, + setSelectedShapeId, + customPath, + customName, + addCustomShape, + addEditedShape, + deleteShape, + setCustomPath, + setCustomName, + customShapes, + editedShapes, + } = useClipPathStore(); + + const [selectedImage, setSelectedImage] = useState(SAMPLE_IMAGES[0]); + const [uploadedImages, setUploadedImages] = useState([]); + const [editClipPathId, _setEditClipPathId] = useState("edit-clip-path"); + const [editMode, setEditMode] = useState(false); + const [controlPoints, setControlPoints] = useState< + { x: number; y: number; command: string; isControl: boolean }[] + >([]); + const [_selectedPoint, setSelectedPoint] = useState(null); + const [zoomLevel, setZoomLevel] = useState(0.8); + const [previewOffset, setPreviewOffset] = useState({ x: 0, y: 0 }); + const [currentEditPath, setCurrentEditPath] = useState(""); + const [editingShapeKey, setEditingShapeKey] = useState(null); + const [controlPointsHistory, setControlPointsHistory] = useState< + { x: number; y: number; command: string; isControl: boolean }[][] + >([]); + const [historyIndex, setHistoryIndex] = useState(-1); + const [draggedPointIndex, setDraggedPointIndex] = useState(-1); + const [activeSidebarTab, setActiveSidebarTab] = useState< + "presets" | "settings" | "edited" | "saved" + >("presets"); + const [isSidebarExpanded, setIsSidebarExpanded] = useState(true); + + const editorRef = useRef(null); + + const allShapes = [...INITIAL_CLIP_PATHS, ...editedShapes, ...customShapes]; + const selectedShape = + allShapes.find((shape) => shape.id === selectedShapeId) || + INITIAL_CLIP_PATHS[0]; + + useEffect(() => { + if (INITIAL_CLIP_PATHS.length > 0 && !selectedShapeId) { + setSelectedShapeId(INITIAL_CLIP_PATHS[0].id); + } + }, [selectedShapeId, setSelectedShapeId]); + + useEffect(() => { + if (controlPoints.length > 0) { + const path = pointsToSvgPath(controlPoints); + setCurrentEditPath(path); + } + }, [controlPoints]); + + const handleImageUpload = (e: React.ChangeEvent) => { + const files = e.target.files; + if (files) { + const newUploadedImages: string[] = []; // Ensure this is initialized outside the loop + + for (const file of Array.from(files)) { + const reader = new FileReader(); + reader.onload = (event) => { + const uploadedImage = event.target?.result as string; + newUploadedImages.push(uploadedImage); + + setUploadedImages((prev) => [...prev, uploadedImage]); + setSelectedImage(uploadedImage); + }; + reader.readAsDataURL(file); + } + } + }; + + const enterEditMode = (shapeId?: string) => { + const id = shapeId || selectedShapeId; + if (!id) return; + + setEditingShapeKey(id); + + const shape = allShapes.find((s) => s.id === id); + if (!shape) return; + + const pathData = shape.path; + + const points = parseSvgPath(pathData); + + setSelectedShapeId(id); + setControlPoints(points); + setCurrentEditPath(pathData); + setEditMode(true); + + setControlPointsHistory([points]); + setHistoryIndex(0); + }; + + const exitEditMode = (save = true) => { + if (save && controlPoints.length > 0 && editingShapeKey) { + const newPath = pointsToSvgPath(controlPoints); + const shape = allShapes.find((s) => s.id === editingShapeKey); + const className = shape?.className || ""; + addEditedShape(editingShapeKey, newPath, className); + } + + setEditMode(false); + setSelectedPoint(null); + setEditingShapeKey(null); + }; + + const startDrag = (index: number) => { + setDraggedPointIndex(index); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (draggedPointIndex === -1 || !editorRef.current) return; + + const svg = editorRef.current; + const ctm = svg.getScreenCTM(); + if (!ctm) return; + + const newX = (e.clientX - ctm.e) / ctm.a; + const newY = (e.clientY - ctm.f) / ctm.d; + + if (Number.isNaN(newX) || Number.isNaN(newY)) return; + + setControlPoints((prev) => { + const newPoints = [...prev]; + if (newPoints[draggedPointIndex]) { + newPoints[draggedPointIndex] = { + ...newPoints[draggedPointIndex], + x: newX, + y: newY, + }; + } + return newPoints; + }); + }; + + const endDrag = () => { + if (draggedPointIndex !== -1) { + setControlPointsHistory((prev) => { + const newHistory = prev.slice(0, historyIndex + 1); + return [...newHistory, [...controlPoints]]; + }); + setHistoryIndex((prev) => prev + 1); + } + setDraggedPointIndex(-1); + }; + + const handleUndo = useCallback(() => { + if (historyIndex > 0) { + const newIndex = historyIndex - 1; + setHistoryIndex(newIndex); + setControlPoints(controlPointsHistory[newIndex]); + } + }, [historyIndex, controlPointsHistory]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "+" || e.key === "=") { + setZoomLevel((prev) => Math.min(5, prev + 0.25)); + } else if (e.key === "-" || e.key === "_") { + setZoomLevel((prev) => Math.max(0.25, prev - 0.25)); + } else if (e.key === "0") { + setZoomLevel(0.8); + setPreviewOffset({ x: 0, y: 0 }); + } else if (e.key === "z" && (e.ctrlKey || e.metaKey) && editMode) { + e.preventDefault(); + handleUndo(); // safely call the memoized function + } + }; + + // Handle mouse wheel for zoom + const handleWheel = (e: WheelEvent) => { + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + + const delta = e.deltaY > 0 ? -0.1 : 0.1; + setZoomLevel((prev) => Math.max(0.25, Math.min(5, prev + delta))); + } + }; + + window.addEventListener("keydown", handleKeyDown); + document.addEventListener("wheel", handleWheel, { passive: false }); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("wheel", handleWheel); + }; + }, [editMode, handleUndo]); + if (allShapes.length === 0 || !selectedShape) { + return ( +
+ Loading shapes... +
+ ); + } + + return ( + <> + {isMobile && ( +

+ Please use a desktop/laptop to view the Editor. +

+ )} + +
+ {!isMobile && ( +
+
+ {[ + { key: "presets", label: "Presets", icon: PanelsTopLeft }, + { key: "settings", label: "Settings", icon: Settings2 }, + { key: "edited", label: "Edited", icon: Layers }, + { key: "saved", label: "Saved", icon: Bookmark }, + ].map((item) => ( + + ))} +
+
+ + + +
+
+ )} + + {!isTab && isSidebarExpanded && ( +
+ {activeSidebarTab === "presets" && ( +
+ + + + {!editMode ? ( + + ) : ( + exitEditMode(true)} + onCancel={() => exitEditMode(false)} + /> + )} +
+ )} + + {activeSidebarTab === "settings" && ( + +
+ + + addCustomShape(customPath, customName) + } + disabled={editMode} + /> +
+
+ )} + + {activeSidebarTab === "edited" && ( + + {editedShapes.length > 0 ? ( + + ) : ( +

+ No Edited Shapes Found +

+ )} +
+ )} + + {activeSidebarTab === "saved" && ( + + {customShapes.length > 0 ? ( + + ) : ( +

+ No Custom/Saved Shapes Found +

+ )} +
+ )} +
+ )} + +
+
+ +
+ + +
+ + + {zoomLevel > 1 && ( +

+ Click and drag to pan the zoomed view +

+ )} +

+ Use mouse wheel + Ctrl to zoom in/out, or press + and - keys +

+
+
+
+
+
+ + ); } diff --git a/components/view/clip-path/shape-preview.tsx b/components/view/clip-path/shape-preview.tsx index 0bb9c8a..684e228 100644 --- a/components/view/clip-path/shape-preview.tsx +++ b/components/view/clip-path/shape-preview.tsx @@ -10,286 +10,283 @@ import { useRef, useState } from "react"; import type { ClipPathShape } from "./data"; interface ShapePreviewProps { - selectedShape: ClipPathShape; - selectedImage: string; - editMode: boolean; - editClipPathId: string; - currentEditPath: string; - controlPoints: Array<{ - x: number; - y: number; - command: string; - isControl: boolean; - }>; - zoomLevel: number; - setZoomLevel: (value: number) => void; - previewOffset: { x: number; y: number }; - setPreviewOffset: (offset: { x: number; y: number }) => void; - onMouseMove?: (e: React.MouseEvent) => void; - onMouseUp?: () => void; - onMouseLeave?: () => void; - startDrag?: (index: number) => void; - editorRef?: React.RefObject; + selectedShape: ClipPathShape; + selectedImage: string; + editMode: boolean; + editClipPathId: string; + currentEditPath: string; + controlPoints: Array<{ + x: number; + y: number; + command: string; + isControl: boolean; + }>; + zoomLevel: number; + className?: string; + setZoomLevel: (value: number) => void; + previewOffset: { x: number; y: number }; + setPreviewOffset: (offset: { x: number; y: number }) => void; + onMouseMove?: (e: React.MouseEvent) => void; + onMouseUp?: () => void; + onMouseLeave?: () => void; + startDrag?: (index: number) => void; + editorRef?: React.RefObject; } export function ShapePreview({ - selectedShape, - selectedImage, - editMode, - editClipPathId, - currentEditPath, - controlPoints, - zoomLevel, - setZoomLevel, - previewOffset, - setPreviewOffset, - onMouseMove, - onMouseUp, - onMouseLeave, - startDrag, - editorRef, + selectedShape, + selectedImage, + editMode, + className, + editClipPathId, + currentEditPath, + controlPoints, + zoomLevel, + setZoomLevel, + previewOffset, + setPreviewOffset, + onMouseMove, + onMouseUp, + onMouseLeave, + startDrag, + editorRef, }: ShapePreviewProps) { - const [isDraggingPreview, setIsDraggingPreview] = useState(false); - const [startDragPos, setStartDragPos] = useState({ x: 0, y: 0 }); + const [isDraggingPreview, setIsDraggingPreview] = useState(false); + const [startDragPos, setStartDragPos] = useState({ x: 0, y: 0 }); - const svgRef = useRef(null); - const previewRef = useRef(null); + const svgRef = useRef(null); + const previewRef = useRef(null); - const resetZoom = () => { - setZoomLevel(0.8); - setPreviewOffset({ x: 0, y: 0 }); - }; + const resetZoom = () => { + setZoomLevel(0.8); + setPreviewOffset({ x: 0, y: 0 }); + }; - const getViewBox = () => { - const size = 1 / zoomLevel; - const offset = (1 - size) / 2; - return `${offset} ${offset} ${size} ${size}`; - }; + const getViewBox = () => { + const size = 1 / zoomLevel; + const offset = (1 - size) / 2; + return `${offset} ${offset} ${size} ${size}`; + }; - const startPreviewDrag = (e: React.MouseEvent) => { - if (zoomLevel <= 1) return; - setIsDraggingPreview(true); - setStartDragPos({ x: e.clientX, y: e.clientY }); - }; + const startPreviewDrag = (e: React.MouseEvent) => { + if (zoomLevel <= 1) return; + setIsDraggingPreview(true); + setStartDragPos({ x: e.clientX, y: e.clientY }); + }; - const handlePreviewMouseMove = (e: React.MouseEvent) => { - if (!isDraggingPreview || zoomLevel <= 1 || !previewRef.current) return; + const handlePreviewMouseMove = (e: React.MouseEvent) => { + if (!isDraggingPreview || zoomLevel <= 1 || !previewRef.current) return; - const deltaX = - (((e.clientX - startDragPos.x) / previewRef.current.clientWidth) * 100) / - zoomLevel; - const deltaY = - (((e.clientY - startDragPos.y) / previewRef.current.clientHeight) * 100) / - zoomLevel; - // @ts-ignore - setPreviewOffset((prev) => ({ - x: Math.min(50, Math.max(-50, prev.x - deltaX)), - y: Math.min(50, Math.max(-50, prev.y - deltaY)), - })); + const deltaX = + (((e.clientX - startDragPos.x) / previewRef.current.clientWidth) * 100) / + zoomLevel; + const deltaY = + (((e.clientY - startDragPos.y) / previewRef.current.clientHeight) * 100) / + zoomLevel; + // @ts-ignore + setPreviewOffset((prev) => ({ + x: Math.min(50, Math.max(-50, prev.x - deltaX)), + y: Math.min(50, Math.max(-50, prev.y - deltaY)), + })); - setStartDragPos({ x: e.clientX, y: e.clientY }); - }; + setStartDragPos({ x: e.clientX, y: e.clientY }); + }; - // End dragging the preview - const endPreviewDrag = () => { - setIsDraggingPreview(false); - }; + // End dragging the preview + const endPreviewDrag = () => { + setIsDraggingPreview(false); + }; - // Make sure we have a valid path - const shapePath = selectedShape?.path || ""; - const editPath = currentEditPath || shapePath; + // Make sure we have a valid path + const shapePath = selectedShape?.path || ""; + const editPath = currentEditPath || shapePath; - return ( -
1 - ? "grab" - : "default", - }} - > -
+ return ( +
1 + ? "grab" + : "default", + }} + > +
- + {editMode ? ( +
+
+ Preview +
- {editMode ? ( -
-
- Preview -
+ {/* SVG editor overlay */} +
+ ) : ( +
+ Preview +
+ )} - {/* Control points */} - {controlPoints.map((point, index) => ( - { - e.stopPropagation(); // Prevent event bubbling - if (startDrag) startDrag(index); - }} - style={{ cursor: "move" }} - /> - ))} - -
- ) : ( -
- {/* Figure with clip path */} -
- Preview -
-
- )} - - {/* Zoom controls */} -
-
Zoom: {zoomLevel.toFixed(1)}x
- setZoomLevel(value[0])} - /> - -
-
- ); + {/* Zoom controls */} +
+
Zoom: {zoomLevel.toFixed(1)}x
+ setZoomLevel(value[0])} + /> + +
+
+ ); } diff --git a/components/view/shadow/index.tsx b/components/view/shadow/index.tsx index d06d44d..84b1963 100644 --- a/components/view/shadow/index.tsx +++ b/components/view/shadow/index.tsx @@ -1,293 +1,432 @@ "use client"; +import { Button } from "@/components/ui/button"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { useMediaQuery } from "@/components/ui/use-media-query"; import { preBuiltShadows } from "@/config/shadow-data"; import { useShadowStore } from "@/store/useShadowStore"; import type { ShadowLayer, ShadowPreset } from "@/types/shadow"; +import { + Bookmark, + Layers, + Moon, + PanelsTopLeft, + Settings2, + SidebarClose, + SidebarOpen, + Sun, +} from "lucide-react"; import { useTheme } from "next-themes"; +import { usePathname, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; import ShadowControls from "./shadow-controls"; -import ShadowGallery from "./shadow-gallery"; import ShadowPresets from "./shadow-presets"; import ShadowPreview from "./shadow-preview"; +import ThemeSwitch from "@/components/theme-switcher"; export default function ShadowGenerator() { - const { theme } = useTheme(); - const isTab = useMediaQuery("(max-width:1024px)"); - const isMobile = useMediaQuery("(max-width:768px)"); - const [shadowName, setShadowName] = useState(""); - const [isEdited, setIsEdited] = useState(false); - const [currentPresetId, setCurrentPresetId] = useState(null); - - const { - savedShadows, - favorites, - addShadow, - updateShadow, - deleteShadow, - toggleFavorite, - isFavorite, - } = useShadowStore(); - - const [isDarkMode, setIsDarkMode] = useState(false); - const [isRemoveShadow, setIsRemoveShadow] = useState(false); - const [layers, setLayers] = useState([ - { - offsetX: 0, - offsetY: 0, - blur: 0, - spread: 1, - color: "#000000", - opacity: 4, - isVisible: true, - }, - { - offsetX: 0, - offsetY: 1, - blur: 0, - spread: 0, - color: "#000000", - opacity: 5, - isVisible: true, - }, - { - offsetX: 0, - offsetY: 2, - blur: 2, - spread: 0, - color: "#000000", - opacity: 5, - isVisible: true, - }, - { - offsetX: 0, - offsetY: 2, - blur: 4, - spread: 0, - color: "#000000", - opacity: 5, - isVisible: true, - }, - ]); - const [activeShadow, setActiveShadow] = useState( - preBuiltShadows[0], - ); - const [activeLayerIndex, setActiveLayerIndex] = useState(0); - const [tailwindClass, setTailwindClass] = useState(""); - const [cssValue, setCssValue] = useState(""); - const [_uniqueColors, setUniqueColors] = useState< - { color: string; count: number }[] - >([]); - - const [globalPositionMode, setGlobalPositionMode] = useState(false); - const [globalShadowTypeMode, setGlobalShadowTypeMode] = useState(false); - const [globalBlurMode, setGlobalBlurMode] = useState(false); - const [globalSpreadMode, setGlobalSpreadMode] = useState(false); - const [globalOpacityMode, setGlobalOpacityMode] = useState(false); - const [globalMasterMode, setGlobalMasterMode] = useState(false); + const { theme } = useTheme(); + const isTab = useMediaQuery("(max-width:1024px)"); + const isMobile = useMediaQuery("(max-width:768px)"); + const [shadowName, setShadowName] = useState(""); + const [isEdited, setIsEdited] = useState(false); + const [currentPresetId, setCurrentPresetId] = useState(null); + const [shadowMode, setShadowMode] = useState<"box" | "text">("box"); - const toggleGlobalMasterMode = (enabled: boolean) => { - setGlobalMasterMode(enabled); - setGlobalPositionMode(enabled); - setGlobalBlurMode(enabled); - setGlobalSpreadMode(enabled); - setGlobalOpacityMode(enabled); - setGlobalShadowTypeMode(enabled); - }; + const { + savedShadows, + favorites, + addShadow, + updateShadow, + deleteShadow, + toggleFavorite, + isFavorite, + } = useShadowStore(); - useEffect(() => { - if (activeShadow) { - setIsEdited(checkIfEdited(layers, activeShadow)); - } - const shadowString = layers - .filter((layer) => layer.isVisible !== false) - .map((layer) => { - const { offsetX, offsetY, blur, spread, color, opacity, isInner } = - layer; - const rgba = `rgba(${Number.parseInt( - color.slice(1, 3), - 16, - )}, ${Number.parseInt(color.slice(3, 5), 16)}, ${Number.parseInt( - color.slice(5, 7), - 16, - )}, ${opacity / 100})`; - return `${ - isInner ? "inset " : "" - }${offsetX}px ${offsetY}px ${blur}px ${spread}px ${rgba}`; - }) - .join(", "); + const [isDarkMode, setIsDarkMode] = useState(false); + const [isRemoveShadow, setIsRemoveShadow] = useState(false); + const [layers, setLayers] = useState([ + { + offsetX: 0, + offsetY: 0, + blur: 0, + spread: 1, + color: "#000000", + opacity: 4, + isVisible: true, + }, + { + offsetX: 0, + offsetY: 1, + blur: 0, + spread: 0, + color: "#000000", + opacity: 5, + isVisible: true, + }, + { + offsetX: 0, + offsetY: 2, + blur: 2, + spread: 0, + color: "#000000", + opacity: 5, + isVisible: true, + }, + { + offsetX: 0, + offsetY: 2, + blur: 4, + spread: 0, + color: "#000000", + opacity: 5, + isVisible: true, + }, + ]); + const [activeShadow, setActiveShadow] = useState( + preBuiltShadows[0], + ); + const [activeLayerIndex, setActiveLayerIndex] = useState(0); + const [tailwindClass, setTailwindClass] = useState(""); + const [cssValue, setCssValue] = useState(""); + const [_uniqueColors, setUniqueColors] = useState< + { color: string; count: number }[] + >([]); - setCssValue(shadowString); + const [globalPositionMode, setGlobalPositionMode] = useState(false); + const [globalShadowTypeMode, setGlobalShadowTypeMode] = useState(false); + const [globalBlurMode, setGlobalBlurMode] = useState(false); + const [globalSpreadMode, setGlobalSpreadMode] = useState(false); + const [globalOpacityMode, setGlobalOpacityMode] = useState(false); + const [globalMasterMode, setGlobalMasterMode] = useState(false); + const [activeSidebarTab, setActiveSidebarTab] = useState< + "presets" | "settings" | "edited" | "saved" + >("settings"); + const [isSidebarExpanded, setIsSidebarExpanded] = useState(true); + const [previewBackground, setPreviewBackground] = useState("#ECF0F3"); + const [previewSurfaceColor, setPreviewSurfaceColor] = useState("#FFFFFF"); + const pathname = usePathname(); + const router = useRouter(); - const tailwindShadow = `shadow-[${layers - .filter((layer) => layer.isVisible !== false) - .map((layer) => { - const { offsetX, offsetY, blur, spread, color, opacity, isInner } = - layer; - const rgba = - opacity === 100 - ? color - : `rgba(${Number.parseInt(color.slice(1, 3), 16)},${Number.parseInt( - color.slice(3, 5), - 16, - )},${Number.parseInt(color.slice(5, 7), 16)},${opacity / 100})`; - return `${ - isInner ? "inset_" : "" - }${offsetX}px_${offsetY}px_${blur}px_${spread}px_${rgba}`; - }) - .join(",")}]`; + const toggleGlobalMasterMode = (enabled: boolean) => { + setGlobalMasterMode(enabled); + setGlobalPositionMode(enabled); + setGlobalBlurMode(enabled); + setGlobalSpreadMode(enabled); + setGlobalOpacityMode(enabled); + setGlobalShadowTypeMode(enabled); + }; - setTailwindClass(tailwindShadow); + useEffect(() => { + if (activeShadow) { + setIsEdited(checkIfEdited(layers, activeShadow)); + } + const shadowString = layers + .filter((layer) => layer.isVisible !== false) + .map((layer) => { + const { offsetX, offsetY, blur, spread, color, opacity, isInner } = + layer; + const rgba = `rgba(${Number.parseInt( + color.slice(1, 3), + 16, + )}, ${Number.parseInt(color.slice(3, 5), 16)}, ${Number.parseInt( + color.slice(5, 7), + 16, + )}, ${opacity / 100})`; + return `${ + isInner ? "inset " : "" + }${offsetX}px ${offsetY}px ${blur}px ${spread}px ${rgba}`; + }) + .join(", "); - const colorMap = new Map(); + setCssValue(shadowString); - for (const layer of layers) { - const color = layer.color.toUpperCase(); - colorMap.set(color, (colorMap.get(color) || 0) + 1); - } + const tailwindShadow = `shadow-[${layers + .filter((layer) => layer.isVisible !== false) + .map((layer) => { + const { offsetX, offsetY, blur, spread, color, opacity, isInner } = + layer; + const rgba = + opacity === 100 + ? color + : `rgba(${Number.parseInt(color.slice(1, 3), 16)},${Number.parseInt( + color.slice(3, 5), + 16, + )},${Number.parseInt(color.slice(5, 7), 16)},${opacity / 100})`; + return `${ + isInner ? "inset_" : "" + }${offsetX}px_${offsetY}px_${blur}px_${spread}px_${rgba}`; + }) + .join(",")}]`; - const uniqueColorArray = Array.from(colorMap.entries()).map( - ([color, count]) => ({ - color, - count, - }), - ); + setTailwindClass(tailwindShadow); - setUniqueColors(uniqueColorArray); - }, [layers, activeShadow]); + const colorMap = new Map(); - useEffect(() => { - setIsDarkMode(theme === "dark"); - }, [theme]); + for (const layer of layers) { + const color = layer.color.toUpperCase(); + colorMap.set(color, (colorMap.get(color) || 0) + 1); + } - const checkIfEdited = ( - currentLayers: ShadowLayer[], - originalPreset: ShadowPreset, - ) => { - if (!originalPreset) return false; + setUniqueColors( + Array.from(colorMap.entries()).map(([color, count]) => ({ + color, + count, + })), + ); + }, [layers, activeShadow]); - if (currentLayers.length !== originalPreset.layers.length) return true; + useEffect(() => { + setIsDarkMode(theme === "dark"); + }, [theme]); - for (let i = 0; i < currentLayers.length; i++) { - const currentLayer = currentLayers[i]; - const originalLayer = originalPreset.layers[i]; + const checkIfEdited = ( + currentLayers: ShadowLayer[], + originalPreset: ShadowPreset, + ) => { + if (!originalPreset) return false; + if (currentLayers.length !== originalPreset.layers.length) return true; - if ( - currentLayer.offsetX !== originalLayer.offsetX || - currentLayer.offsetY !== originalLayer.offsetY || - currentLayer.blur !== originalLayer.blur || - currentLayer.spread !== originalLayer.spread || - currentLayer.color !== originalLayer.color || - currentLayer.opacity !== originalLayer.opacity || - currentLayer.isInner !== originalLayer.isInner || - currentLayer.isVisible !== originalLayer.isVisible - ) { - return true; - } - } + for (let i = 0; i < currentLayers.length; i++) { + const currentLayer = currentLayers[i]; + const originalLayer = originalPreset.layers[i]; + if ( + currentLayer.offsetX !== originalLayer.offsetX || + currentLayer.offsetY !== originalLayer.offsetY || + currentLayer.blur !== originalLayer.blur || + currentLayer.spread !== originalLayer.spread || + currentLayer.color !== originalLayer.color || + currentLayer.opacity !== originalLayer.opacity || + currentLayer.isInner !== originalLayer.isInner || + currentLayer.isVisible !== originalLayer.isVisible + ) { + return true; + } + } - return false; - }; + return false; + }; - const applyPreset = (preset: ShadowPreset) => { - if (isDarkMode && preset.darkLayers) { - setLayers([...preset.darkLayers]); - } else { - setLayers([...preset.layers]); - } - setActiveLayerIndex(0); - setActiveShadow(preset); - setCurrentPresetId(preset.id || null); - setIsEdited(false); - setShadowName(preset.name || ""); - }; + const applyPreset = (preset: ShadowPreset) => { + if (isDarkMode && preset.darkLayers) { + setLayers([...preset.darkLayers]); + } else { + setLayers([...preset.layers]); + } + setShadowMode("box"); + setActiveLayerIndex(0); + setActiveShadow(preset); + setCurrentPresetId(preset.id || null); + setIsEdited(false); + setShadowName(preset.name || ""); + }; - const saveCurrentShadow = () => { - const shadowToSave = { - name: shadowName || "Custom Shadow", - tailwind: tailwindClass, - css: cssValue, - darkTailwind: isDarkMode ? tailwindClass : undefined, - darkCss: isDarkMode ? cssValue : undefined, - layers: [...layers], - darkLayers: isDarkMode ? [...layers] : undefined, - isCustom: true, - }; + const saveCurrentShadow = () => { + const shadowToSave = { + name: shadowName || "Custom Shadow", + tailwind: tailwindClass, + css: cssValue, + darkTailwind: isDarkMode ? tailwindClass : undefined, + darkCss: isDarkMode ? cssValue : undefined, + layers: [...layers], + darkLayers: isDarkMode ? [...layers] : undefined, + isCustom: true, + }; - if (currentPresetId && savedShadows.some((s) => s.id === currentPresetId)) { - updateShadow(currentPresetId, shadowToSave); + if (currentPresetId && savedShadows.some((s) => s.id === currentPresetId)) { + updateShadow(currentPresetId, shadowToSave); + toast.success("Your custom shadow has been updated.!"); + } else { + const newId = addShadow(shadowToSave); + setCurrentPresetId(newId); + toast.success("Your custom shadow has been saved."); + } - toast.success("Your custom shadow has been updated.!"); - } else { - const newId = addShadow(shadowToSave); - setCurrentPresetId(newId); - toast.success("Your custom shadow has been saved."); - } + setIsEdited(false); + }; - setIsEdited(false); - }; + const textShadowValue = layers + .filter((layer) => layer.isVisible !== false) + .map((layer) => { + const rgba = `rgba(${Number.parseInt(layer.color.slice(1, 3), 16)}, ${Number.parseInt(layer.color.slice(3, 5), 16)}, ${Number.parseInt(layer.color.slice(5, 7), 16)}, ${layer.opacity / 100})`; + return `${layer.offsetX}px ${layer.offsetY}px ${layer.blur}px ${rgba}`; + }) + .join(", "); - return ( - <> - - {isMobile && ( -

- Please use a desktop/laptop to view the Editor. -

- )} -
- {!isTab && ( - - )} + return ( + <> + {isMobile && ( +

+ Please use a desktop/laptop to view the Editor. +

+ )} +
+
+
+ {[ + { key: "presets", label: "Presets", icon: PanelsTopLeft }, + { key: "settings", label: "Settings", icon: Settings2 }, + { key: "edited", label: "Edited", icon: Layers }, + { key: "saved", label: "Saved", icon: Bookmark }, + ].map((item) => ( + + ))} +
+
+ + + +
+
- + {!isTab && isSidebarExpanded && ( +
+
+ + +
+ {activeSidebarTab === "settings" ? ( +
+ {shadowMode === "text" && ( +

+ Using the same layer controls as box shadow. For + text-shadow, spread/inset are ignored in output. +

+ )} +
+ +
+
+ ) : ( + + )} +
+ )} - {!isMobile && ( - - )} -
- - ); +
+ +
+
+ + ); } diff --git a/components/view/shadow/shadow-controls.tsx b/components/view/shadow/shadow-controls.tsx index 6d2d484..bcff7a3 100644 --- a/components/view/shadow/shadow-controls.tsx +++ b/components/view/shadow/shadow-controls.tsx @@ -152,7 +152,7 @@ export default function ShadowControls({ }; return ( - +

Shadow Controls

diff --git a/components/view/shadow/shadow-presets.tsx b/components/view/shadow/shadow-presets.tsx index 32bc1a5..339d150 100644 --- a/components/view/shadow/shadow-presets.tsx +++ b/components/view/shadow/shadow-presets.tsx @@ -3,15 +3,17 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { preBuiltShadows } from "@/config/shadow-data"; +import { preBuiltShadows, preBuiltTextShadows } from "@/config/shadow-data"; import { cn } from "@/lib/utils"; -import type { ShadowPreset } from "@/types/shadow"; +import type { ShadowLayer, ShadowPreset } from "@/types/shadow"; import { Star, Trash } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import type React from "react"; import toast from "react-hot-toast"; interface ShadowPresetsProps { + mode: "presets" | "edited" | "saved"; + shadowMode: "box" | "text"; activeShadow: ShadowPreset | null; applyPreset: (preset: ShadowPreset) => void; savedShadows: ShadowPreset[]; @@ -22,9 +24,13 @@ interface ShadowPresetsProps { currentPresetId: string | null; setCurrentPresetId: React.Dispatch>; isDarkMode: boolean; + setLayers: React.Dispatch>; + setActiveLayerIndex: React.Dispatch>; } export default function ShadowPresets({ + mode, + shadowMode, activeShadow, applyPreset, savedShadows, @@ -35,21 +41,72 @@ export default function ShadowPresets({ currentPresetId, setCurrentPresetId, isDarkMode, + setLayers, + setActiveLayerIndex, }: ShadowPresetsProps) { const favouriteShadows = preBuiltShadows.filter((s) => favorites.includes(s.id), ); + const activeTextShadow = layersToTextShadow(activeShadow?.layers || []); + + function layersToTextShadow(layers: readonly ShadowLayer[]) { + return layers + .filter((layer) => layer.isVisible !== false) + .map((layer) => { + const rgba = `rgba(${Number.parseInt(layer.color.slice(1, 3), 16)},${Number.parseInt(layer.color.slice(3, 5), 16)},${Number.parseInt(layer.color.slice(5, 7), 16)},${layer.opacity / 100})`; + return `${layer.offsetX}px ${layer.offsetY}px ${layer.blur}px ${rgba}`; + }) + .join(", "); + } + + if (shadowMode === "text") { + return ( + + {mode === "presets" ? ( +
+ {preBuiltTextShadows.map((preset) => ( + + ))} +
+ ) : ( +

+ Text shadow presets are currently available under the Presets tab. +

+ )} +
+ ); + } + return ( - - {savedShadows.length > 0 && ( -
+ + {mode === "saved" && savedShadows.length > 0 && ( +
{savedShadows.map((shadow) => ( { - // Create a preset-like object from saved shadow const savedPreset = { id: shadow.id, name: shadow.name, @@ -78,9 +135,7 @@ export default function ShadowPresets({ )}
)} - {favouriteShadows.length > 0 && ( -
+ {mode === "edited" && favouriteShadows.length > 0 && ( +
{favouriteShadows.map((shadow) => (
@@ -166,57 +219,68 @@ export default function ShadowPresets({
)} -
- {preBuiltShadows.map((shadow, index) => ( - applyPreset(shadow)} - > - - {shadow.id === activeShadow?.id && ( - - )} - -
- {shadow.name} -
- -
- ))} -
+ + {shadow.id === activeShadow?.id && ( + + )} + +
+ {shadow.name} +
+ +
+ ))} +
+ )} + + {mode === "saved" && savedShadows.length === 0 && ( +

+ No saved shadows yet. +

+ )} + {mode === "edited" && favouriteShadows.length === 0 && ( +

+ No edited/favorited shadows yet. +

+ )} ); } diff --git a/components/view/shadow/shadow-preview.tsx b/components/view/shadow/shadow-preview.tsx index 6454356..7bde201 100644 --- a/components/view/shadow/shadow-preview.tsx +++ b/components/view/shadow/shadow-preview.tsx @@ -25,12 +25,14 @@ import { import { cn } from "@/lib/utils"; import type { ShadowPreset } from "@/types/shadow"; import { Check, Copy, Save } from "lucide-react"; -import { useState } from "react"; +import { useId, useState } from "react"; import toast from "react-hot-toast"; interface ShadowPreviewProps { + shadowMode: "box" | "text"; cssValue: string; tailwindClass: string; + textShadowValue: string; isRemoveShadow: boolean; setIsRemoveShadow: React.Dispatch>; isEdited: boolean; @@ -38,11 +40,17 @@ interface ShadowPreviewProps { setShadowName: React.Dispatch>; saveCurrentShadow: () => void; activeShadow: ShadowPreset | null; + previewBackground: string; + setPreviewBackground: React.Dispatch>; + previewSurfaceColor: string; + setPreviewSurfaceColor: React.Dispatch>; } export default function ShadowPreview({ + shadowMode, cssValue, tailwindClass, + textShadowValue, isRemoveShadow, setIsRemoveShadow, isEdited, @@ -50,9 +58,21 @@ export default function ShadowPreview({ setShadowName, saveCurrentShadow, activeShadow, + previewBackground, + setPreviewBackground, + previewSurfaceColor, + setPreviewSurfaceColor, }: ShadowPreviewProps) { const [copiedTailwind, setCopiedTailwind] = useState(false); const [copiedCss, setCopiedCss] = useState(false); + const backgroundPickerId = useId(); + + const twValue = + shadowMode === "text" + ? `[text-shadow:${textShadowValue.replaceAll(" ", "_")}]` + : tailwindClass; + const cssProperty = shadowMode === "text" ? "text-shadow" : "box-shadow"; + const cssOutputValue = shadowMode === "text" ? textShadowValue : cssValue; const copyToClipboard = (text: string, isTailwind = true) => { navigator.clipboard.writeText(text); @@ -71,8 +91,11 @@ export default function ShadowPreview({ }; return ( - - + + - +
@@ -104,6 +130,7 @@ export default function ShadowPreview({ onClick={() => { setIsRemoveShadow(!isRemoveShadow); }} + disabled={shadowMode === "text"} > - Check Without Shadow + {shadowMode === "text" + ? "Not used for text shadows" + : "Check Without Shadow"} @@ -134,10 +163,11 @@ export default function ShadowPreview({
- + {["#ECF0F3", "#111827", "#fef3c7", "#dbeafe"].map((bg) => ( +
+ + -
+ {shadowMode === "text" ? ( +
+

+ Text Shadow +

+
+ ) : ( +
+ )} - -

Tailwind Class

+ + +

Tailwind CSS v4 Class

- {tailwindClass} + {twValue}
- +

How to use in your project @@ -239,108 +340,51 @@ export default function ShadowPreview({
-

Using Tailwind Classes

+

+ Using Tailwind CSS v4 Arbitrary Value +

- + - {`
Your content here
`} + {`
Your content here
`}
-

Using Custom CSS

+

Using Inline Style

- + - {`
Your content here
`} + {shadowMode === "text" + ? `

Text Shadow

` + : `
Your content here
`}
-

- Extending Tailwind Config (v3) -

-
- - - {`// tailwind.config.js -module.exports = { - theme: { - extend: { - boxShadow: { - '${activeShadow?.shadowName || activeShadow?.name || "custom"}': '${cssValue}', - } - } - } -}`} - -

- Extending global.css (v4) + Tailwind CSS v4 @theme Token

- {`// global.css -@theme { - --shadow-${activeShadow?.shadowName || activeShadow?.name || "custom"}: ${cssValue}; - } - `} + {`// global.css\n@theme {\n --shadow-${activeShadow?.shadowName || activeShadow?.name || "custom"}: ${cssOutputValue};\n}`}

- Then use it with:{" "} + Then use it with{" "} - shadow-custom + {shadowMode === "text" + ? "[text-shadow:var(--shadow-custom)]" + : "shadow-custom"}

- -

diff --git a/components/view/svg-line-draw/animate-svg.tsx b/components/view/svg-line-draw/animate-svg.tsx index 0a30ed8..9403f44 100644 --- a/components/view/svg-line-draw/animate-svg.tsx +++ b/components/view/svg-line-draw/animate-svg.tsx @@ -4,354 +4,355 @@ import { type Variants, motion, useAnimationControls } from "motion/react"; import React, { useState } from "react"; type PathData = { - d: string; - stroke?: string; - strokeWidth?: number; - strokeLinecap?: "butt" | "round" | "square"; + d: string; + stroke?: string; + strokeWidth?: number; + strokeLinecap?: "butt" | "round" | "square"; }; type HoverAnimationType = "float" | "pulse" | "redraw" | "color" | "sequential"; type TAnimateSvgProps = { - width: string; - height: string; - viewBox: string; - className: string; - path?: string; // Single path (legacy) - paths?: PathData[]; // New multiple path support - strokeColor?: string; - strokeWidth?: number; - strokeLinecap?: "butt" | "round" | "square"; - animationDuration?: number; - animationDelay?: number; - animationBounce?: number; - staggerDelay?: number; - reverseAnimation?: boolean; - enableHoverAnimation?: boolean; - hoverAnimationType?: HoverAnimationType; - hoverStrokeColor?: string | null; - initialAnimation?: boolean; + width: string; + height: string; + viewBox: string; + className: string; + path?: string; // Single path (legacy) + paths?: PathData[]; // New multiple path support + strokeColor?: string; // Deprecated - use CSS classes instead + strokeWidth?: number; + strokeLinecap?: "butt" | "round" | "square"; + animationDuration?: number; + animationDelay?: number; + animationBounce?: number; + staggerDelay?: number; + reverseAnimation?: boolean; + enableHoverAnimation?: boolean; + hoverAnimationType?: HoverAnimationType; + hoverStrokeColor?: string | null; + initialAnimation?: boolean; }; export const AnimateSvg: React.FC = ({ - width, - height, - viewBox, - className = "flex justify-center w-full h-full", - path, - paths = [], - strokeColor = "#cecece", - strokeWidth = 3, - strokeLinecap = "round", - animationDuration = 1.5, - animationDelay = 0, - animationBounce = 0.3, - staggerDelay = 0.2, - reverseAnimation = false, - enableHoverAnimation = false, - hoverAnimationType = "redraw", - hoverStrokeColor = "#4f46e5", - initialAnimation = true, + width, + height, + viewBox, + className = "flex justify-center w-full h-full", + path, + paths = [], + strokeColor = "text-primary", // Default to CSS class + strokeWidth = 3, + strokeLinecap = "round", + animationDuration = 1.5, + animationDelay = 0, + animationBounce = 0.3, + staggerDelay = 0.2, + reverseAnimation = false, + enableHoverAnimation = false, + hoverAnimationType = "redraw", + hoverStrokeColor = "#4f46e5", + initialAnimation = true, }) => { - const [isHovering, setIsHovering] = useState(false); + const [isHovering, setIsHovering] = useState(false); - const normalizedPaths: PathData[] = React.useMemo(() => { - if (paths.length > 0) return paths; - if (path) { - // Parse the path to normalize coordinates if needed - let normalizedPath = path; + const normalizedPaths: PathData[] = React.useMemo(() => { + if (paths.length > 0) return paths; + if (path) { + // Parse the path to normalize coordinates if needed + let normalizedPath = path; - // If the path is a complex path with large starting coordinates, try to normalize it - if (path.startsWith("M") && path.includes(",")) { - try { - // Extract all coordinates from the path - const allCoords: number[] = []; - const regex = /[-+]?[0-9]*\.?[0-9]+/g; - // biome-ignore lint/suspicious/noImplicitAnyLet: - let match; - // biome-ignore lint/suspicious/noAssignInExpressions: - while ((match = regex.exec(path)) !== null) { - allCoords.push(Number.parseFloat(match[0])); - } + // If the path is a complex path with large starting coordinates, try to normalize it + if (path.startsWith("M") && path.includes(",")) { + try { + // Extract all coordinates from the path + const allCoords: number[] = []; + const regex = /[-+]?[0-9]*\.?[0-9]+/g; + // biome-ignore lint/suspicious/noImplicitAnyLet: + let match; + // biome-ignore lint/suspicious/noAssignInExpressions: + while ((match = regex.exec(path)) !== null) { + allCoords.push(Number.parseFloat(match[0])); + } - // Find min and max x and y to normalize - if (allCoords.length >= 2) { - let minX = Number.POSITIVE_INFINITY; - let minY = Number.POSITIVE_INFINITY; - let maxX = Number.NEGATIVE_INFINITY; - let maxY = Number.NEGATIVE_INFINITY; + // Find min and max x and y to normalize + if (allCoords.length >= 2) { + let minX = Number.POSITIVE_INFINITY; + let minY = Number.POSITIVE_INFINITY; + let maxX = Number.NEGATIVE_INFINITY; + let maxY = Number.NEGATIVE_INFINITY; - for (let i = 0; i < allCoords.length; i += 2) { - if (i + 1 < allCoords.length) { - minX = Math.min(minX, allCoords[i]); - minY = Math.min(minY, allCoords[i + 1]); - maxX = Math.max(maxX, allCoords[i]); - maxY = Math.max(maxY, allCoords[i + 1]); - } - } + for (let i = 0; i < allCoords.length; i += 2) { + if (i + 1 < allCoords.length) { + minX = Math.min(minX, allCoords[i]); + minY = Math.min(minY, allCoords[i + 1]); + maxX = Math.max(maxX, allCoords[i]); + maxY = Math.max(maxY, allCoords[i + 1]); + } + } - // Calculate the path width and height - const pathWidth = maxX - minX; - const pathHeight = maxY - minY; + // Calculate the path width and height + const pathWidth = maxX - minX; + const pathHeight = maxY - minY; - // Parse the viewBox to get its dimensions - const viewBoxDims = viewBox.split(" ").map(Number); - const viewBoxWidth = viewBoxDims.length >= 3 ? viewBoxDims[2] : 100; - const viewBoxHeight = - viewBoxDims.length >= 4 ? viewBoxDims[3] : 100; + // Parse the viewBox to get its dimensions + const viewBoxDims = viewBox.split(" ").map(Number); + const viewBoxWidth = viewBoxDims.length >= 3 ? viewBoxDims[2] : 100; + const viewBoxHeight = + viewBoxDims.length >= 4 ? viewBoxDims[3] : 100; - // Calculate padding (10% of viewBox width) - const paddingX = viewBoxWidth * 0.1; - const paddingY = viewBoxHeight * 0.1; + // Calculate padding (10% of viewBox width) + const paddingX = viewBoxWidth * 0.1; + const paddingY = viewBoxHeight * 0.1; - // Calculate scale to fit the path within the viewBox with padding - const scaleX = (viewBoxWidth - 2 * paddingX) / pathWidth; - const scaleY = (viewBoxHeight - 2 * paddingY) / pathHeight; - const scale = Math.min(scaleX, scaleY); + // Calculate scale to fit the path within the viewBox with padding + const scaleX = (viewBoxWidth - 2 * paddingX) / pathWidth; + const scaleY = (viewBoxHeight - 2 * paddingY) / pathHeight; + const scale = Math.min(scaleX, scaleY); - // Adjust all coordinates to start from the left with padding and proper scaling - let adjustedPath = path; - let index = 0; - adjustedPath = adjustedPath.replace( - /[-+]?[0-9]*\.?[0-9]+/g, - (match) => { - const val = Number.parseFloat(match); - const isX = index % 2 === 0; - index++; + // Adjust all coordinates to start from the left with padding and proper scaling + let adjustedPath = path; + let index = 0; + adjustedPath = adjustedPath.replace( + /[-+]?[0-9]*\.?[0-9]+/g, + (match) => { + const val = Number.parseFloat(match); + const isX = index % 2 === 0; + index++; - if (isX) { - // X coordinate - shift to start from left with padding - return ((val - minX) * scale + paddingX).toFixed(2); - } - // Y coordinate - center vertically - return ((val - minY) * scale + paddingY).toFixed(2); - }, - ); - normalizedPath = adjustedPath; - } - } catch (e) { - // If parsing fails, use the original path - console.error("Failed to normalize path:", e); - } - } + if (isX) { + // X coordinate - shift to start from left with padding + return ((val - minX) * scale + paddingX).toFixed(2); + } + // Y coordinate - center vertically + return ((val - minY) * scale + paddingY).toFixed(2); + }, + ); + normalizedPath = adjustedPath; + } + } catch (e) { + // If parsing fails, use the original path + console.error("Failed to normalize path:", e); + } + } - return [ - { - d: normalizedPath, - stroke: strokeColor, - strokeWidth, - strokeLinecap, - }, - ]; - } - return []; - }, [paths, path, strokeColor, strokeWidth, strokeLinecap, viewBox]); + return [ + { + d: normalizedPath, + stroke: strokeColor, + strokeWidth, + strokeLinecap, + }, + ]; + } + return []; + }, [paths, path, strokeColor, strokeWidth, strokeLinecap, viewBox]); - // Initial animation variants - const getPathVariants = (index: number): Variants => ({ - hidden: { - pathLength: 0, - opacity: 0, - pathOffset: reverseAnimation ? 1 : 0, - }, - visible: { - pathLength: 1, - opacity: 1, - pathOffset: reverseAnimation ? 0 : 0, - transition: { - pathLength: { - type: "spring", - duration: animationDuration, - bounce: animationBounce, - delay: animationDelay + index * staggerDelay, - }, - pathOffset: { - duration: animationDuration, - delay: animationDelay + index * staggerDelay, - }, - opacity: { - duration: animationDuration / 4, - delay: animationDelay + index * staggerDelay, - }, - }, - }, - }); + // Initial animation variants + const getPathVariants = (index: number): Variants => ({ + hidden: { + pathLength: 0, + opacity: 0, + pathOffset: reverseAnimation ? 1 : 0, + }, + visible: { + pathLength: 1, + opacity: 1, + pathOffset: reverseAnimation ? 0 : 0, + transition: { + pathLength: { + type: "spring", + duration: animationDuration, + bounce: animationBounce, + delay: animationDelay + index * staggerDelay, + }, + pathOffset: { + duration: animationDuration, + delay: animationDelay + index * staggerDelay, + }, + opacity: { + duration: animationDuration / 4, + delay: animationDelay + index * staggerDelay, + }, + }, + }, + }); - if (normalizedPaths.length === 0) return null; + if (normalizedPaths.length === 0) return null; - return ( - setIsHovering(true)} - onHoverEnd={() => setIsHovering(false)} - whileHover={ - enableHoverAnimation && hoverAnimationType !== "redraw" - ? { scale: 1.05 } - : {} - } - preserveAspectRatio="xMidYMid meet" - style={{ maxWidth: "100%", maxHeight: "100%", display: "block" }} - > - animate svg - {normalizedPaths.map((pathData, index) => ( - - ))} - - ); + return ( + setIsHovering(true)} + onHoverEnd={() => setIsHovering(false)} + whileHover={ + enableHoverAnimation && hoverAnimationType !== "redraw" + ? { scale: 1.05 } + : {} + } + preserveAspectRatio="xMidYMid meet" + style={{ maxWidth: "100%", maxHeight: "100%", display: "block" }} + > + animate svg + {normalizedPaths.map((pathData, index) => ( + + ))} + + ); }; interface AnimatedPathProps { - pathData: PathData; - index: number; - strokeColor: string; - strokeWidth: number; - strokeLinecap: "butt" | "round" | "square"; - initialAnimation: boolean; - pathVariants: Variants; - isHovering: boolean; - hoverAnimationType: HoverAnimationType; - hoverStrokeColor: string | null; - totalPaths: number; + pathData: PathData; + index: number; + strokeColor: string; + strokeWidth: number; + strokeLinecap: "butt" | "round" | "square"; + initialAnimation: boolean; + pathVariants: Variants; + isHovering: boolean; + hoverAnimationType: HoverAnimationType; + hoverStrokeColor: string | null; + totalPaths: number; } const AnimatedPath: React.FC = ({ - pathData, - index, - strokeColor, - strokeWidth, - strokeLinecap, - initialAnimation, - pathVariants, - isHovering, - hoverAnimationType, - hoverStrokeColor, - totalPaths, + pathData, + index, + strokeColor, + strokeWidth, + strokeLinecap, + initialAnimation, + pathVariants, + isHovering, + hoverAnimationType, + hoverStrokeColor, + totalPaths, }) => { - const controls = useAnimationControls(); - const originalColor = pathData.stroke || strokeColor; + const controls = useAnimationControls(); + const originalColor = pathData.stroke || strokeColor; - // Handle hover animations - React.useEffect(() => { - if (!isHovering) { - controls.stop(); - if (initialAnimation) { - controls.start("visible"); - } - return; - } + // Handle hover animations + React.useEffect(() => { + if (!isHovering) { + controls.stop(); + if (initialAnimation) { + controls.start("visible"); + } + return; + } - switch (hoverAnimationType) { - case "redraw": - controls.start({ - pathLength: [1, 0, 1], - transition: { - pathLength: { - repeat: Number.POSITIVE_INFINITY, - duration: 3, - ease: "easeInOut", - }, - }, - }); - break; + switch (hoverAnimationType) { + case "redraw": + controls.start({ + pathLength: [1, 0, 1], + transition: { + pathLength: { + repeat: Number.POSITIVE_INFINITY, + duration: 3, + ease: "easeInOut", + }, + }, + }); + break; - case "float": - controls.start({ - y: [0, -2, 0], - transition: { - y: { - repeat: Number.POSITIVE_INFINITY, - duration: 1.5, - ease: "easeInOut", - }, - }, - }); - break; + case "float": + controls.start({ + y: [0, -2, 0], + transition: { + y: { + repeat: Number.POSITIVE_INFINITY, + duration: 1.5, + ease: "easeInOut", + }, + }, + }); + break; - case "pulse": - controls.start({ - scale: [1, 1.03, 1], - transition: { - scale: { - repeat: Number.POSITIVE_INFINITY, - duration: 1.3, - ease: "easeInOut", - }, - }, - }); - break; + case "pulse": + controls.start({ + scale: [1, 1.03, 1], + transition: { + scale: { + repeat: Number.POSITIVE_INFINITY, + duration: 1.3, + ease: "easeInOut", + }, + }, + }); + break; - case "color": - controls.start({ - stroke: [ - originalColor, - hoverStrokeColor || strokeColor, - originalColor, - ], - transition: { - stroke: { - repeat: Number.POSITIVE_INFINITY, - duration: 2, - ease: "easeInOut", - }, - }, - }); - break; + case "color": + controls.start({ + stroke: [ + originalColor, + hoverStrokeColor || strokeColor, + originalColor, + ], + transition: { + stroke: { + repeat: Number.POSITIVE_INFINITY, + duration: 2, + ease: "easeInOut", + }, + }, + }); + break; - case "sequential": - controls.start({ - pathLength: [1, 0, 1], - transition: { - pathLength: { - repeat: Number.POSITIVE_INFINITY, - duration: 2, - delay: (index / Math.max(totalPaths, 1)) * 2, - ease: "easeInOut", - }, - }, - }); - break; - } - }, [ - isHovering, - hoverAnimationType, - controls, - originalColor, - hoverStrokeColor, - strokeColor, - index, - totalPaths, - initialAnimation, - ]); + case "sequential": + controls.start({ + pathLength: [1, 0, 1], + transition: { + pathLength: { + repeat: Number.POSITIVE_INFINITY, + duration: 2, + delay: (index / Math.max(totalPaths, 1)) * 2, + ease: "easeInOut", + }, + }, + }); + break; + } + }, [ + isHovering, + hoverAnimationType, + controls, + originalColor, + hoverStrokeColor, + strokeColor, + index, + totalPaths, + initialAnimation, + ]); - return ( - - ); + return ( + + ); }; diff --git a/components/view/svg-line-draw/example-paths.tsx b/components/view/svg-line-draw/example-paths.tsx index 7172b4d..8379e02 100644 --- a/components/view/svg-line-draw/example-paths.tsx +++ b/components/view/svg-line-draw/example-paths.tsx @@ -12,46 +12,45 @@ import { toast } from "sonner"; import { examplesSvgPath } from "./data"; interface ExamplePathsProps { - onSelectPath: (path: string) => void; - // onEditPath: (path: string, viewBox: string) => void - setActivePresets: (presets: string) => void; - activePresets: string | null; + onSelectPath: (path: string) => void; + // onEditPath: (path: string, viewBox: string) => void + setActivePresets: (presets: string) => void; + activePresets: string | null; } export function ExamplePaths({ - onSelectPath, - setActivePresets, - activePresets, + onSelectPath, + setActivePresets, + activePresets, }: ExamplePathsProps) { - const { theme } = useTheme(); - const [_exampleViewBox, setExampleViewBox] = useQueryState("viewBox", { - defaultValue: "0 0 250 100", - }); - const [animationKeys, setAnimationKeys] = useState>( - {}, - ); - const [copiedIndex, setCopiedIndex] = useState(null); + const { theme } = useTheme(); + const [_exampleViewBox, setExampleViewBox] = useQueryState("viewBox", { + defaultValue: "0 0 250 100", + }); + const [animationKeys, setAnimationKeys] = useState>( + {}, + ); + const [copiedIndex, setCopiedIndex] = useState(null); - // Reload animation for a specific example - const reloadAnimation = (index: number) => { - setAnimationKeys((prev) => ({ - ...prev, - [index]: (prev[index] || 0) + 1, - })); - }; + // Reload animation for a specific example + const reloadAnimation = (index: number) => { + setAnimationKeys((prev) => ({ + ...prev, + [index]: (prev[index] || 0) + 1, + })); + }; - // Copy component code to clipboard - const copyComponentCode = (index: number) => { - const example = examplesSvgPath[index]; + // Copy component code to clipboard + const copyComponentCode = (index: number) => { + const example = examplesSvgPath[index]; - // Generate the full component code - const code = ``; - navigator.clipboard.writeText(code).then(() => { - setCopiedIndex(index); - toast.success("Component code copied", { - description: `${example.name} component code copied to clipboard`, - }); + navigator.clipboard.writeText(code).then(() => { + setCopiedIndex(index); + toast.success("Component code copied", { + description: `${example.name} component code copied to clipboard`, + }); - // Reset copied state after 2 seconds - setTimeout(() => { - setCopiedIndex(null); - }, 2000); - }); - }; + // Reset copied state after 2 seconds + setTimeout(() => { + setCopiedIndex(null); + }, 2000); + }); + }; - return ( -
- {examplesSvgPath.map((example, index) => ( -
-
{ - onSelectPath(example.path); - setExampleViewBox(example.viewBox); - setActivePresets(example.id); - }} - onKeyDown={(e) => { - e.preventDefault(); - onSelectPath(example.path); - setExampleViewBox(example.viewBox); - setActivePresets(example.id); - }} - > - -

- {example.name} -

-
+ return ( +
+ {examplesSvgPath.map((example, index) => ( +
+
{ + onSelectPath(example.path); + setExampleViewBox(example.viewBox); + setActivePresets(example.id); + }} + onKeyDown={(e) => { + e.preventDefault(); + onSelectPath(example.path); + setExampleViewBox(example.viewBox); + setActivePresets(example.id); + }} + > + +

+ {example.name} +

+
-
- - -
-
- ))} -
- ); +
+ + +
+
+ ))} +
+ ); } diff --git a/components/view/svg-line-draw/index.tsx b/components/view/svg-line-draw/index.tsx index b326058..5d62c13 100644 --- a/components/view/svg-line-draw/index.tsx +++ b/components/view/svg-line-draw/index.tsx @@ -2,54 +2,58 @@ import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import CopyToClipboard from "@/components/ui/copy-to-clipboard"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { - Popover, - PopoverContent, - PopoverTrigger, + Popover, + PopoverContent, + PopoverTrigger, } from "@/components/ui/popover"; import { CustomSlider } from "@/components/ui/range-slider"; import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; -import { ScrollArea } from "@radix-ui/react-scroll-area"; import { TabsTrigger } from "@radix-ui/react-tabs"; import { - Check, - ChevronDown, - ChevronUp, - ChevronsDown, - CodeIcon, - Copy, - Edit2, - GripVertical, - Loader, - PencilLine, - Play, - RefreshCcw, - RefreshCw, - Save, - Trash2, - X, + Bookmark, + ChevronDown, + ChevronUp, + CodeIcon, + Copy, + Edit2, + GripVertical, + Layers, + Loader, + PanelsTopLeft, + PencilLine, + Play, + RefreshCcw, + Save, + Settings2, + SidebarClose, + SidebarOpen, + Trash2, + X, } from "lucide-react"; import { useTheme } from "next-themes"; +import { usePathname, useRouter } from "next/navigation"; import { parseAsBoolean, parseAsIndex, useQueryState } from "nuqs"; import { useEffect, useState } from "react"; import { HexColorPicker } from "react-colorful"; @@ -59,155 +63,147 @@ import { CopyCode } from "../mesh-gradient/copy-code"; import { AnimateSvg } from "./animate-svg"; import { CodePreview } from "./code-preview"; import { CustomLineInput } from "./custom-line-input"; -import { compoentCode, examplesSvgPath } from "./data"; +import { compoentCode } from "./data"; import { DrawingCanvas } from "./drawing-canvas"; import { SavedEditedPathsTab } from "./edited-paths"; import { ExamplePaths } from "./example-paths"; import { SavePathDialog } from "./save-path-dialog"; import { SavedPathsTab } from "./saved-draw-paths"; import { SvgEditor } from "./svg-editor"; +import ThemeSwitch from "@/components/theme-switcher"; type AnimationSettings = { - width: string; - height: string; - viewBox: string; - strokeColor: string; - strokeWidth: number; - strokeLinecap: "butt" | "round" | "square"; - animationDuration: number; - animationDelay: number; - animationBounce: number; - reverseAnimation: boolean; - enableHoverAnimation: boolean; - hoverAnimationType: "float" | "pulse" | "redraw" | "color" | "sequential"; + width: string; + height: string; + viewBox: string; + strokeColor: string; + strokeWidth: number; + strokeLinecap: "butt" | "round" | "square"; + animationDuration: number; + animationDelay: number; + animationBounce: number; + reverseAnimation: boolean; + enableHoverAnimation: boolean; + hoverAnimationType: "float" | "pulse" | "redraw" | "color" | "sequential"; }; function SVGLineDrawGenerator() { - const { theme } = useTheme(); - const [activePresets, setActivePresets] = useQueryState("presets"); - const [exampleViewBox, setExampleViewBox] = useQueryState("viewBox", { - defaultValue: "0 0 250 100", - }); - const [editPath, setEditPath] = useQueryState( - "editPath", - parseAsBoolean.withDefault(false), - ); - const [customDrawLine, setCustomDrawLine] = useQueryState( - "customDrawLine", - parseAsBoolean.withDefault(false), - ); - const [viewAll, setViewAll] = useState(false); - const [currentPath, setCurrentPath] = useState(""); - const [animationKeys, setAnimationKeys] = useState>( - {}, - ); - const [copiedIndex, setCopiedIndex] = useState(null); - const [savedPaths, _setSavedPaths] = useState([]); - const [previewKey, setPreviewKey] = useState(0); - const [_showEditor, setShowEditor] = useState(false); - const [_editorViewBox, _setEditorViewBox] = useState("0 0 100 100"); - const [strokeColorPickerOpen, setStrokeColorPickerOpen] = useState(false); - const [showSaveDialog, setShowSaveDialog] = useState(false); - // Update the default smoothing value to be more appropriate - const [settings, setSettings] = useState({ - width: "100%", - height: "100%", - viewBox: "0 0 250 100", - strokeColor: "#000000", - strokeWidth: 2, - strokeLinecap: "round", - animationDuration: 1.5, - animationDelay: 0, - animationBounce: 0.3, - reverseAnimation: false, - enableHoverAnimation: false, - hoverAnimationType: "redraw", - }); - - useEffect(() => { - setSettings((prev) => ({ - ...prev, - viewBox: exampleViewBox, - strokeColor: theme === "dark" ? "#ffffff" : "#000000", - })); - }, [theme, exampleViewBox]); - // Update settings - const updateSetting = ( - key: K, - value: AnimationSettings[K], - ) => { - setSettings((prev) => ({ ...prev, [key]: value })); - setPreviewKey((prev) => prev + 1); - }; - - // Save current path - const savePath = () => { - if (!currentPath) { - toast.success("No path to save", { - description: "Draw something first before saving", - }); - return; - } - setShowSaveDialog(true); - console.log(currentPath); - - // setSavedPaths((prev) => [...prev, currentPath]); - // toast.success("Path saved", { - // description: "Your path has been added to the collection", - // }); - }; - - // Clear current path - const _clearPath = () => { - setCurrentPath(""); - }; - - // Copy generated code - const copyCode = () => { - const code = generateCode(); - navigator.clipboard.writeText(code); - toast.success("code copied", { - description: "The code has been copied to your clipboard", - }); - }; - - // Open the SVG editor for the current path - const _openEditor = () => { - if (!currentPath) { - toast("No path to edit", { - description: "Draw something first before editing", - }); - return; - } - setShowEditor(true); - }; - - // Open the SVG editor for an example path - // const openEditorForExample = (path: string, viewBox: string) => { - // setCurrentPath(path); - // setEditorViewBox(viewBox); - // setSettings((prev) => ({ - // ...prev, - // viewBox: viewBox, - // })); - // setShowEditor(true); - // }; - - const reloadAnimation = (index: number) => { - setAnimationKeys((prev) => ({ - ...prev, - [index]: (prev[index] || 0) + 1, - })); - }; - - // Generate code for the current animation - const generateCode = () => { - const pathsCode = - savedPaths.length > 0 - ? `paths={[${savedPaths.map((p) => `{ d: "${p}" }`).join(", ")}]}` - : `path="${currentPath}"`; - - return `(""); + const [savedPaths, _setSavedPaths] = useState([]); + const [previewKey, setPreviewKey] = useState(0); + const [_showEditor, setShowEditor] = useState(false); + const [_editorViewBox, _setEditorViewBox] = useState("0 0 100 100"); + const [strokeColorPickerOpen, setStrokeColorPickerOpen] = useState(false); + const [showSaveDialog, setShowSaveDialog] = useState(false); + const [activeSidebarTab, setActiveSidebarTab] = useState< + "presets" | "settings" | "edited" | "saved" + >("presets"); + const [isSidebarExpanded, setIsSidebarExpanded] = useState(true); + const pathname = usePathname(); + const router = useRouter(); + // Update the default smoothing value to be more appropriate + const [settings, setSettings] = useState({ + width: "100%", + height: "100%", + viewBox: "0 0 250 100", + strokeColor: "#000000", + strokeWidth: 2, + strokeLinecap: "round", + animationDuration: 1.5, + animationDelay: 0, + animationBounce: 0.3, + reverseAnimation: false, + enableHoverAnimation: false, + hoverAnimationType: "redraw", + }); + + useEffect(() => { + setSettings((prev) => ({ + ...prev, + viewBox: exampleViewBox, + strokeColor: theme === "dark" ? "#ffffff" : "#000000", + })); + }, [theme, exampleViewBox]); + // Update settings + const updateSetting = ( + key: K, + value: AnimationSettings[K], + ) => { + setSettings((prev) => ({ ...prev, [key]: value })); + setPreviewKey((prev) => prev + 1); + }; + + // Save current path + const savePath = () => { + if (!currentPath) { + toast.success("No path to save", { + description: "Draw something first before saving", + }); + return; + } + setShowSaveDialog(true); + console.log(currentPath); + + // setSavedPaths((prev) => [...prev, currentPath]); + // toast.success("Path saved", { + // description: "Your path has been added to the collection", + // }); + }; + + // Clear current path + const _clearPath = () => { + setCurrentPath(""); + }; + + // Copy generated code + const copyCode = () => { + const code = generateCode(); + navigator.clipboard.writeText(code); + toast.success("code copied", { + description: "The code has been copied to your clipboard", + }); + }; + + // Open the SVG editor for the current path + const _openEditor = () => { + if (!currentPath) { + toast("No path to edit", { + description: "Draw something first before editing", + }); + return; + } + setShowEditor(true); + }; + + // Open the SVG editor for an example path + // const openEditorForExample = (path: string, viewBox: string) => { + // setCurrentPath(path); + // setEditorViewBox(viewBox); + // setSettings((prev) => ({ + // ...prev, + // viewBox: viewBox, + // })); + // setShowEditor(true); + // }; + + // Generate code for the current animation + const generateCode = () => { + const pathsCode = + savedPaths.length > 0 + ? `paths={[${savedPaths.map((p) => `{ d: "${p}" }`).join(", ")}]}` + : `path="${currentPath}"`; + + return ``; - }; - - const handleLineDraw = () => { - if (activePresets) { - setActivePresets(null); - setEditPath(false); - setCustomDrawLine(true); - } else { - setCustomDrawLine(!customDrawLine); - } - }; - const handleEditPath = () => { - if (activePresets) { - setEditPath(!editPath); - } else { - setEditPath(!editPath); - } - }; - - const copyDynamicCode = (index: number) => { - const example = examplesSvgPath[index]; - - // Generate the full component code - const code = ``; + }; + + const handleLineDraw = () => { + if (activePresets) { + setActivePresets(null); + setEditPath(false); + setCustomDrawLine(true); + } else { + setCustomDrawLine(!customDrawLine); + } + }; + const handleEditPath = () => { + if (activePresets) { + setEditPath(!editPath); + } else { + setEditPath(!editPath); + } + }; + + return ( +
+
+
+
+ {[ + { key: "presets", label: "Presets", icon: PanelsTopLeft }, + { key: "settings", label: "Settings", icon: Settings2 }, + { key: "edited", label: "Edited", icon: Layers }, + { key: "saved", label: "Saved", icon: Bookmark }, + ].map((item) => ( + + ))} +
+
+ + + +
+
+ + {/* Left Column - Examples and Animation Settings */} +