complete text and group,position duplcate feature in topbar

This commit is contained in:
smfahim25 2025-02-01 17:27:20 +06:00
parent 79ca662a08
commit 8e6637f7fb
14 changed files with 1205 additions and 499 deletions

491
package-lock.json generated
View file

@ -34,6 +34,7 @@
"react-icons": "^5.4.0",
"react-image-file-resizer": "^0.4.8",
"react-rnd": "^10.4.13",
"react-tooltip": "^5.28.0",
"react-window": "^1.8.10",
"tailwind-merge": "^2.5.4",
"tailwind-scrollbar": "^3.1.0",
@ -54,6 +55,7 @@
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"postcss": "^8.4.48",
"sass-embedded": "^1.83.4",
"tailwindcss": "^3.4.14",
"vite": "^5.4.10"
}
@ -378,6 +380,13 @@
"node": ">=6.9.0"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.3.tgz",
"integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==",
"dev": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@ -3814,6 +3823,13 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer-builder": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
"dev": true,
"license": "MIT/X11"
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@ -3965,6 +3981,12 @@
"url": "https://polar.sh/cva"
}
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -4002,6 +4024,13 @@
"color-support": "bin.js"
}
},
"node_modules/colorjs.io": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -5458,6 +5487,13 @@
"node": ">= 4"
}
},
"node_modules/immutable": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
"dev": true,
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -7251,6 +7287,20 @@
}
}
},
"node_modules/react-tooltip": {
"version": "5.28.0",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.0.tgz",
"integrity": "sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.1",
"classnames": "^2.3.0"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/react-window": {
"version": "1.8.10",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz",
@ -7474,6 +7524,16 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-array-concat": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
@ -7539,6 +7599,407 @@
"license": "MIT",
"optional": true
},
"node_modules/sass-embedded": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.83.4.tgz",
"integrity": "sha512-Hf2burRA/y5PGxsg6jB9UpoK/xZ6g/pgrkOcdl6j+rRg1Zj8XhGKZ1MTysZGtTPUUmiiErqzkP5+Kzp95yv9GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"buffer-builder": "^0.2.0",
"colorjs.io": "^0.5.0",
"immutable": "^5.0.2",
"rxjs": "^7.4.0",
"supports-color": "^8.1.1",
"sync-child-process": "^1.0.2",
"varint": "^6.0.0"
},
"bin": {
"sass": "dist/bin/sass.js"
},
"engines": {
"node": ">=16.0.0"
},
"optionalDependencies": {
"sass-embedded-android-arm": "1.83.4",
"sass-embedded-android-arm64": "1.83.4",
"sass-embedded-android-ia32": "1.83.4",
"sass-embedded-android-riscv64": "1.83.4",
"sass-embedded-android-x64": "1.83.4",
"sass-embedded-darwin-arm64": "1.83.4",
"sass-embedded-darwin-x64": "1.83.4",
"sass-embedded-linux-arm": "1.83.4",
"sass-embedded-linux-arm64": "1.83.4",
"sass-embedded-linux-ia32": "1.83.4",
"sass-embedded-linux-musl-arm": "1.83.4",
"sass-embedded-linux-musl-arm64": "1.83.4",
"sass-embedded-linux-musl-ia32": "1.83.4",
"sass-embedded-linux-musl-riscv64": "1.83.4",
"sass-embedded-linux-musl-x64": "1.83.4",
"sass-embedded-linux-riscv64": "1.83.4",
"sass-embedded-linux-x64": "1.83.4",
"sass-embedded-win32-arm64": "1.83.4",
"sass-embedded-win32-ia32": "1.83.4",
"sass-embedded-win32-x64": "1.83.4"
}
},
"node_modules/sass-embedded-android-arm": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.83.4.tgz",
"integrity": "sha512-9Z4pJAOgEkXa3VDY/o+U6l5XvV0mZTJcSl0l/mSPHihjAHSpLYnOW6+KOWeM8dxqrsqTYcd6COzhanI/a++5Gw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.83.4.tgz",
"integrity": "sha512-tgX4FzmbVqnQmD67ZxQDvI+qFNABrboOQgwsG05E5bA/US42zGajW9AxpECJYiMXVOHmg+d81ICbjb0fsVHskw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.83.4.tgz",
"integrity": "sha512-RsFOziFqPcfZXdFRULC4Ayzy9aK6R6FwQ411broCjlOBX+b0gurjRadkue3cfUEUR5mmy0KeCbp7zVKPLTK+5Q==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-riscv64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.83.4.tgz",
"integrity": "sha512-EHwh0nmQarBBrMRU928eTZkFGx19k/XW2YwbPR4gBVdWLkbTgCA5aGe8hTE6/1zStyx++3nDGvTZ78+b/VvvLg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-android-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.83.4.tgz",
"integrity": "sha512-0PgQNuPWYy1jEOEPDVsV89KfqOsMLIp9CSbjBY7jRcwRhyVAcigqrUG6bDeNtojHUYKA1kU+Eh/85WxOHUOgBw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-darwin-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.83.4.tgz",
"integrity": "sha512-rp2ywymWc3nymnSnAFG5R/8hvxWCsuhK3wOnD10IDlmNB7o4rzKby1c+2ZfpQGowlYGWsWWTgz8FW2qzmZsQRw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-darwin-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.83.4.tgz",
"integrity": "sha512-kLkN2lXz9PCgGfDS8Ev5YVcl/V2173L6379en/CaFuJJi7WiyPgBymW7hOmfCt4uO4R1y7CP2Uc08DRtZsBlAA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-arm": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.83.4.tgz",
"integrity": "sha512-nL90ryxX2lNmFucr9jYUyHHx21AoAgdCL1O5Ltx2rKg2xTdytAGHYo2MT5S0LIeKLa/yKP/hjuSvrbICYNDvtA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.83.4.tgz",
"integrity": "sha512-E0zjsZX2HgESwyqw31EHtI39DKa7RgK7nvIhIRco1d0QEw227WnoR9pjH3M/ZQy4gQj3GKilOFHM5Krs/omeIA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.83.4.tgz",
"integrity": "sha512-ew5HpchSzgAYbQoriRh8QhlWn5Kw2nQ2jHoV9YLwGKe3fwwOWA0KDedssvDv7FWnY/FCqXyymhLd6Bxae4Xquw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-arm": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.83.4.tgz",
"integrity": "sha512-0RrJRwMrmm+gG0VOB5b5Cjs7Sd+lhqpQJa6EJNEaZHljJokEfpE5GejZsGMRMIQLxEvVphZnnxl6sonCGFE/QQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.83.4.tgz",
"integrity": "sha512-IzMgalf6MZOxgp4AVCgsaWAFDP/IVWOrgVXxkyhw29fyAEoSWBJH4k87wyPhEtxSuzVHLxKNbc8k3UzdWmlBFg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.83.4.tgz",
"integrity": "sha512-LLb4lYbcxPzX4UaJymYXC+WwokxUlfTJEFUv5VF0OTuSsHAGNRs/rslPtzVBTvMeG9TtlOQDhku1F7G6iaDotA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-riscv64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.83.4.tgz",
"integrity": "sha512-zoKlPzD5Z13HKin1UGR74QkEy+kZEk2AkGX5RelRG494mi+IWwRuWCppXIovor9+BQb9eDWPYPoMVahwN5F7VA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-musl-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.83.4.tgz",
"integrity": "sha512-hB8+/PYhfEf2zTIcidO5Bpof9trK6WJjZ4T8g2MrxQh8REVtdPcgIkoxczRynqybf9+fbqbUwzXtiUao2GV+vQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-riscv64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.83.4.tgz",
"integrity": "sha512-83fL4n+oeDJ0Y4KjASmZ9jHS1Vl9ESVQYHMhJE0i4xDi/P3BNarm2rsKljq/QtrwGpbqwn8ujzOu7DsNCMDSHA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-linux-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.83.4.tgz",
"integrity": "sha512-NlnGdvCmTD5PK+LKXlK3sAuxOgbRIEoZfnHvxd157imCm/s2SYF/R28D0DAAjEViyI8DovIWghgbcqwuertXsA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-arm64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.83.4.tgz",
"integrity": "sha512-J2BFKrEaeSrVazU2qTjyQdAk+MvbzJeTuCET0uAJEXSKtvQ3AzxvzndS7LqkDPbF32eXAHLw8GVpwcBwKbB3Uw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-ia32": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.83.4.tgz",
"integrity": "sha512-uPAe9T/5sANFhJS5dcfAOhOJy8/l2TRYG4r+UO3Wp4yhqbN7bggPvY9c7zMYS0OC8tU/bCvfYUDFHYMCl91FgA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded-win32-x64": {
"version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.83.4.tgz",
"integrity": "sha512-C9fkDY0jKITdJFij4UbfPFswxoXN9O/Dr79v17fJnstVwtUojzVJWKHUXvF0Zg2LIR7TCc4ju3adejKFxj7ueA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-embedded/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/saxes": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
@ -7985,6 +8446,29 @@
"license": "MIT",
"optional": true
},
"node_modules/sync-child-process": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"sync-message-port": "^1.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/sync-message-port": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz",
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/tailwind-merge": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz",
@ -8396,6 +8880,13 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "5.4.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",

View file

@ -36,6 +36,7 @@
"react-icons": "^5.4.0",
"react-image-file-resizer": "^0.4.8",
"react-rnd": "^10.4.13",
"react-tooltip": "^5.28.0",
"react-window": "^1.8.10",
"tailwind-merge": "^2.5.4",
"tailwind-scrollbar": "^3.1.0",
@ -56,6 +57,7 @@
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"postcss": "^8.4.48",
"sass-embedded": "^1.83.4",
"tailwindcss": "^3.4.14",
"vite": "^5.4.10"
}

View file

@ -1,4 +1,18 @@
.fabric-canvas-container {
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.2);
border-radius: 0px;
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.2);
border-radius: 0px;
}
.tooltip {
--tooltip-spacing: 30px;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.example::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.example {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}

View file

@ -160,7 +160,7 @@ export default function Canvas() {
return (
<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 ${
activeObject ? "mt-5" : "mt-20"
activeObject ? "mt-20" : "mt-20"
} mx-auto bg-white pl-5 pb-5 pt-5 border-0 shadow-none`}
>
<CardContent className="p-0 space-y-2">

View file

@ -1,4 +1,4 @@
import { useContext, useState, useRef, useEffect } from "react";
import { useContext, useState, useRef, useEffect, useCallback } from "react";
import { fabric } from "fabric";
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
@ -47,34 +47,37 @@ const StrokeCustomization = () => {
}
}
if (object.strokeWidth) {
setStrokeWidth(0);
setStrokeWidth(object.strokeWidth || 0);
}
};
// Recursively process group objects
const processGroupObjects = (group) => {
const processGroupObjects = useCallback((group, callback) => {
group._objects.forEach((obj) => {
if (obj.type === "group") {
processGroupObjects(obj); // Handle nested groups
processGroupObjects(obj, callback); // Handle nested groups
} else {
handleObjectStyle(obj); // Apply styles to each object
callback(obj); // Apply callback to each object
}
});
};
}, []);
// Effect to get previous values from active object
useEffect(() => {
if (activeObject) {
if (activeObject.type === "group") {
processGroupObjects(activeObject); // Process grouped objects
processGroupObjects(activeObject, handleObjectStyle); // Process grouped objects
} else {
handleObjectStyle(activeObject); // Process single object
}
}
}, [activeObject]);
}, [activeObject, processGroupObjects]);
const updatePreview = () => {
if (!previewRef.current) return;
// Update preview style
const updatePreview = useCallback(() => {
if (!previewRef.current) {
return;
}
const previewStyle = {
width: "80px",
@ -90,10 +93,6 @@ const StrokeCustomization = () => {
}
Object.assign(previewRef.current.style, previewStyle);
};
useEffect(() => {
updatePreview();
}, [
strokeWidth,
strokeColor,
@ -102,24 +101,35 @@ const StrokeCustomization = () => {
gradientDirection,
]);
useEffect(() => {
updatePreview();
}, [updatePreview]);
// Handle stroke width change
const handleStrokeWidthChange = (value) => {
setStrokeWidth(value);
};
// Handle color type change
const handleColorTypeChange = (type) => {
setColorType(type);
};
// Handle stroke color change
const handleStrokeColorChange = (e) => {
setStrokeColor(e.target.value);
};
// Handle gradient color change
const handleGradientColorChange = (key, e) => {
setGradientStrokeColors((prev) => ({ ...prev, [key]: e.target.value }));
};
const applyStrokeStyle = () => {
if (!activeObject || activeObject.type === "line") return;
// Apply stroke style to active object
const applyStrokeStyle = useCallback(() => {
if (!activeObject || activeObject.type === "line" || !canvas) {
return;
}
const width = activeObject?.width || 0;
const height = activeObject?.height || 0;
@ -132,7 +142,7 @@ const StrokeCustomization = () => {
};
const directionCoords = coords[gradientDirection];
const applyStrokeStyle = (object) => {
const applyStrokeToObject = (object) => {
object.set("strokeWidth", strokeWidth);
if (colorType === "color") {
@ -151,56 +161,42 @@ const StrokeCustomization = () => {
}
};
const processGroupObjects = (group) => {
group._objects.forEach((obj) => {
if (obj.type === "group") {
processGroupObjects(obj);
} else {
applyStrokeStyle(obj);
}
});
};
if (activeObject.type === "group") {
processGroupObjects(activeObject);
processGroupObjects(activeObject, applyStrokeToObject);
} else {
applyStrokeStyle(activeObject);
applyStrokeToObject(activeObject);
}
canvas.renderAll();
};
// Automatically apply stroke styles on state change
useEffect(() => {
applyStrokeStyle();
}, [
activeObject,
strokeWidth,
strokeColor,
gradientStrokeColors,
colorType,
gradientDirection,
canvas,
processGroupObjects,
]);
// Automatically apply stroke styles on state change
useEffect(() => {
applyStrokeStyle();
}, [applyStrokeStyle]);
// Revert stroke styles
const revertStroke = () => {
if (!activeObject) return;
if (!activeObject || !canvas) {
return;
}
const revertObject = (object) => {
object.set("strokeWidth", 0);
object.set("stroke", null);
};
const processGroupObjects = (group) => {
group._objects.forEach((obj) => {
if (obj.type === "group") {
processGroupObjects(obj);
} else {
revertObject(obj);
}
});
};
if (activeObject.type === "group") {
processGroupObjects(activeObject);
processGroupObjects(activeObject, revertObject);
} else {
revertObject(activeObject);
}
@ -208,187 +204,196 @@ const StrokeCustomization = () => {
canvas.renderAll();
};
const content = () => {
return (
<CardContent className="p-0 mb-2">
<div className="space-y-2">
<div>
<Label htmlFor="stroke-width">Stroke Width</Label>
<div className="flex items-center space-x-2">
<Slider
id="stroke-width"
min={0}
max={50}
step={1}
value={[strokeWidth]}
onValueChange={([value]) => handleStrokeWidthChange(value)}
className="flex-grow"
/>
<Input
type="number"
min={0}
max={50}
value={strokeWidth}
onChange={(e) =>
handleStrokeWidthChange(Number(e.target.value))
}
className="w-16"
/>
// Render the component
return (
<Card className="shadow-none border-0">
<CollapsibleComponent text={"Stroke Control"}>
<CardContent className="p-0 mb-2">
<div className="space-y-2">
<div>
<Label htmlFor="stroke-width">Stroke Width</Label>
<div className="flex items-center space-x-2">
<Slider
id="stroke-width"
min={0}
max={50}
step={1}
value={[strokeWidth]}
onValueChange={([value]) => handleStrokeWidthChange(value)}
className="flex-grow"
/>
<Input
type="number"
min={0}
max={50}
value={strokeWidth}
onChange={(e) =>
handleStrokeWidthChange(Number(e.target.value))
}
className="w-16"
/>
</div>
</div>
</div>
<div>
<Tabs
value={colorType}
onValueChange={(value) => handleColorTypeChange(value)}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="color">
<Paintbrush className="w-4 h-4 mr-2" />
Solid
</TabsTrigger>
<TabsTrigger value="gradient" className="flex gap-2">
<div className="h-4 w-4 rounded bg-gradient-to-r from-purple-500 to-pink-500" />
Gradient
</TabsTrigger>
</TabsList>
<TabsContent value="color" className="space-y-4">
<div className="space-y-1">
<Label htmlFor="stroke-color">Stroke Color</Label>
<div className="flex items-center space-x-2">
<div className="relative">
<div>
<Tabs
value={colorType}
onValueChange={(value) => handleColorTypeChange(value)}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="color">
<Paintbrush className="w-4 h-4 mr-2" />
Solid
</TabsTrigger>
<TabsTrigger value="gradient" className="flex gap-2">
<div className="h-4 w-4 rounded bg-gradient-to-r from-purple-500 to-pink-500" />
Gradient
</TabsTrigger>
</TabsList>
<TabsContent value="color" className="space-y-4">
<div className="space-y-1">
<Label htmlFor="stroke-color">Stroke Color</Label>
<div className="flex items-center space-x-2">
<div className="relative">
<Input
id="stroke-color"
type="color"
value={strokeColor}
onChange={handleStrokeColorChange}
className="w-10 h-10 p-1 rounded-md cursor-pointer"
/>
<div
className="absolute inset-0 pointer-events-none"
style={{
backgroundColor: strokeColor,
borderRadius: "0.375rem",
}}
></div>
</div>
<Input
id="stroke-color"
type="color"
type="text"
value={strokeColor}
onChange={handleStrokeColorChange}
className="w-10 h-10 p-1 rounded-md cursor-pointer"
className="flex-grow"
/>
<div
className="absolute inset-0 pointer-events-none"
style={{
backgroundColor: strokeColor,
borderRadius: "0.375rem",
}}
></div>
</div>
<Input
type="text"
value={strokeColor}
onChange={handleStrokeColorChange}
className="flex-grow"
/>
</div>
</div>
</TabsContent>
<TabsContent value="gradient" className="space-y-4">
<div className="space-y-1">
<Label htmlFor="gradient-direction">Gradient Direction</Label>
<Select
value={gradientDirection}
onValueChange={setGradientDirection}
>
<SelectTrigger id="gradient-direction">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="to bottom">Top to Bottom</SelectItem>
<SelectItem value="to top">Bottom to Top</SelectItem>
<SelectItem value="to right">Left to Right</SelectItem>
<SelectItem value="to left">Right to Left</SelectItem>
</SelectContent>
</Select>
</div>
</TabsContent>
<TabsContent value="gradient" className="space-y-4">
<div className="space-y-1">
<Label htmlFor="gradient-direction">
Gradient Direction
</Label>
<Select
value={gradientDirection}
onValueChange={setGradientDirection}
>
<SelectTrigger id="gradient-direction">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="to bottom" key="to-bottom">
Top to Bottom
</SelectItem>
<SelectItem value="to top" key="to-top">
Bottom to Top
</SelectItem>
<SelectItem value="to right" key="to-right">
Left to Right
</SelectItem>
<SelectItem value="to left" key="to-left">
Right to Left
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label htmlFor="gradient-color-1">Gradient Color 1</Label>
<div className="flex items-center space-x-2">
<div className="relative">
<div className="space-y-1">
<Label htmlFor="gradient-color-1">Gradient Color 1</Label>
<div className="flex items-center space-x-2">
<div className="relative">
<Input
id="gradient-color-1"
type="color"
value={gradientStrokeColors.color1}
onChange={(e) =>
handleGradientColorChange("color1", e)
}
className="w-10 h-10 p-1 rounded-md cursor-pointer"
/>
<div
className="absolute inset-0 pointer-events-none"
style={{
backgroundColor: gradientStrokeColors.color1,
borderRadius: "0.375rem",
}}
></div>
</div>
<Input
id="gradient-color-1"
type="color"
type="text"
value={gradientStrokeColors.color1}
onChange={(e) => handleGradientColorChange("color1", e)}
className="w-10 h-10 p-1 rounded-md cursor-pointer"
className="flex-grow"
/>
<div
className="absolute inset-0 pointer-events-none"
style={{
backgroundColor: gradientStrokeColors.color1,
borderRadius: "0.375rem",
}}
></div>
</div>
<Input
type="text"
value={gradientStrokeColors.color1}
onChange={(e) => handleGradientColorChange("color1", e)}
className="flex-grow"
/>
</div>
</div>
<div className="space-y-1">
<Label htmlFor="gradient-color-2">Gradient Color 2</Label>
<div className="flex items-center space-x-2">
<div className="relative">
<div className="space-y-1">
<Label htmlFor="gradient-color-2">Gradient Color 2</Label>
<div className="flex items-center space-x-2">
<div className="relative">
<Input
id="gradient-color-2"
type="color"
value={gradientStrokeColors.color2}
onChange={(e) =>
handleGradientColorChange("color2", e)
}
className="w-10 h-10 p-1 rounded-md cursor-pointer"
/>
<div
className="absolute inset-0 pointer-events-none"
style={{
backgroundColor: gradientStrokeColors.color2,
borderRadius: "0.375rem",
}}
></div>
</div>
<Input
id="gradient-color-2"
type="color"
type="text"
value={gradientStrokeColors.color2}
onChange={(e) => handleGradientColorChange("color2", e)}
className="w-10 h-10 p-1 rounded-md cursor-pointer"
className="flex-grow"
/>
<div
className="absolute inset-0 pointer-events-none"
style={{
backgroundColor: gradientStrokeColors.color2,
borderRadius: "0.375rem",
}}
></div>
</div>
<Input
type="text"
value={gradientStrokeColors.color2}
onChange={(e) => handleGradientColorChange("color2", e)}
className="flex-grow"
/>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</TabsContent>
</Tabs>
</div>
<div className="space-y-1">
<Label>Preview</Label>
<div
className="border rounded-md p-2 flex items-center justify-center"
style={{ height: "120px" }}
>
<div className="space-y-1">
<Label>Preview</Label>
<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 className="grid gap-1">
<Button onClick={applyStrokeStyle} className="bg-[#FF2B85]">
Apply Stroke
</Button>
<Button onClick={revertStroke} variant="outline">
Revert Stroke
</Button>
</div>
</div>
<div className="grid gap-1">
<Button onClick={applyStrokeStyle} className="bg-[#FF2B85]">
Apply Stroke
</Button>
<Button onClick={revertStroke} variant="outline">
Revert Stroke
</Button>
</div>
</div>
</CardContent>
);
};
return (
<Card className=" shadow-none border-0">
<CollapsibleComponent text={"Stroke Control"}>
{content()}
</CardContent>
</CollapsibleComponent>
</Card>
);

View file

@ -17,7 +17,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useCallback, useContext, useEffect, useState } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import {
AlignLeft,
AlignCenter,
@ -31,6 +31,7 @@ import {
} from "lucide-react";
import { RiLineHeight } from "react-icons/ri";
import { Slider } from "@/components/ui/slider";
import { Tooltip } from "react-tooltip";
const fonts = [
"Roboto",
@ -88,6 +89,11 @@ const TextCustomization = () => {
const { activeObject } = useContext(ActiveObjectContext);
const { canvas, setSelectedPanel, textColor } = useContext(CanvasContext);
const activeObjectType = activeObject?.type;
const hasClipPath = !!activeObject?.clipPath;
const customClipPath = activeObject?.isClipPath;
const prevTextRef = useRef("");
const [text, setText] = useState("");
const [fontFamily, setFontFamily] = useState("Arial");
const [fontSize, setFontSize] = useState(20);
@ -102,6 +108,13 @@ const TextCustomization = () => {
useEffect(() => {
if (activeObject?.type === "i-text") {
if (
activeObject?.text !== undefined &&
activeObject.text !== prevTextRef.current
) {
setText(activeObject.text);
prevTextRef.current = activeObject.text;
}
setText(activeObject?.text || "");
setFontFamily(activeObject?.fontFamily || "Arial");
setFontSize(activeObject?.fontSize || 20);
@ -119,11 +132,16 @@ const TextCustomization = () => {
const updateActiveObject = useCallback(
(properties) => {
if (activeObject?.type === "i-text") {
activeObject.set(properties);
// Preserve the text value when updating other properties
const updatedProperties = {
...properties,
text: text || prevTextRef.current || properties.text,
};
activeObject.set(updatedProperties);
canvas?.renderAll();
}
},
[activeObject, canvas]
[activeObject, canvas, text]
); // Add dependencies
const applyChanges = useCallback(() => {
@ -153,6 +171,12 @@ const TextCustomization = () => {
updateActiveObject, // Add this dependency
]);
const handleColorPanelClick = () => {
// Store current text value before switching panels
prevTextRef.current = text;
setSelectedPanel("color");
};
// Automatically apply changes when state updates
useEffect(() => {
if (activeObject?.type === "i-text") {
@ -209,52 +233,75 @@ const TextCustomization = () => {
{/* New Toolbar Design */}
<div className="flex w-full items-center space-x-2 rounded-lg p-1 bg-white">
{/* Font Family Select */}
<Select value={fontFamily} onValueChange={handleFontFamilyChange}>
<SelectTrigger className="min-w-[140px] h-8">
<SelectValue placeholder="Select a font" />
</SelectTrigger>
<SelectContent className="h-[250px]">
<SelectGroup>
{fonts.map((font) => (
<SelectItem
key={font}
value={font}
style={{ fontFamily: font }}
>
{font}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<a data-tooltip-id="fonts">
<Select
value={fontFamily}
onValueChange={handleFontFamilyChange}
title="Font Family"
>
<SelectTrigger className="min-w-[140px] h-8">
<SelectValue placeholder="Select a font" />
</SelectTrigger>
<SelectContent className="h-[250px]">
<SelectGroup>
{fonts.map((font) => (
<SelectItem
key={font}
value={font}
style={{ fontFamily: font }}
>
{font}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</a>
<Tooltip id="fonts" content="Font Family" place="bottom" />
{/* Font Size Controls */}
<div className="flex items-center border rounded-md">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-none"
onClick={() => handleFontSizeChange(Math.max(8, fontSize - 1))}
>
<Minus className="h-4 w-4" />
</Button>
<Input
type="text"
value={fontSize}
onChange={(e) => {
const numericValue = e.target.value.replace(/\D/g, "");
handleFontSizeChange(parseInt(numericValue) || 12);
}}
className="w-12 h-8 border-0 text-center"
<a data-tooltip-id="font-dec">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-none"
onClick={() => handleFontSizeChange(Math.max(8, fontSize - 1))}
>
<Minus className="h-4 w-4" />
</Button>
</a>
<Tooltip
id="font-dec"
content="Decrease font size"
place="bottom"
/>
<a data-tooltip-id="font-size">
<Input
type="text"
value={fontSize}
onChange={(e) => {
const numericValue = e.target.value.replace(/\D/g, "");
handleFontSizeChange(parseInt(numericValue) || 12);
}}
className="w-12 h-8 border-0 text-center"
/>
</a>
<Tooltip id="font-size" content="Font size" place="bottom" />
<a data-tooltip-id="font-inc">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-none"
onClick={() => handleFontSizeChange(Math.min(72, fontSize + 1))}
>
<Plus className="h-4 w-4" />
</Button>
</a>
<Tooltip
id="font-inc"
content="Increase font size"
place="bottom"
/>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-none"
onClick={() => handleFontSizeChange(Math.min(72, fontSize + 1))}
>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* Vertical Separator */}
@ -262,74 +309,102 @@ const TextCustomization = () => {
{/* Text Formatting Controls */}
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="icon"
className="relative"
onClick={() => setSelectedPanel("color")}
>
<div className="relative">
<span className="text-lg font-semibold">A</span>
<div
className="absolute -bottom-0.5 -left-1 right-0 h-1 w-5 transition-all duration-200"
style={{ backgroundColor: fillColor }}
/>
</div>
</Button>
{activeObjectType !== "image" &&
!hasClipPath &&
!customClipPath && (
<a data-tooltip-id="text-color">
<Button
variant="ghost"
size="icon"
className="relative"
onClick={handleColorPanelClick} // Updated onClick handler
>
<div className="relative">
<span className="text-lg font-semibold">A</span>
<div
className="absolute -bottom-0.5 -left-1 right-0 h-1 w-5 transition-all duration-200"
style={{ backgroundColor: fillColor }}
/>
</div>
</Button>
</a>
)}
<Button
variant={textAlign === "left" ? "secondary" : "ghost"}
size="icon"
onClick={() => handleTextAlignChange("left")}
>
<AlignLeft className="h-4 w-4" />
</Button>
<Button
variant={textAlign === "center" ? "secondary" : "ghost"}
size="icon"
onClick={() => handleTextAlignChange("center")}
>
<AlignCenter className="h-4 w-4" />
</Button>
<Button
variant={textAlign === "right" ? "secondary" : "ghost"}
size="icon"
onClick={() => handleTextAlignChange("right")}
>
<AlignRight className="h-4 w-4" />
</Button>
<Button
variant={fontWeight === "bold" ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleFontWeightChange}
>
<Bold className="h-4 w-4" />
</Button>
<Button
variant={underline ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleUnderlineChange}
>
<Underline className="h-4 w-4" />
</Button>
<Button
variant={fontStyle === "italic" ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleFontStyleChange}
>
<Italic className="h-4 w-4" />
</Button>
<Button
variant={linethrough ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleLinethroughChange}
>
<Strikethrough className="h-4 w-4" />
</Button>
<Tooltip id="text-color" content="Text color" place="bottom" />
<a data-tooltip-id="text-left">
<Button
variant={textAlign === "left" ? "secondary" : "ghost"}
size="icon"
onClick={() => handleTextAlignChange("left")}
>
<AlignLeft className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="text-center">
<Button
variant={textAlign === "center" ? "secondary" : "ghost"}
size="icon"
onClick={() => handleTextAlignChange("center")}
>
<AlignCenter className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="text-right">
<Button
variant={textAlign === "right" ? "secondary" : "ghost"}
size="icon"
onClick={() => handleTextAlignChange("right")}
>
<AlignRight className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="text-bold">
<Button
variant={fontWeight === "bold" ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleFontWeightChange}
>
<Bold className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="text-underline">
<Button
variant={underline ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleUnderlineChange}
>
<Underline className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="text-italic">
<Button
variant={fontStyle === "italic" ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleFontStyleChange}
>
<Italic className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="line-through">
<Button
variant={linethrough ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleLinethroughChange}
>
<Strikethrough className="h-4 w-4" />
</Button>
</a>
<Tooltip id="text-left" content="Left align" place="bottom" />
<Tooltip id="text-center" content="Center align" place="bottom" />
<Tooltip id="text-right" content="Right align" place="bottom" />
<Tooltip id="text-bold" content="Bold" place="bottom" />
<Tooltip id="text-underline" content="Underline" place="bottom" />
<Tooltip id="text-italic" content="Italics" place="bottom" />
<Tooltip id="line-through" content="StrikeThrough" place="bottom" />
</div>
{/* Vertical Separator */}
@ -338,9 +413,11 @@ const TextCustomization = () => {
{/* Spacing Controls */}
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<RiLineHeight className="h-4 w-4" />
</Button>
<a data-tooltip-id="spacing">
<Button variant="ghost" size="icon" className="h-8 w-8">
<RiLineHeight className="h-4 w-4" />
</Button>
</a>
</PopoverTrigger>
<PopoverContent className="w-44 mt-3">
<div className="space-y-4">
@ -373,6 +450,7 @@ const TextCustomization = () => {
</div>
</PopoverContent>
</Popover>
<Tooltip id="spacing" content="Spacing" place="bottom" />
</div>
{/* Text Input */}

View file

@ -1,19 +1,39 @@
import { useCallback, useContext } from 'react'
import CanvasContext from './Context/canvasContext/CanvasContext';
import ActiveObjectContext from './Context/activeObject/ObjectContext';
import { fabric } from 'fabric';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
import { Button } from './ui/button';
import { BringToFront, CopyPlus, GroupIcon, SquareX, Trash2, UngroupIcon } from 'lucide-react';
import { useCallback, useContext, useMemo, useState } from "react";
import CanvasContext from "./Context/canvasContext/CanvasContext";
import ActiveObjectContext from "./Context/activeObject/ObjectContext";
import { fabric } from "fabric";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip";
import { Button } from "./ui/button";
import {
BringToFront,
SendToBack,
CopyPlus,
GroupIcon,
SquareX,
Trash2,
UngroupIcon,
Layers,
} from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
export const ObjectShortcut = ({ value }) => {
const { canvas } = useContext(CanvasContext);
const { setActiveObject, activeObject } = useContext(ActiveObjectContext);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const activeObjects = canvas.getActiveObjects();
const multipleObjects = activeObjects.length > 1;
const objectActive = canvas.getActiveObject();
const isGroupObject = objectActive && objectActive.type === "group";
const groupSelectedObjects = () => {
const activeObjects = canvas.getActiveObjects(); // Get selected objects
if (activeObjects.length > 1) {
canvas.discardActiveObject();
const group = new fabric.Group(activeObjects, {
@ -21,70 +41,98 @@ export const ObjectShortcut = ({ value }) => {
top: canvas?.height / 2,
originX: "center",
originY: "center",
selectable: true, // Allow group selection
subTargetCheck: true, // Allow individual object selection
hasControls: true, // Enable resizing/movement of the group
})
selectable: true, // Allow group selection
subTargetCheck: true, // Allow individual object selection
hasControls: true, // Enable resizing/movement of the group
});
canvas.remove(...activeObjects);
canvas.add(group);
canvas.setActiveObject(group);
setActiveObject(group);
canvas.renderAll();
} else {
console.log("Select at least two objects")
console.log("Select at least two objects");
}
};
const ungroupSelectedObjects = () => {
const activeObject = canvas.getActiveObject();
if (activeObject && activeObject.type === "group") {
// Get the group
const group = activeObject;
const groupObjects = activeObject._objects;
canvas.discardActiveObject();
// Remove the group from the canvas
canvas.remove(group);
canvas.remove(activeObject);
setActiveObject(null);
// Iterate through each object in the group
group._objects.forEach((object) => {
// Calculate the absolute position based on the group's transformation
const objLeft = object.left * group.scaleX + group.left;
const objTop = object.top * group.scaleY + group.top;
const ungroupedObjects = [];
groupObjects.forEach((object) => {
// Calculate absolute position
const objLeft = object.left * activeObject.scaleX + activeObject.left;
const objTop = object.top * activeObject.scaleY + activeObject.top;
// Reset transformations and positions
object.set({
left: objLeft,
top: objTop,
scaleX: object.scaleX * group.scaleX, // Adjust scale based on group
scaleY: object.scaleY * group.scaleY, // Adjust scale based on group
angle: object.angle + group.angle, // Adjust rotation
hasControls: true, // Allow resizing
selectable: true, // Make selectable
group: null, // Remove group reference
scaleX: object.scaleX * activeObject.scaleX,
scaleY: object.scaleY * activeObject.scaleY,
angle: object.angle + activeObject.angle,
hasControls: true,
selectable: true,
group: null,
originX: "center",
originY: "center",
});
// Update coordinates
object.setCoords();
// Add the object back to the canvas
canvas.add(object);
ungroupedObjects.push(object);
});
// Clear the selection
canvas.discardActiveObject();
const selection = new fabric.ActiveSelection(ungroupedObjects, {
canvas: canvas,
originX: "center",
originY: "center",
});
canvas.setActiveObject(selection);
setActiveObject(selection);
// Render the canvas to reflect changes
canvas.renderAll();
}
};
// Bring Selected Object to Front
// Check if object is at front or back
const objectPosition = useMemo(() => {
if (!activeObject || !canvas) {
return { isAtFront: false, isAtBack: false };
}
const allObjects = canvas.getObjects();
const index = allObjects.indexOf(activeObject);
return {
isAtFront: index === allObjects.length - 1,
isAtBack: index === 0,
};
}, [activeObject, canvas]);
// Layer ordering functions with checks
const bringToFront = () => {
if (activeObject) {
if (activeObject && !objectPosition.isAtFront) {
activeObject.bringToFront();
canvas.renderAll();
setIsPopoverOpen(false);
}
};
const sendToBack = () => {
if (activeObject && !objectPosition.isAtBack) {
activeObject.sendToBack();
canvas.renderAll();
setIsPopoverOpen(false);
}
};
@ -94,8 +142,10 @@ export const ObjectShortcut = ({ value }) => {
const allObjects = canvas?.getObjects();
const textObjects = allObjects.filter((obj) => obj.type === 'textbox' || obj.type === 'text' ||
obj.type === 'i-text');
const textObjects = allObjects.filter(
(obj) =>
obj.type === "textbox" || obj.type === "text" || obj.type === "i-text"
);
if (activeObject) {
canvas.remove(activeObject);
@ -116,47 +166,62 @@ export const ObjectShortcut = ({ value }) => {
// Clone the active object to create a true deep copy
activeObject.clone((clonedObject) => {
// Add the cloned object to the canvas
clonedObject.set("left", clonedObject?.left + 30)
clonedObject.set("left", clonedObject?.left + 30);
canvas.add(clonedObject);
canvas.renderAll();
});
}
};
// for clear canvas
const clearCanvas = () => {
canvas.clear();
canvas.renderAll();
setActiveObject(null);
}
};
return (
<div>
<TooltipProvider>
<div className={`grid grid-cols-3 gap-2 ${value === "default" ? "xl:grid-cols-6 lg:grid-cols-6 md:grid-cols-6" : "xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-3"}`}>
<ActionButton
icon={<GroupIcon className="h-4 w-4" />}
label="Group"
onClick={groupSelectedObjects}
tooltipContent={
<div className="text-sm">
<p className="font-semibold mb-1">Group selected objects</p>
<p>To select multiple objects:</p>
<ol className="list-decimal list-inside mt-1">
<li>Hold down the Shift key</li>
<li>Click and drag with the left mouse button to select objects</li>
<li>Release the Shift key and mouse button</li>
</ol>
<p className="mt-1">Then click this button to group the selected objects.</p>
</div>
}
/>
<div
className={`flex items-center gap-2 ${
value === "default"
? "space-x-4"
: "xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-3"
}`}
>
{multipleObjects && (
<ActionButton
icon={<GroupIcon className="h-4 w-4" />}
label="Group"
onClick={groupSelectedObjects}
tooltipContent={
<div className="text-sm">
<p className="font-semibold mb-1">Group selected objects</p>
<p>To select multiple objects:</p>
<ol className="list-decimal list-inside mt-1">
<li>Hold down the Shift key</li>
<li>
Click and drag with the left mouse button to select
objects
</li>
<li>Release the Shift key and mouse button</li>
</ol>
<p className="mt-1">
Then click this button to group the selected objects.
</p>
</div>
}
/>
)}
<ActionButton
icon={<UngroupIcon className="h-4 w-4" />}
label="Ungroup"
onClick={ungroupSelectedObjects}
tooltipContent="Ungroup selected objects"
/>
{isGroupObject && (
<ActionButton
icon={<UngroupIcon className="h-4 w-4" />}
label="Ungroup"
onClick={ungroupSelectedObjects}
tooltipContent="Ungroup selected objects"
/>
)}
<ActionButton
icon={<CopyPlus className="h-4 w-4" />}
@ -164,13 +229,45 @@ export const ObjectShortcut = ({ value }) => {
onClick={duplicating}
tooltipContent="Duplicate selected objects"
/>
<ActionButton
icon={<BringToFront className="h-4 w-4" />}
label="To Front"
onClick={bringToFront}
tooltipContent="Bring selected objects to front"
/>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button variant="outline" size="md" className="w-full">
<div className="flex items-center gap-1 p-1">
<Layers className="h-4 w-4" />
<span className="text-[10px] font-bold">Position</span>
</div>
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" align="center">
<p>Change object position</p>
</TooltipContent>
</Tooltip>
<PopoverContent className="w-40 p-2">
<div className="flex flex-col gap-2">
<Button
variant="ghost"
size="sm"
onClick={bringToFront}
disabled={objectPosition.isAtFront}
>
<BringToFront className="h-4 w-4 mr-2" />
<span className="text-sm">Bring to Front</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={sendToBack}
disabled={objectPosition.isAtBack}
>
<SendToBack className="h-4 w-4 mr-2" />
<span className="text-sm">Send to Back</span>
</Button>
</div>
</PopoverContent>
</Popover>
<ActionButton
icon={<Trash2 className="h-4 w-4" />}
@ -188,15 +285,19 @@ export const ObjectShortcut = ({ value }) => {
</div>
</TooltipProvider>
</div>
)
}
);
};
function ActionButton({ icon, label, onClick, tooltipContent }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="md" className="w-full" onClick={onClick}>
<Button
variant="outline"
size="md"
className="w-full"
onClick={onClick}
>
<div className="flex items-center gap-1 p-1">
{icon}
<span className="text-[10px] font-bold">{label}</span>
@ -207,5 +308,5 @@ function ActionButton({ icon, label, onClick, tooltipContent }) {
<p>{tooltipContent}</p>
</TooltipContent>
</Tooltip>
)
);
}

View file

@ -19,7 +19,7 @@ const ColorPanel = () => {
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] xl:h-[calc(100vh-190px)] px-4 py-4">
<ScrollArea className="lg:h-[calc(90vh-190px)] px-4 py-4">
<ApplyColor />
</ScrollArea>
</div>

View file

@ -20,7 +20,7 @@ const EditorPanel = () => {
return (
<>
{selectedPanel !== "" && (
<div className="w-80 lg:h-[calc(90vh-100px)] xl:h-[calc(100vh-100px)] bg-background rounded-xl shadow-lg mx-4 my-auto">
<div className="w-80 lg:h-[calc(90vh-100px)] bg-background rounded-xl shadow-lg mx-4 my-auto">
{renderPanel()}
</div>
)}

View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import ApplyColor from "../EachComponent/ApplyColor";
import { Button } from "../ui/button";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { X } from "lucide-react";
import { ScrollArea } from "../ui/scroll-area";
const ColorPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Color</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] xl:h-[calc(100vh-190px)] px-4 py-4">
<ApplyColor />
</ScrollArea>
</div>
);
};
export default ColorPanel;

View file

@ -20,6 +20,7 @@ export default function TextPanel() {
setOpen(false);
}
}, [activeObject]);
const addText = () => {
if (canvas) {
const text = new fabric.IText("Editable Text", {
@ -27,6 +28,8 @@ export default function TextPanel() {
top: 100,
fontFamily: "Poppins",
fontSize: 16,
stroke: "", // empty string for no stroke color
strokeWidth: 0, // set stroke width to 0
});
// Add the text to the canvas and re-render
canvas.add(text);
@ -50,7 +53,7 @@ export default function TextPanel() {
</Button>
</div>
<ScrollArea className="lg:h-[calc(90vh-190px)] xl:h-[calc(100vh-190px)] px-4 py-4">
<ScrollArea className="lg:h-[calc(90vh-190px)] 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={() => {

View file

@ -1,47 +1,35 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/Select";
import TextCustomization from "../EachComponent/Customization/TextCustomization";
import LockObject from "../EachComponent/Customization/LockObject";
import { ScrollArea, ScrollBar } from "../ui/scroll-area";
import OpacityCustomization from "../EachComponent/Customization/OpacityCustomization";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { useContext } from "react";
import { ObjectShortcut } from "../ObjectShortcut";
export function TopBar() {
const { selectedPanel } = useContext(CanvasContext);
return (
<div>
<ScrollArea
className={`absolute top-2 lg:left-[20%] ${
className={`!absolute top-2 -translate-x-1/2 z-40 h-28 ${
selectedPanel !== ""
? "lg:w-[600px] xl:w-[820px] xl:left-[40%]"
: "w-[70%] xl:left-[50%]"
} -translate-x-1/2 z-40 scrollbar-hide`}
? "w-[500px] lg:w-[600px] xl:w-[900px] lg:left-[10%] xl:left-[25%] 2xl:left-[50%]"
: "w-[500px] lg:w-[600px] xl:w-[900px] lg:left-[41%] xl:left-[50%]"
} `}
>
<div className="bg-white rounded-[16px] shadow-sm px-4 py-2 flex items-center gap-2">
<div className="bg-white shadow-sm mx-auto px-4 py-2 flex justify-center items-center gap-2">
<div>
<TextCustomization />
</div>
<OpacityCustomization />
<div className="h-4 w-px bg-border mx-2" />
<Select defaultValue="position">
<SelectTrigger className="w-[100px] h-9">
<SelectValue placeholder="Position" />
</SelectTrigger>
<SelectContent>
<SelectItem value="position">Position</SelectItem>
<SelectItem value="front">Bring to Front</SelectItem>
<SelectItem value="back">Send to Back</SelectItem>
</SelectContent>
</Select>
<LockObject />
<div>
<ObjectShortcut value={"default"} />
</div>
<div className="ml-4">
<LockObject />
</div>
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>

View file

@ -1,38 +1,43 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] p-1">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollArea = React.forwardRef(
({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] p-1">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
);
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
const ScrollBar = React.forwardRef(
({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && " border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
" flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
);
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar }
export { ScrollArea, ScrollBar };

View file

@ -1,5 +1,5 @@
import scrollbar from 'tailwind-scrollbar';
import scrollbarHide from 'tailwind-scrollbar-hide';
import scrollbar from "tailwind-scrollbar";
import scrollbarHide from "tailwind-scrollbar-hide";
/** @type {import('tailwindcss').Config} */
export default {
@ -9,66 +9,56 @@ export default {
extend: {
textColor: {
primary: {
DEFAULT: 'hsl(var(--primary-text))',
foreground: 'hsl(var(--primary-foreground))',
DEFAULT: "hsl(var(--primary-text))",
foreground: "hsl(var(--primary-foreground))",
},
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
selection: {
DEFAULT: 'hsl(var(--selection))',
foreground: 'hsl(var(--selection-foreground))',
DEFAULT: "hsl(var(--selection))",
foreground: "hsl(var(--selection-foreground))",
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [
scrollbar,
scrollbarHide,
],
plugins: [scrollbar, scrollbarHide],
};