diff --git a/src/App.jsx b/src/App.jsx index feec0c5..8931724 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -33,6 +33,7 @@ import ListResources from "./pages/ResourceHub/resourcehub/ListResources"; import Base64Image from "./pages/DevUtilities/devutilities/Base64Image"; import Base64Url from "./pages/DevUtilities/devutilities/Base64Url"; import BcryptGenerator from "./pages/DevUtilities/devutilities/BcryptGenerator"; +import BorderImageGenerator from "./pages/DevUtilities/devutilities/BorderImageGenerator"; import ChmodCalculator from "./pages/DevUtilities/devutilities/ChmodCalculator"; import ClipPathMaker from "./pages/DevUtilities/devutilities/ClipPathMaker"; import CodeSandbox from "./pages/DevUtilities/devutilities/CodeSandbox"; @@ -501,6 +502,10 @@ function AppInner({ toggleHUD, hudVisible }) { path="/devutilities/css-gradient" element={} /> + } + /> } diff --git a/src/config/sidebarSections.js b/src/config/sidebarSections.js index c59199a..2e164a5 100644 --- a/src/config/sidebarSections.js +++ b/src/config/sidebarSections.js @@ -297,6 +297,12 @@ const SIDEBAR_SECTIONS = [ description: "Create beautiful CSS gradients with live preview", path: "/devutilities/css-gradient", }, + { + label: "CSS Border-Image Generator", + description: + "Design custom sliced image borders and generate copy-ready CSS properties visually.", + path: "/devutilities/border-image", + }, { label: "Glassmorphism Playground", description: "Design glassmorphism effects with shadows and blur", diff --git a/src/pages/DevUtilities/DevUtilities.jsx b/src/pages/DevUtilities/DevUtilities.jsx index 1aa9d8e..a867b5f 100644 --- a/src/pages/DevUtilities/DevUtilities.jsx +++ b/src/pages/DevUtilities/DevUtilities.jsx @@ -964,6 +964,27 @@ const DevUtilities = () => { ), }, + { + title: "CSS Border-Image Generator", + description: + "Design custom sliced image borders and generate copy-ready CSS properties visually.", + path: "/devutilities/border-image", + icon: ( + + + + ), + }, { title: "Password Generator", description: diff --git a/src/pages/DevUtilities/devutilities/BorderImageGenerator.jsx b/src/pages/DevUtilities/devutilities/BorderImageGenerator.jsx new file mode 100644 index 0000000..d7666dc --- /dev/null +++ b/src/pages/DevUtilities/devutilities/BorderImageGenerator.jsx @@ -0,0 +1,462 @@ +import { useRef, useState } from "react"; +import { Link } from "react-router-dom"; +import { useTheme } from "../../../context/ThemeContext"; +import { toast } from "sonner"; +import { + FaArrowLeft, + FaBorderStyle, + FaCopy, + FaImage, + FaLink, + FaUnlink, + FaUpload, +} from "react-icons/fa"; + +// Centers of the 8 outer cells of a 90x90 asset sliced into a 3x3 grid of 30px cells +const CELL_CENTERS = [ + [15, 15], + [45, 15], + [75, 15], + [15, 45], + [75, 45], + [15, 75], + [45, 75], + [75, 75], +]; + +const makeSvg = (shapes) => + "data:image/svg+xml," + + encodeURIComponent( + `${shapes}` + ); + +const defaultAssets = [ + { + name: "Diamonds", + slice: 30, + url: makeSvg( + CELL_CENTERS.map( + ([x, y]) => + `` + ).join("") + ), + }, + { + name: "Dots", + slice: 30, + url: makeSvg( + CELL_CENTERS.map( + ([x, y]) => `` + ).join("") + ), + }, + { + name: "Frame", + slice: 30, + url: makeSvg( + `` + + `` + + `` + + `` + + `` + ), + }, + { + name: "Stripes", + slice: 30, + url: makeSvg( + `` + + `` + ), + }, +]; + +const repeatOptions = ["stretch", "repeat", "round", "space"]; +const sliceSides = ["top", "right", "bottom", "left"]; + +export default function BorderImageGenerator() { + const { dark } = useTheme(); + const fileInputRef = useRef(null); + + const [source, setSource] = useState(defaultAssets[0].url); + const [sourceName, setSourceName] = useState(defaultAssets[0].name); + const [slices, setSlices] = useState({ top: 30, right: 30, bottom: 30, left: 30 }); + const [linked, setLinked] = useState(true); + const [sliceUnit, setSliceUnit] = useState(""); + const [fill, setFill] = useState(false); + const [repeatX, setRepeatX] = useState("round"); + const [repeatY, setRepeatY] = useState("round"); + const [borderWidth, setBorderWidth] = useState(20); + const [dragging, setDragging] = useState(false); + + const theme = { + light: { + wrapper: "bg-[#F8F9FA] text-zinc-900", + heading: "text-zinc-900", + subtext: "text-zinc-500", + card: "bg-white border-zinc-200/85 shadow-sm", + input: "bg-zinc-50 border-zinc-200 text-zinc-900 focus:border-zinc-400 focus:outline-none", + secondaryBtn: "bg-white text-zinc-800 border-zinc-200 hover:bg-zinc-50 transition-all duration-200", + backLink: "bg-white border-neutral-200 text-neutral-600 hover:text-black hover:border-neutral-350", + codeBox: "bg-zinc-900 text-zinc-100 border-zinc-800", + dropzone: "border-zinc-300 bg-zinc-50/60 text-zinc-500", + dropzoneActive: "border-indigo-400 bg-indigo-50 text-indigo-600", + }, + dark: { + wrapper: "bg-[#090A0F] text-zinc-100", + heading: "text-zinc-100", + subtext: "text-zinc-500", + card: "bg-zinc-900/50 border-zinc-800/85 backdrop-blur-md shadow-md", + input: "bg-zinc-900 border-zinc-700 text-zinc-100 focus:border-zinc-500 focus:outline-none", + secondaryBtn: "bg-zinc-800/50 text-zinc-300 border-zinc-700 hover:bg-zinc-700/50 transition-all duration-200", + backLink: "bg-zinc-800/80 border-zinc-700 text-zinc-300 hover:text-white hover:border-zinc-600", + codeBox: "bg-black/40 text-emerald-400 border-zinc-800/80 font-mono", + dropzone: "border-zinc-700 bg-zinc-900/40 text-zinc-500", + dropzoneActive: "border-indigo-500 bg-indigo-500/10 text-indigo-400", + }, + }; + + const t = dark ? theme.dark : theme.light; + + const updateSlice = (side, rawValue) => { + const value = Math.min(100, Math.max(0, Number(rawValue) || 0)); + if (linked) { + setSlices({ top: value, right: value, bottom: value, left: value }); + } else { + setSlices((prev) => ({ ...prev, [side]: value })); + } + }; + + const loadDefaultAsset = (asset) => { + setSource(asset.url); + setSourceName(asset.name); + setSlices({ top: asset.slice, right: asset.slice, bottom: asset.slice, left: asset.slice }); + setSliceUnit(""); + toast.success(`Loaded ${asset.name} asset`); + }; + + const loadFile = (file) => { + if (!file || !file.type.startsWith("image/")) { + toast.error("Please drop an image file (SVG, PNG, JPG, ...)"); + return; + } + const reader = new FileReader(); + reader.onload = () => { + setSource(reader.result); + setSourceName(file.name); + toast.success(`Loaded ${file.name}`); + }; + reader.onerror = () => toast.error("Could not read the image file"); + reader.readAsDataURL(file); + }; + + const sliceValue = `${sliceSides.map((s) => `${slices[s]}${sliceUnit}`).join(" ")}${fill ? " fill" : ""}`; + const repeatValue = repeatX === repeatY ? repeatX : `${repeatX} ${repeatY}`; + + const buildCSS = (url) => + [ + `border-style: solid;`, + `border-width: ${borderWidth}px;`, + `border-image-source: url('${url}');`, + `border-image-slice: ${sliceValue};`, + `border-image-repeat: ${repeatValue};`, + ].join("\n"); + + const displayUrl = source.length > 72 ? `${source.slice(0, 72)}…` : source; + + const copyCSS = () => { + navigator.clipboard.writeText(buildCSS(source)); + toast.success("Copied CSS to clipboard!"); + }; + + const previewStyle = { + borderStyle: "solid", + borderWidth: `${borderWidth}px`, + borderImageSource: `url('${source}')`, + borderImageSlice: sliceValue, + borderImageRepeat: repeatValue, + resize: "both", + overflow: "auto", + }; + + return ( +
+ CSS Border-Image Generator — DevTasks + + +
+ {/* Header */} +
+ + + +
+

+ CSS Border-Image Generator +

+

+ Slice a border asset visually and export copy-paste border-image CSS. +

+
+
+ +
+ {/* Left: Controls & Slice Guides */} +
+ {/* Asset loader */} +
+

+ + Load Asset +

+ +
+ {defaultAssets.map((asset) => ( + + ))} +
+ +
{ + e.preventDefault(); + setDragging(true); + }} + onDragLeave={() => setDragging(false)} + onDrop={(e) => { + e.preventDefault(); + setDragging(false); + loadFile(e.dataTransfer.files[0]); + }} + onClick={() => fileInputRef.current?.click()} + className={`rounded-2xl border-2 border-dashed p-5 text-center cursor-pointer transition-colors duration-200 ${ + dragging ? t.dropzoneActive : t.dropzone + }`} + > + +

+ Drag & drop a custom border image here, or click to browse +

+

+ Loaded: {sourceName} +

+ { + loadFile(e.target.files[0]); + e.target.value = ""; + }} + /> +
+
+ + {/* Slice guides */} +
+
+

+ + Slice Guides +

+
+ + +
+
+ +
+ {sliceSides.map((side) => ( +
+ +
+ updateSlice(side, e.target.value)} + className="flex-1 accent-indigo-500 h-1.5 bg-zinc-200 dark:bg-zinc-800 rounded-lg cursor-pointer" + /> + updateSlice(side, e.target.value)} + className={`w-16 px-2 py-1 rounded-lg border text-xs font-semibold text-center ${t.input}`} + /> +
+
+ ))} +
+ + +
+ + {/* Repeat & width */} +
+

Repeat & Border Width

+ +
+
+ + +
+
+ + +
+
+ +
+ + setBorderWidth(Number(e.target.value))} + className="flex-1 accent-indigo-500 h-1.5 bg-zinc-200 dark:bg-zinc-800 rounded-lg cursor-pointer" + /> +
+
+
+ + {/* Right: Live Preview & Code Output */} +
+
+

Live Preview

+

+ Drag the bottom-right corner of the box to resize it and see how the border adapts. +

+ +
+
+ Resizable preview +
+
+
+ +
+

CSS Code Output

+
+
+                  {buildCSS(displayUrl)}
+                
+ +
+

+ Long image URLs are truncated in the display above — the copied CSS always contains the + full border-image-source. +

+
+
+
+
+
+ ); +}