complete text and group,position duplcate feature in topbar
This commit is contained in:
parent
79ca662a08
commit
8e6637f7fb
14 changed files with 1205 additions and 499 deletions
491
package-lock.json
generated
491
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
20
src/App.css
20
src/App.css
|
|
@ -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 */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
29
src/components/Panel/StrokePanel.jsx
Normal file
29
src/components/Panel/StrokePanel.jsx
Normal 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;
|
||||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue