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-icons": "^5.4.0",
"react-image-file-resizer": "^0.4.8", "react-image-file-resizer": "^0.4.8",
"react-rnd": "^10.4.13", "react-rnd": "^10.4.13",
"react-tooltip": "^5.28.0",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.4",
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
@ -54,6 +55,7 @@
"eslint-plugin-react-refresh": "^0.4.14", "eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0", "globals": "^15.11.0",
"postcss": "^8.4.48", "postcss": "^8.4.48",
"sass-embedded": "^1.83.4",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"vite": "^5.4.10" "vite": "^5.4.10"
} }
@ -378,6 +380,13 @@
"node": ">=6.9.0" "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": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "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": "^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": { "node_modules/call-bind": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@ -3965,6 +3981,12 @@
"url": "https://polar.sh/cva" "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": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -4002,6 +4024,13 @@
"color-support": "bin.js" "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": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -5458,6 +5487,13 @@
"node": ">= 4" "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": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "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": { "node_modules/react-window": {
"version": "1.8.10", "version": "1.8.10",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz",
@ -7474,6 +7524,16 @@
"queue-microtask": "^1.2.2" "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": { "node_modules/safe-array-concat": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
@ -7539,6 +7599,407 @@
"license": "MIT", "license": "MIT",
"optional": true "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": { "node_modules/saxes": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
@ -7985,6 +8446,29 @@
"license": "MIT", "license": "MIT",
"optional": true "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": { "node_modules/tailwind-merge": {
"version": "2.5.5", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz",
@ -8396,6 +8880,13 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "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": { "node_modules/vite": {
"version": "5.4.11", "version": "5.4.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",

View file

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

View file

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

View file

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

View file

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

View file

@ -19,7 +19,7 @@ const ColorPanel = () => {
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</div> </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 /> <ApplyColor />
</ScrollArea> </ScrollArea>
</div> </div>

View file

@ -20,7 +20,7 @@ const EditorPanel = () => {
return ( return (
<> <>
{selectedPanel !== "" && ( {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()} {renderPanel()}
</div> </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); setOpen(false);
} }
}, [activeObject]); }, [activeObject]);
const addText = () => { const addText = () => {
if (canvas) { if (canvas) {
const text = new fabric.IText("Editable Text", { const text = new fabric.IText("Editable Text", {
@ -27,6 +28,8 @@ export default function TextPanel() {
top: 100, top: 100,
fontFamily: "Poppins", fontFamily: "Poppins",
fontSize: 16, 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 // Add the text to the canvas and re-render
canvas.add(text); canvas.add(text);
@ -50,7 +53,7 @@ export default function TextPanel() {
</Button> </Button>
</div> </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 <Button
className="w-full bg-[#FF2B85] hover:bg-[#FF2B85] text-white rounded-[10px] mb-6 h-12 font-medium text-xl" className="w-full bg-[#FF2B85] hover:bg-[#FF2B85] text-white rounded-[10px] mb-6 h-12 font-medium text-xl"
onClick={() => { onClick={() => {

View file

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

View file

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

View file

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