[FEAT] Improved UI
This commit is contained in:
476
package-lock.json
generated
476
package-lock.json
generated
@@ -1390,9 +1390,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
|
||||
"integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
|
||||
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1403,9 +1403,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
|
||||
"integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1416,9 +1416,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
|
||||
"integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1429,9 +1429,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
|
||||
"integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
|
||||
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1442,9 +1442,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
|
||||
"integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1455,9 +1455,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
|
||||
"integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
|
||||
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1468,9 +1468,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
|
||||
"integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
|
||||
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1481,9 +1481,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
|
||||
"integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
|
||||
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1494,9 +1494,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1507,9 +1507,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz",
|
||||
"integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1520,9 +1520,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1533,9 +1533,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1546,9 +1546,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1559,9 +1559,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
|
||||
"integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1572,9 +1572,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1585,9 +1585,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1598,9 +1598,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
|
||||
"integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1611,9 +1611,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
|
||||
"integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1624,9 +1624,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
|
||||
"integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1637,9 +1637,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
|
||||
"integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1650,9 +1650,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1663,9 +1663,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
|
||||
"integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2172,39 +2172,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
|
||||
"integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
|
||||
"integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.25",
|
||||
"entities": "^4.5.0",
|
||||
"@vue/shared": "3.5.26",
|
||||
"entities": "^7.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz",
|
||||
"integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz",
|
||||
"integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-core": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz",
|
||||
"integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
|
||||
"integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25",
|
||||
"@vue/compiler-core": "3.5.26",
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/compiler-ssr": "3.5.26",
|
||||
"@vue/shared": "3.5.26",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.6",
|
||||
@@ -2212,13 +2212,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz",
|
||||
"integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz",
|
||||
"integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-vue2": {
|
||||
@@ -2328,53 +2328,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz",
|
||||
"integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz",
|
||||
"integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz",
|
||||
"integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz",
|
||||
"integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/reactivity": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz",
|
||||
"integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz",
|
||||
"integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/runtime-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25",
|
||||
"csstype": "^3.1.3"
|
||||
"@vue/reactivity": "3.5.26",
|
||||
"@vue/runtime-core": "3.5.26",
|
||||
"@vue/shared": "3.5.26",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz",
|
||||
"integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz",
|
||||
"integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-ssr": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.25"
|
||||
"vue": "3.5.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz",
|
||||
"integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz",
|
||||
"integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/tsconfig": {
|
||||
@@ -2505,9 +2505,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.9",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.9.tgz",
|
||||
"integrity": "sha512-V8fbOCSeOFvlDj7LLChUcqbZrdKD9RU/VR260piF1790vT0mfLSwGc/Qzxv3IqiTukOpNtItePa0HBpMAj7MDg==",
|
||||
"version": "2.9.11",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -2628,9 +2628,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001760",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
|
||||
"integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
|
||||
"version": "1.0.30001762",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
|
||||
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2854,9 +2854,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz",
|
||||
"integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
@@ -3028,9 +3028,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
|
||||
"integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==",
|
||||
"version": "11.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
||||
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4181,9 +4181,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
|
||||
"integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
|
||||
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -4196,28 +4196,28 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.53.5",
|
||||
"@rollup/rollup-android-arm64": "4.53.5",
|
||||
"@rollup/rollup-darwin-arm64": "4.53.5",
|
||||
"@rollup/rollup-darwin-x64": "4.53.5",
|
||||
"@rollup/rollup-freebsd-arm64": "4.53.5",
|
||||
"@rollup/rollup-freebsd-x64": "4.53.5",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.53.5",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.53.5",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.53.5",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-x64-musl": "4.53.5",
|
||||
"@rollup/rollup-openharmony-arm64": "4.53.5",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.53.5",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.53.5",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.53.5",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.53.5",
|
||||
"@rollup/rollup-android-arm-eabi": "4.54.0",
|
||||
"@rollup/rollup-android-arm64": "4.54.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.54.0",
|
||||
"@rollup/rollup-darwin-x64": "4.54.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.54.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.54.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.54.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.54.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.54.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.54.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.54.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -4251,9 +4251,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.0.tgz",
|
||||
"integrity": "sha512-KR0igP1z4avUJetEuIeOdDlwaUDvkH8wSx7FdSjyYBS3dpyX3TzHfAMO0G1Q4/3cdjcmi3r7idh+KCmKqS+KeQ==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
|
||||
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -4272,9 +4272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.97.0.tgz",
|
||||
"integrity": "sha512-Unwu0MtlAt9hQGHutB2NJhwhPcxiJX99AI7PSz7W4lkikQg9S/HYFtgxtIjpTB4DW7sOYX2xnxvtU/nep9HXTA==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.97.1.tgz",
|
||||
"integrity": "sha512-wH3CbOThHYGX0bUyqFf7laLKyhVWIFc2lHynitkqMIUCtX2ixH9mQh0bN7+hkUu5BFt/SXvEMjFbkEbBMpQiSQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4294,30 +4294,30 @@
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sass-embedded-all-unknown": "1.97.0",
|
||||
"sass-embedded-android-arm": "1.97.0",
|
||||
"sass-embedded-android-arm64": "1.97.0",
|
||||
"sass-embedded-android-riscv64": "1.97.0",
|
||||
"sass-embedded-android-x64": "1.97.0",
|
||||
"sass-embedded-darwin-arm64": "1.97.0",
|
||||
"sass-embedded-darwin-x64": "1.97.0",
|
||||
"sass-embedded-linux-arm": "1.97.0",
|
||||
"sass-embedded-linux-arm64": "1.97.0",
|
||||
"sass-embedded-linux-musl-arm": "1.97.0",
|
||||
"sass-embedded-linux-musl-arm64": "1.97.0",
|
||||
"sass-embedded-linux-musl-riscv64": "1.97.0",
|
||||
"sass-embedded-linux-musl-x64": "1.97.0",
|
||||
"sass-embedded-linux-riscv64": "1.97.0",
|
||||
"sass-embedded-linux-x64": "1.97.0",
|
||||
"sass-embedded-unknown-all": "1.97.0",
|
||||
"sass-embedded-win32-arm64": "1.97.0",
|
||||
"sass-embedded-win32-x64": "1.97.0"
|
||||
"sass-embedded-all-unknown": "1.97.1",
|
||||
"sass-embedded-android-arm": "1.97.1",
|
||||
"sass-embedded-android-arm64": "1.97.1",
|
||||
"sass-embedded-android-riscv64": "1.97.1",
|
||||
"sass-embedded-android-x64": "1.97.1",
|
||||
"sass-embedded-darwin-arm64": "1.97.1",
|
||||
"sass-embedded-darwin-x64": "1.97.1",
|
||||
"sass-embedded-linux-arm": "1.97.1",
|
||||
"sass-embedded-linux-arm64": "1.97.1",
|
||||
"sass-embedded-linux-musl-arm": "1.97.1",
|
||||
"sass-embedded-linux-musl-arm64": "1.97.1",
|
||||
"sass-embedded-linux-musl-riscv64": "1.97.1",
|
||||
"sass-embedded-linux-musl-x64": "1.97.1",
|
||||
"sass-embedded-linux-riscv64": "1.97.1",
|
||||
"sass-embedded-linux-x64": "1.97.1",
|
||||
"sass-embedded-unknown-all": "1.97.1",
|
||||
"sass-embedded-win32-arm64": "1.97.1",
|
||||
"sass-embedded-win32-x64": "1.97.1"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-all-unknown": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.97.0.tgz",
|
||||
"integrity": "sha512-9F6MyQcwp3YiuGMk5bC7g9jL+D1KkW/ONQgrkoTQ7ALcmoPKmsauZg5WgRhLYW9UhpnGTgANrWrZdiREAR1YkA==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.97.1.tgz",
|
||||
"integrity": "sha512-0au5gUNibfob7W/g+ycBx74O22CL8vwHiZdEDY6J0uzMkHPiSJk//h0iRf5AUnMArFHJjFd3urIiQIaoRKYa1Q==",
|
||||
"cpu": [
|
||||
"!arm",
|
||||
"!arm64",
|
||||
@@ -4327,13 +4327,13 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"sass": "1.97.0"
|
||||
"sass": "1.97.1"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-android-arm": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.97.0.tgz",
|
||||
"integrity": "sha512-VLxeVR5FMwSZoOliBY8Qy2trZCWYz3w4ILf0QZ68eep3mIQjtykY3BSKC2R/w9DkPQDNJXdgbgnxeOubC8k5xw==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.97.1.tgz",
|
||||
"integrity": "sha512-B5dlv4utJ+yC8ZpBeWTHwSZPVKRlqA8pcaD0FAzeNm/DelIFgQUQtt0UwgYoAI6wDIiie5uSVpMK9l2DaCbiBQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4347,9 +4347,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-android-arm64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.97.0.tgz",
|
||||
"integrity": "sha512-uDG/0DS6A+KRiOYUV1UNHBq67DHvO+/54Ja+dg8S5fl5uvPwZGHpJFheemA9R6vvddwyjGmzVacvCQxdmECcfQ==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.97.1.tgz",
|
||||
"integrity": "sha512-h62DmOiS2Jn87s8+8GhJcMerJnTKa1IsIa9iIKjLiqbAvBDKCGUs027RugZkM+Zx7I+vhPq86PUXBYZ9EkRxdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4363,9 +4363,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-android-riscv64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.97.0.tgz",
|
||||
"integrity": "sha512-yrwsyPR08CXW5Ggr0kI1jTUcKkBOtjODbDj11nRrBwyrXRqhf1obqfchQxTW0HlYT8VZmZGfnHvPNNDwOSdfZg==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.97.1.tgz",
|
||||
"integrity": "sha512-tGup88vgaXPnUHEgDMujrt5rfYadvkiVjRb/45FJTx2hQFoGVbmUXz5XqUFjIIbEjQ3kAJqp86A2jy11s43UiQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -4379,9 +4379,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-android-x64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.97.0.tgz",
|
||||
"integrity": "sha512-a1QW1pFykLCtV8J3AZ+wtrwOx0ORZsW4orF6fOrBYL2sLhlzhB3iK+QzWezFvH5+FMgLQBC4xgYYk4NV9WCO9g==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.97.1.tgz",
|
||||
"integrity": "sha512-CAzKjjzu90LZduye2O9+UGX1oScMyF5/RVOa5CxACKALeIS+3XL3LVdV47kwKPoBv5B1aFUvGLscY0CR7jBAbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4395,9 +4395,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-darwin-arm64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.97.0.tgz",
|
||||
"integrity": "sha512-5XV42FEqhQEGFQ/w8HUk///k0XMHLyBt1j2alxTr9ZI77HqiAIl6kVZp0kxJ++gt/y3E6hKoMLngHHC6zIBR5A==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.97.1.tgz",
|
||||
"integrity": "sha512-tyDzspzh5PbqdAFGtVKUXuf0up6Lff3c1U8J7+4Y7jW6AWRBnq95vTzIIxfnNifGCTI2fW5e7GAZpYygKpNwcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4411,9 +4411,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-darwin-x64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.97.0.tgz",
|
||||
"integrity": "sha512-Kc0aKFfKPd/kz8mSGtRKTEN7FKnqs30iZf6APb0ZHMuvMVfOfdD+fZ/85htT+j5k2F+UUSFBpbx04W0gZW020A==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.97.1.tgz",
|
||||
"integrity": "sha512-FMrRuSPI2ICt2M2SYaLbiG4yxn86D6ae+XtrRdrrBMhWprAcB7Iyu67bgRzZkipMZNIKKeTR7EUvJHgZzi5ixQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4427,9 +4427,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-arm": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.97.0.tgz",
|
||||
"integrity": "sha512-pwM5A1+w3l1T/FXwJNqZD0WukCENeRkgxPSpZmsO4/QNLdTpGCz16D5spYPQ7f7GZo9aNaHt1EaDLHCjlEA8LQ==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.97.1.tgz",
|
||||
"integrity": "sha512-48VxaTUApLyx1NXFdZhKqI/7FYLmz8Ju3Ki2V/p+mhn5raHgAiYeFgn8O1WGxTOh+hBb9y3FdSR5a8MNTbmKMQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4443,9 +4443,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-arm64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.97.0.tgz",
|
||||
"integrity": "sha512-ofm9esPA9P0sB6wJPcDhQYjSDfa7RoVKD0IHvFPMrK9OLTKg8lw80/afH49a9URYeYiE4wFP76Fr9t+s7A6E1Q==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.97.1.tgz",
|
||||
"integrity": "sha512-im80gfDWRivw9Su3r3YaZmJaCATcJgu3CsCSLodPk1b1R2+X/E12zEQayvrl05EGT9PDwTtuiqKgS4ND4xjwVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4459,9 +4459,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-musl-arm": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.97.0.tgz",
|
||||
"integrity": "sha512-+rsW0OreW4sPtdXDewDESxJLJdxW3B0EL7ICajkRFs3KbeNdgOVnP5DJQ39hquAoZH0AcEEGcd6236ZMMzEbwQ==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.97.1.tgz",
|
||||
"integrity": "sha512-FUFs466t3PVViVOKY/60JgLLtl61Pf7OW+g5BeEfuqVcSvYUECVHeiYHtX1fT78PEVa0h9tHpM6XpWti+7WYFA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -4475,9 +4475,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-musl-arm64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.97.0.tgz",
|
||||
"integrity": "sha512-8VF4nc7oUklhUGGAY0T6Ktd9T9ZFwoOsWje7ocOV57tjbocFp/eeAPqX1v2BpiZtMVURyYwaZsRSAL79DT7oRw==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.97.1.tgz",
|
||||
"integrity": "sha512-kD35WSD9o0279Ptwid3Jnbovo1FYnuG2mayYk9z4ZI4mweXEK6vTu+tlvCE/MdF/zFKSj11qaxaH+uzXe2cO5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4491,9 +4491,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-musl-riscv64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.97.0.tgz",
|
||||
"integrity": "sha512-nlaeeZ5P7tde/c/aMiIl5UduQZPA9ftEyWJxdmWcs3pASFSykslVJR5D4L161EUHzB5z+MxSnbbzcrck0F1slA==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.97.1.tgz",
|
||||
"integrity": "sha512-ZgpYps5YHuhA2+KiLkPukRbS5298QObgUhPll/gm5i0LOZleKCwrFELpVPcbhsSBuxqji2uaag5OL+n3JRBVVg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -4507,9 +4507,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-musl-x64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.97.0.tgz",
|
||||
"integrity": "sha512-QB6JLr2p1UuEXhiTXEYNypf+w2x/SCMY17vcnXKM47CeaJ88v2C9fJ9oVne6eZntlCylSow/vZCov0JMhklknA==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.97.1.tgz",
|
||||
"integrity": "sha512-wcAigOyyvZ6o1zVypWV7QLZqpOEVnlBqJr9MbpnRIm74qFTSbAEmShoh8yMXBymzuVSmEbThxAwW01/TLf62tA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4523,9 +4523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-riscv64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.97.0.tgz",
|
||||
"integrity": "sha512-m7QaK4M+YhQ6FZWMI9O8g4tqmM4JrvzJl/YC/eEJXpfgwxMeXsDsPVQWFiBdWOuxqMSH8WhFksw/Bg0J+kK6VQ==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.97.1.tgz",
|
||||
"integrity": "sha512-9j1qE1ZrLMuGb+LUmBzw93Z4TNfqlRkkxjPVZy6u5vIggeSfvGbte7eRoYBNWX6SFew/yBCL90KXIirWFSGrlQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -4539,9 +4539,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-x64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.97.0.tgz",
|
||||
"integrity": "sha512-yc7yLWJrAtTBCjEAoNxvE040EGYdsgmaWMSyI9LSIOFlSwrOc4x+W/8IMhLWCygTAgorNPuNlRfPDgkQm1sJmw==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.97.1.tgz",
|
||||
"integrity": "sha512-7nrLFYMH/UgvEgXR5JxQJ6y9N4IJmnFnYoDxN0nw0jUp+CQWQL4EJ4RqAKTGelneueRbccvt2sEyPK+X0KJ9Jg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4555,9 +4555,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-unknown-all": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.97.0.tgz",
|
||||
"integrity": "sha512-dDky3ETKeOo543myScL4sp3pj2cANLNKea5aR6v8ZCpDSCDTRxqv4Sj/goTmkVqnp/HOVF88qB3GHtQ8rFtULQ==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.97.1.tgz",
|
||||
"integrity": "sha512-oPSeKc7vS2dx3ZJHiUhHKcyqNq0GWzAiR8zMVpPd/kVMl5ZfVyw+5HTCxxWDBGkX02lNpou27JkeBPCaneYGAQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4567,13 +4567,13 @@
|
||||
"!win32"
|
||||
],
|
||||
"dependencies": {
|
||||
"sass": "1.97.0"
|
||||
"sass": "1.97.1"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-win32-arm64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.97.0.tgz",
|
||||
"integrity": "sha512-JMU2SKIgUJDw4oaKBcVbuobWRU6f2XmFuYqJdkxJhlITAGimwjZ860gttlzjNtZcVI4+p4ovT14HwpsEcIzfnw==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.97.1.tgz",
|
||||
"integrity": "sha512-L5j7J6CbZgHGwcfVedMVpM3z5MYeighcyZE8GF2DVmjWzZI3JtPKNY11wNTD/P9o1Uql10YPOKhGH0iWIXOT7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -4587,9 +4587,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-win32-x64": {
|
||||
"version": "1.97.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.97.0.tgz",
|
||||
"integrity": "sha512-mKIJGXxEl6OoWEoT4ee5OsBOfExla2ilY5J8tupVwSCxf/i3aOJNLm7ZzRWG9er2K3bC8aovgMisMIVGlBM5hw==",
|
||||
"version": "1.97.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.97.1.tgz",
|
||||
"integrity": "sha512-rfaZAKXU8cW3E7gvdafyD6YtgbEcsDeT99OEiHXRT0UGFuXT8qCOjpAwIKaOA3XXr2d8S42xx6cXcaZ1a+1fgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5200,16 +5200,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
||||
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
||||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-sfc": "3.5.25",
|
||||
"@vue/runtime-dom": "3.5.25",
|
||||
"@vue/server-renderer": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/compiler-sfc": "3.5.26",
|
||||
"@vue/runtime-dom": "3.5.26",
|
||||
"@vue/server-renderer": "3.5.26",
|
||||
"@vue/shared": "3.5.26"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [2.10.0] - 2026-01-01
|
||||
- Improved the whole UI. Happy new year!
|
||||
|
||||
## [2.0.0] - 2025-12-17
|
||||
- Add rotate sprite to context menu
|
||||
- Add flip sprite to context menu
|
||||
|
||||
99
src/App.vue
99
src/App.vue
@@ -1,67 +1,36 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col p-4 sm:p-8 bg-slate-50 dark:bg-gray-950 transition-colors duration-300" :class="{ 'lg:h-screen': layers.some(l => l.sprites.length) && $route.name === 'home' }">
|
||||
<!-- Decorative gradient blur in top-right corner -->
|
||||
<div class="fixed top-0 right-0 w-[600px] h-[600px] bg-gradient-to-bl from-blue-400/40 via-purple-400/30 to-transparent dark:from-blue-400/30 dark:via-purple-400/20 blur-3xl pointer-events-none -translate-y-32 translate-x-32"></div>
|
||||
<div class="flex flex-col flex-1" :class="{ 'lg:overflow-hidden': layers.some(l => l.sprites.length) && $route.name === 'home' }">
|
||||
<header class="mb-6 sm:mb-8">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-6 mb-6">
|
||||
<div class="text-center sm:text-left w-full sm:w-auto">
|
||||
<router-link to="/" class="block group" title="Spritesheet Generator - Create professional spritesheets" aria-label="Go to homepage">
|
||||
<span class="inline-block text-3xl sm:text-4xl lg:text-5xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 tracking-tight mb-2 sm:mb-3 group-hover:opacity-80 transition-opacity">Spritesheet generator</span>
|
||||
</router-link>
|
||||
<p class="text-sm sm:text-base text-gray-600 dark:text-gray-400 font-medium">Create professional spritesheets for your game development projects</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center sm:items-end gap-3 w-full sm:w-auto">
|
||||
<nav class="flex flex-wrap items-center justify-center sm:justify-end gap-2 sm:gap-3">
|
||||
<a href="https://gitea.adhd.sh/root/spritesheet-generator" target="_blank" rel="noopener noreferrer" class="btn btn-secondary hover:shadow-md" data-rybbit-event="source-link" title="View source code on Gitea" aria-label="View source code repository">
|
||||
<i class="fab fa-github"></i>
|
||||
<span class="font-medium">Source</span>
|
||||
</a>
|
||||
<a href="https://discord.gg/JTev3nzeDa" target="_blank" rel="noopener noreferrer" class="btn btn-secondary hover:shadow-md" data-rybbit-event="discord-link" title="Join our Discord community" aria-label="Join Discord server">
|
||||
<i class="fab fa-discord"></i>
|
||||
<span class="font-medium">Discord</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="openHelpModal" class="btn btn-secondary hover:shadow-md" data-rybbit-event="help-link" title="Get help and documentation" aria-label="Open help modal">
|
||||
<i class="fas fa-question-circle"></i>
|
||||
<span class="font-medium">Help</span>
|
||||
</a>
|
||||
<a href="#" @click.prevent="openFeedbackModal" class="btn btn-secondary hover:shadow-md" data-rybbit-event="feedback-link" title="Share your feedback with us" aria-label="Open feedback modal">
|
||||
<i class="fas fa-comment-dots"></i>
|
||||
<span class="font-medium">Feedback</span>
|
||||
</a>
|
||||
<dark-mode-toggle />
|
||||
</nav>
|
||||
<div class="flex flex-wrap items-center justify-center sm:justify-end gap-3 sm:gap-4 text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
<router-link to="/" class="hover:text-gray-900 dark:hover:text-white transition-colors whitespace-nowrap" title="Spritesheet Generator Home" aria-label="Navigate to home page">Home</router-link>
|
||||
<router-link to="/blog" class="hover:text-gray-900 dark:hover:text-white transition-colors whitespace-nowrap" title="Read our blog posts" aria-label="Navigate to blog">Blog</router-link>
|
||||
<router-link to="/about" class="hover:text-gray-900 dark:hover:text-white transition-colors whitespace-nowrap" title="Learn more about us" aria-label="Navigate to about page">About Us</router-link>
|
||||
<router-link to="/contact" class="hover:text-gray-900 dark:hover:text-white transition-colors whitespace-nowrap" title="Get in touch with us" aria-label="Navigate to contact page">Contact</router-link>
|
||||
<router-link to="/faq" class="hover:text-gray-900 dark:hover:text-white transition-colors whitespace-nowrap" title="Frequently Asked Questions" aria-label="Navigate to FAQ page">FAQ</router-link>
|
||||
<router-link to="/privacy-policy" class="hover:text-gray-900 dark:hover:text-white transition-colors whitespace-nowrap" title="Read our privacy policy" aria-label="Navigate to privacy policy">Privacy Policy</router-link>
|
||||
<a href="/sitemap.xml" target="_blank" rel="noopener noreferrer" class="hover:text-gray-900 dark:hover:text-white transition-colors whitespace-nowrap" title="View XML sitemap" aria-label="View sitemap">Sitemap</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="min-h-screen flex flex-col bg-gray-50 dark:bg-gray-950 transition-colors duration-300 font-sans" :class="{ 'lg:h-screen': layers.some(l => l.sprites.length) && $route.name === 'home' }">
|
||||
<!-- Navbar -->
|
||||
<Navbar @open-help="openHelpModal" />
|
||||
|
||||
<Breadcrumbs />
|
||||
<div class="flex flex-col flex-1 relative z-10 p-4 sm:p-6 lg:p-8 pt-6" :class="{ 'lg:overflow-hidden': layers.some(l => l.sprites.length) && $route.name === 'home' }">
|
||||
<Breadcrumbs class="mb-6" />
|
||||
|
||||
<router-view />
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
|
||||
<HelpModal :is-open="isHelpModalOpen" @close="closeHelpModal" />
|
||||
<FeedbackModal :is-open="isFeedbackModalOpen" @close="closeFeedbackModal" />
|
||||
|
||||
<!-- One-time feedback popup -->
|
||||
<div v-if="showFeedbackPopup" class="fixed inset-0 z-50 flex items-center justify-center backdrop-blur-sm">
|
||||
<div class="max-w-md p-6 mx-4 bg-white dark:bg-gray-800 border border-gray-600 rounded-xl shadow-xl">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-4xl">💬</div>
|
||||
<h3 class="mb-3 text-lg font-semibold text-gray-900 dark:text-white">Help us improve!</h3>
|
||||
<p class="mb-6 text-gray-600 dark:text-gray-300">We'd love to hear your thoughts about the Spritesheet generator. Would you like to share your feedback?</p>
|
||||
<div class="flex justify-center gap-3">
|
||||
<button @click="handleFeedbackPopupResponse(false)" class="px-4 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors cursor-pointer">Maybe later</button>
|
||||
<button @click="handleFeedbackPopupResponse(true)" class="px-6 py-2 font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg transition-colors cursor-pointer">Share feedback</button>
|
||||
<div v-if="showFeedbackPopup" class="fixed bottom-6 right-6 z-50 flex items-center justify-center">
|
||||
<div class="max-w-sm p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-2xl glass-panel relative">
|
||||
<button @click="handleFeedbackPopupResponse(false)" class="absolute top-2 right-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" title="Close">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
<div class="flex gap-4">
|
||||
<div class="text-3xl">💬</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-gray-900 dark:text-gray-100 mb-1">Help us improve!</h3>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-300 mb-3 leading-relaxed">Enjoying Spritesheet Generator? We'd love your feedback.</p>
|
||||
<div class="flex gap-2">
|
||||
<button @click="handleFeedbackPopupResponse(true)" class="px-3 py-1.5 text-xs font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors shadow-sm">Share feedback</button>
|
||||
<button @click="handleFeedbackPopupResponse(false)" class="px-3 py-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors">No thanks</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,14 +40,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { RouterView, RouterLink } from 'vue-router';
|
||||
import { RouterView } from 'vue-router';
|
||||
import Navbar from './components/layout/Navbar.vue';
|
||||
import HelpModal from './components/HelpModal.vue';
|
||||
import FeedbackModal from './components/FeedbackModal.vue';
|
||||
import DarkModeToggle from './components/utilities/DarkModeToggle.vue';
|
||||
import Breadcrumbs from './components/Breadcrumbs.vue';
|
||||
import { useLayers } from './composables/useLayers';
|
||||
|
||||
const { layers, hasSprites } = useLayers();
|
||||
const { layers } = useLayers();
|
||||
|
||||
const isHelpModalOpen = ref(false);
|
||||
const isFeedbackModalOpen = ref(false);
|
||||
@@ -105,7 +74,7 @@
|
||||
if (!hasShownFeedbackPopup) {
|
||||
setTimeout(() => {
|
||||
showFeedbackPopup.value = true;
|
||||
}, 3000);
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -118,3 +87,15 @@
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* Tailwind v4: Use .dark class for dark mode instead of prefers-color-scheme */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* Dark mode transition */
|
||||
html,
|
||||
/* Global Resets & Transitions */
|
||||
html {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-tap-highlight-color: transparent; /* Remove tap highlight on mobile */
|
||||
@apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100 antialiased font-sans;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
html.theme-transition * {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* Additional dark mode styles */
|
||||
body.dark-mode {
|
||||
background-color: #111827;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* Force dark mode styles */
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
@@ -27,37 +28,90 @@ html.dark {
|
||||
input,
|
||||
select,
|
||||
button {
|
||||
font-size: 16px !important; /* Prevent iOS zoom on focus */
|
||||
font-size: 16px !important;
|
||||
}
|
||||
.touch-manipulation {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
}
|
||||
|
||||
.touch-manipulation {
|
||||
touch-action: manipulation; /* Improve touch responsiveness */
|
||||
/* Global Button Styles */
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
[type='button']:disabled,
|
||||
[type='reset']:disabled,
|
||||
[type='submit']:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Premium Utilities */
|
||||
@layer utilities {
|
||||
.glass {
|
||||
@apply bg-white/70 dark:bg-gray-900/70 backdrop-blur-md border border-white/20 dark:border-gray-800/50;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
@apply bg-white/80 dark:bg-gray-900/80 backdrop-blur-lg border border-gray-200/50 dark:border-gray-700/50;
|
||||
box-shadow:
|
||||
0 2px 15px -3px rgba(0, 0, 0, 0.07),
|
||||
0 10px 20px -2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-300 dark:bg-gray-700 rounded-full transition-colors;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-gray-400 dark:bg-gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Buttons */
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center gap-2 px-4 py-2 text-sm rounded-lg transition-all cursor-pointer font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed active:scale-95;
|
||||
@apply inline-flex items-center justify-center gap-2 px-4 py-2 text-sm rounded-lg transition-all duration-200 cursor-pointer font-medium
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed active:scale-[0.98];
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-gray-900 text-white hover:bg-gray-800 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200;
|
||||
@apply bg-gray-900 text-white hover:bg-black shadow-lg shadow-gray-900/20
|
||||
dark:bg-gray-100 dark:text-gray-950 dark:hover:bg-white dark:shadow-gray-100/10 border border-transparent;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-white text-gray-700 border border-gray-200 hover:bg-gray-50 hover:shadow-sm dark:bg-gray-800 dark:text-gray-200 dark:border-gray-700 dark:hover:bg-gray-700;
|
||||
@apply bg-white text-gray-700 border border-gray-200 hover:bg-gray-50 hover:border-gray-300 shadow-sm
|
||||
dark:bg-gray-800 dark:text-gray-200 dark:border-gray-700 dark:hover:bg-gray-700/80 dark:hover:border-gray-600;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800;
|
||||
@apply text-gray-600 hover:bg-gray-100/80 hover:text-gray-900
|
||||
dark:text-gray-400 dark:hover:bg-gray-800/50 dark:hover:text-gray-200;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/30;
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
@apply bg-gray-700 text-white hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-700;
|
||||
@apply text-red-600 hover:bg-red-50 hover:text-red-700
|
||||
dark:text-red-400 dark:hover:bg-red-900/20 dark:hover:text-red-300;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@@ -72,104 +126,32 @@ html.dark {
|
||||
@apply p-2;
|
||||
}
|
||||
|
||||
.btn-icon-sm {
|
||||
@apply p-1.5;
|
||||
}
|
||||
|
||||
.btn-icon-xs {
|
||||
@apply p-1;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply px-2 py-1 text-sm border border-gray-300 rounded outline-none focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100;
|
||||
@apply px-3 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700
|
||||
rounded-lg outline-none transition-shadow focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500
|
||||
placeholder:text-gray-400 dark:placeholder:text-gray-500 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg;
|
||||
@apply bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700/60 rounded-xl shadow-sm;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom prose styles for blog content */
|
||||
/* Typography Overrides (Prose) */
|
||||
.prose {
|
||||
@apply text-gray-700 dark:text-gray-300 leading-7;
|
||||
@apply text-gray-600 dark:text-gray-300 max-w-none;
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
@apply text-4xl font-bold text-gray-900 dark:text-white mb-4 mt-0;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
@apply text-3xl font-bold text-gray-900 dark:text-white mt-8 mb-4;
|
||||
}
|
||||
|
||||
.prose h1,
|
||||
.prose h2,
|
||||
.prose h3 {
|
||||
@apply text-2xl font-bold text-gray-900 dark:text-white mt-6 mb-3;
|
||||
@apply text-gray-900 dark:text-gray-50 font-bold tracking-tight;
|
||||
}
|
||||
|
||||
.prose h4 {
|
||||
@apply text-xl font-bold text-gray-900 dark:text-white mt-4 mb-2;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
@apply text-gray-700 dark:text-gray-300 mb-4 leading-7;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
@apply text-blue-600 dark:text-blue-400 underline hover:text-blue-800 dark:hover:text-blue-300;
|
||||
@apply text-indigo-600 dark:text-indigo-400 no-underline hover:underline font-medium;
|
||||
}
|
||||
|
||||
.prose strong {
|
||||
@apply text-gray-900 dark:text-white font-semibold;
|
||||
}
|
||||
|
||||
.prose em {
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
@apply text-sm font-mono bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 px-1 py-0.5 rounded;
|
||||
@apply bg-gray-100 dark:bg-gray-800 rounded px-1.5 py-0.5 text-sm text-indigo-600 dark:text-indigo-300 font-mono font-medium before:content-[''] after:content-[''];
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
@apply bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 p-4 rounded-lg overflow-x-auto mb-4;
|
||||
}
|
||||
|
||||
.prose pre code {
|
||||
@apply bg-transparent p-0;
|
||||
}
|
||||
|
||||
.prose ul {
|
||||
@apply list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.prose ol {
|
||||
@apply list-decimal pl-6 mb-4 text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
@apply mb-2 leading-7;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
@apply border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic text-gray-600 dark:text-gray-400 my-4;
|
||||
}
|
||||
|
||||
.prose img {
|
||||
@apply rounded-lg shadow-md my-6;
|
||||
}
|
||||
|
||||
.prose hr {
|
||||
@apply border-gray-300 dark:border-gray-700 my-8;
|
||||
}
|
||||
|
||||
.prose table {
|
||||
@apply border-collapse w-full my-6;
|
||||
}
|
||||
|
||||
.prose th {
|
||||
@apply bg-gray-100 dark:bg-gray-800 p-2 text-left font-semibold border border-gray-300 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.prose td {
|
||||
@apply border border-gray-300 dark:border-gray-700 p-2;
|
||||
}
|
||||
@apply bg-gray-900 dark:bg-gray-950/50 border border-gray-200 dark:border-gray-800 rounded-xl shadow-lg;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative border-3 border-dashed rounded-2xl p-8 sm:p-12 text-center transition-all duration-300 cursor-pointer group overflow-hidden"
|
||||
class="relative border-3 border-dashed rounded-2xl p-8 sm:p-12 text-center cursor-pointer group overflow-hidden"
|
||||
:class="{
|
||||
'border-blue-400 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/40 scale-[1.02]': isDragging,
|
||||
'border-blue-400 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/40': isDragging,
|
||||
'border-gray-300 bg-gray-50/50 hover:border-blue-400 hover:bg-blue-50/80 dark:border-gray-600 dark:bg-gray-800/30 dark:hover:border-blue-400 dark:hover:bg-blue-900/30': !isDragging,
|
||||
}"
|
||||
@dragenter.prevent="isDragging = true"
|
||||
@@ -12,12 +12,12 @@
|
||||
@click="openFileDialog"
|
||||
data-rybbit-event="file-upload-area"
|
||||
>
|
||||
<div class="absolute inset-0 bg-blue-400/0 group-hover:bg-blue-400/5 transition-all duration-300"></div>
|
||||
<div class="absolute inset-0 bg-blue-400/0 group-hover:bg-blue-400/5"></div>
|
||||
|
||||
<input ref="fileInput" type="file" multiple accept="image/*,.json" class="hidden" @change="handleFileChange" />
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="mb-6 transform transition-transform duration-300" :class="isDragging ? 'scale-110' : 'group-hover:scale-105'">
|
||||
<div class="mb-6" :class="isDragging ? 'scale-110' : ''">
|
||||
<div class="w-20 h-20 sm:w-24 sm:h-24 mx-auto mb-4 bg-blue-100 dark:bg-blue-900/50 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<i class="fas fa-cloud-upload-alt text-4xl sm:text-5xl text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="h-px flex-1 bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
|
||||
<button class="px-8 py-3.5 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all transform hover:scale-105 flex items-center justify-center gap-3 mx-auto" data-rybbit-event="select-files">
|
||||
<button class="px-8 py-3.5 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white font-semibold rounded-xl shadow-lg hover:shadow-xl flex items-center justify-center gap-3 mx-auto" data-rybbit-event="select-files">
|
||||
<i class="fas fa-folder-open text-lg"></i>
|
||||
<span>Browse Files</span>
|
||||
</button>
|
||||
|
||||
@@ -29,137 +29,13 @@
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<div class="space-y-3 w-full max-w-full overflow-hidden">
|
||||
<!-- Compact tip banner -->
|
||||
<div class="bg-cyan-50/80 dark:bg-cyan-900/20 rounded-md px-3 py-2 border border-cyan-100 dark:border-cyan-800/50 flex items-center gap-2">
|
||||
<i class="fas fa-lightbulb text-xs text-cyan-500 dark:text-cyan-400"></i>
|
||||
<p class="text-xs text-cyan-700 dark:text-cyan-300"><span class="font-medium">Tip:</span> Right-click sprites for quick actions • Hold Ctrl/Cmd to multi-select • Delete key removes selection</p>
|
||||
</div>
|
||||
|
||||
<!-- Compact Toolbar -->
|
||||
<section class="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div class="flex flex-wrap items-center gap-0.5 px-1.5 py-1">
|
||||
<!-- Selection Tools -->
|
||||
<div class="flex items-center">
|
||||
<Tooltip text="Select multiple sprites at once. Also works with Ctrl/Cmd+Click.">
|
||||
<button
|
||||
@click="isMultiSelectMode = !isMultiSelectMode"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', isMultiSelectMode ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-object-group mr-1"></i>Multi
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show blue borders around selected sprites for visibility.">
|
||||
<button
|
||||
@click="showActiveBorder = !showActiveBorder"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', showActiveBorder ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-vector-square mr-1"></i>Borders
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="w-px h-4 bg-gray-200 dark:bg-gray-600 mx-1"></div>
|
||||
|
||||
<!-- Display Options -->
|
||||
<div class="flex items-center">
|
||||
<Tooltip text="Disable anti-aliasing for crisp pixel art rendering.">
|
||||
<button
|
||||
@click="settingsStore.pixelPerfect = !settingsStore.pixelPerfect"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', settingsStore.pixelPerfect ? 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-th mr-1"></i>Pixel
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Drag sprites between cells to swap their positions.">
|
||||
<button
|
||||
@click="allowCellSwap = !allowCellSwap"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', allowCellSwap ? 'bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-exchange-alt mr-1"></i>Swap
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show ghost overlays of all sprites for alignment comparison.">
|
||||
<button
|
||||
@click="showAllSprites = !showAllSprites"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', showAllSprites ? 'bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-clone mr-1"></i>Compare
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="w-px h-4 bg-gray-200 dark:bg-gray-600 mx-1"></div>
|
||||
|
||||
<!-- Canvas Options -->
|
||||
<div class="flex items-center">
|
||||
<Tooltip text="Add padding around sprites to prevent bleeding artifacts.">
|
||||
<button
|
||||
@click="settingsStore.negativeSpacingEnabled = !settingsStore.negativeSpacingEnabled"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', settingsStore.negativeSpacingEnabled ? 'bg-teal-100 dark:bg-teal-900/40 text-teal-700 dark:text-teal-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-compress-alt mr-1"></i>Spacing
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show checkerboard pattern to visualize transparent areas.">
|
||||
<button
|
||||
@click="settingsStore.checkerboardEnabled = !settingsStore.checkerboardEnabled"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', settingsStore.checkerboardEnabled ? 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-chess-board mr-1"></i>Grid
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Display X,Y offset coordinates on each sprite.">
|
||||
<button
|
||||
@click="showOffsetLabels = !showOffsetLabels"
|
||||
:class="['px-2 py-1 rounded text-xs font-medium transition-all cursor-pointer', showOffsetLabels ? 'bg-cyan-100 dark:bg-cyan-900/40 text-cyan-700 dark:text-cyan-300' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700']"
|
||||
>
|
||||
<i class="fas fa-tag mr-1"></i>Labels
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="w-px h-4 bg-gray-200 dark:bg-gray-600 mx-1"></div>
|
||||
|
||||
<!-- Background Color (Compact) -->
|
||||
<div class="flex items-center gap-1.5 px-1">
|
||||
<span class="text-[10px] font-medium text-gray-400 uppercase">Bg</span>
|
||||
<select v-model="bgSelectValue" class="px-1.5 py-0.5 text-xs border border-gray-200 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 dark:text-gray-200 outline-none cursor-pointer hover:border-gray-300">
|
||||
<option value="transparent">None</option>
|
||||
<option value="#ffffff">White</option>
|
||||
<option value="#000000">Black</option>
|
||||
<option value="#f9fafb">Gray</option>
|
||||
<option value="custom">Pick</option>
|
||||
</select>
|
||||
<div v-if="bgSelectValue === 'custom'" class="relative w-5 h-5 rounded overflow-hidden border border-gray-300 dark:border-gray-600">
|
||||
<input type="color" v-model="customColor" @input="settingsStore.setBackgroundColor(customColor)" class="absolute -top-1 -left-1 w-8 h-8 cursor-pointer p-0 border-0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- Zoom Controls (Compact) -->
|
||||
<div class="flex items-center bg-gray-100 dark:bg-gray-700 rounded px-1 py-0.5">
|
||||
<button @click="zoomOut" class="p-1 hover:bg-white dark:hover:bg-gray-600 text-gray-500 dark:text-gray-400 rounded transition-all cursor-pointer" title="Zoom Out">
|
||||
<i class="fas fa-search-minus text-[10px]"></i>
|
||||
</button>
|
||||
<span class="px-1.5 text-[10px] font-mono text-gray-500 dark:text-gray-400 min-w-[4ch] text-center">{{ Math.round(zoom * 100) }}%</span>
|
||||
<button @click="zoomIn" class="p-1 hover:bg-white dark:hover:bg-gray-600 text-gray-500 dark:text-gray-400 rounded transition-all cursor-pointer" title="Zoom In">
|
||||
<i class="fas fa-search-plus text-[10px]"></i>
|
||||
</button>
|
||||
<button @click="resetZoom" class="p-1 hover:bg-white dark:hover:bg-gray-600 text-gray-500 dark:text-gray-400 rounded transition-all cursor-pointer ml-0.5" title="Reset Zoom">
|
||||
<i class="fas fa-compress-arrows-alt text-[10px]"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="h-full w-full flex flex-col p-4">
|
||||
<div class="relative bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm overflow-auto max-h-[calc(100vh-340px)] min-h-[400px] w-full">
|
||||
<div class="canvas-container touch-manipulation relative inline-block min-w-full">
|
||||
<div
|
||||
ref="gridContainerRef"
|
||||
:style="{
|
||||
transform: `scale(${zoom})`,
|
||||
transform: `scale(${props.zoom})`,
|
||||
transformOrigin: 'top left',
|
||||
width: `${gridDimensions.width}px`,
|
||||
height: `${gridDimensions.height}px`,
|
||||
@@ -294,10 +170,8 @@
|
||||
import { ref, onMounted, watch, onUnmounted, toRef, computed, nextTick } from 'vue';
|
||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||
import type { Sprite } from '@/types/sprites';
|
||||
import { useZoom } from '@/composables/useZoom';
|
||||
import { useDragSprite } from '@/composables/useDragSprite';
|
||||
import { useFileDrop } from '@/composables/useFileDrop';
|
||||
import Tooltip from '@/components/utilities/Tooltip.vue';
|
||||
|
||||
import type { Layer } from '@/types/sprites';
|
||||
|
||||
@@ -305,6 +179,12 @@
|
||||
layers: Layer[];
|
||||
activeLayerId: string;
|
||||
columns: number;
|
||||
zoom: number;
|
||||
isMultiSelectMode: boolean;
|
||||
showActiveBorder: boolean;
|
||||
allowCellSwap: boolean;
|
||||
showAllSprites: boolean;
|
||||
showOffsetLabels: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -324,23 +204,9 @@
|
||||
|
||||
const gridContainerRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const {
|
||||
zoom,
|
||||
increase: zoomIn,
|
||||
decrease: zoomOut,
|
||||
reset: resetZoom,
|
||||
} = useZoom({
|
||||
min: 0.5,
|
||||
max: 3,
|
||||
step: 0.25,
|
||||
initial: 1,
|
||||
});
|
||||
|
||||
const allowCellSwap = ref(false);
|
||||
|
||||
const getMousePosition = (event: MouseEvent, z?: number) => {
|
||||
if (!gridContainerRef.value) return null;
|
||||
const currentZoom = z ?? zoom.value;
|
||||
const currentZoom = z ?? props.zoom;
|
||||
const rect = gridContainerRef.value.getBoundingClientRect();
|
||||
const scaleX = gridContainerRef.value.offsetWidth / (rect.width / currentZoom);
|
||||
const scaleY = gridContainerRef.value.offsetHeight / (rect.height / currentZoom);
|
||||
@@ -367,8 +233,8 @@
|
||||
sprites: computed(() => props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? []),
|
||||
layers: toRef(props, 'layers'),
|
||||
columns: toRef(props, 'columns'),
|
||||
zoom,
|
||||
allowCellSwap,
|
||||
zoom: toRef(props, 'zoom'),
|
||||
allowCellSwap: toRef(props, 'allowCellSwap'),
|
||||
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
|
||||
manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'),
|
||||
manualCellWidth: toRef(settingsStore, 'manualCellWidth'),
|
||||
@@ -389,8 +255,6 @@
|
||||
onAddSpriteWithResize: file => emit('addSpriteWithResize', file),
|
||||
});
|
||||
|
||||
const showAllSprites = ref(false);
|
||||
const showOffsetLabels = ref(false);
|
||||
const showContextMenu = ref(false);
|
||||
const contextMenuX = ref(0);
|
||||
const contextMenuY = ref(0);
|
||||
@@ -398,14 +262,14 @@
|
||||
const selectedSpriteIds = ref<Set<string>>(new Set());
|
||||
const replacingSpriteId = ref<string | null>(null);
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const customColor = ref('#ffffff');
|
||||
const isMultiSelectMode = ref(false);
|
||||
const showActiveBorder = ref(true);
|
||||
|
||||
// Clear selection when toggling multi-select mode
|
||||
watch(isMultiSelectMode, () => {
|
||||
watch(
|
||||
() => props.isMultiSelectMode,
|
||||
() => {
|
||||
selectedSpriteIds.value.clear();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Grid metrics
|
||||
const gridMetrics = computed(() => calculateMaxDimensions());
|
||||
@@ -467,52 +331,6 @@
|
||||
return '0 0';
|
||||
};
|
||||
|
||||
// Background select handling
|
||||
const presetBgColors = ['transparent', '#ffffff', '#000000', '#f9fafb'] as const;
|
||||
const isHexColor = (val: string) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val);
|
||||
|
||||
if (isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any)) {
|
||||
customColor.value = settingsStore.backgroundColor;
|
||||
}
|
||||
|
||||
const isCustomMode = ref(isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any));
|
||||
|
||||
const bgSelectValue = computed<string>({
|
||||
get() {
|
||||
if (isCustomMode.value) {
|
||||
const val = settingsStore.backgroundColor;
|
||||
if (isHexColor(val)) {
|
||||
customColor.value = val;
|
||||
}
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
const val = settingsStore.backgroundColor;
|
||||
if (presetBgColors.includes(val as any)) return val;
|
||||
if (isHexColor(val)) {
|
||||
customColor.value = val;
|
||||
isCustomMode.value = true;
|
||||
return 'custom';
|
||||
}
|
||||
return 'transparent';
|
||||
},
|
||||
set(v: string) {
|
||||
if (v === 'custom') {
|
||||
isCustomMode.value = true;
|
||||
const fallback = '#ffffff';
|
||||
const current = settingsStore.backgroundColor;
|
||||
const fromStore = isHexColor(current) ? current : null;
|
||||
const fromLocal = isHexColor(customColor.value) ? customColor.value : null;
|
||||
const color = fromStore || fromLocal || fallback;
|
||||
customColor.value = color;
|
||||
settingsStore.setBackgroundColor(color);
|
||||
} else {
|
||||
isCustomMode.value = false;
|
||||
settingsStore.setBackgroundColor(v);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const startDrag = (event: MouseEvent) => {
|
||||
// If the click originated from an interactive element (button, link, input), ignore drag handling
|
||||
const target = event.target as HTMLElement;
|
||||
@@ -527,7 +345,7 @@
|
||||
// Handle right-click for context menu
|
||||
if ('button' in event && (event as MouseEvent).button === 2) {
|
||||
event.preventDefault();
|
||||
const pos = getMousePosition(event, zoom.value);
|
||||
const pos = getMousePosition(event, props.zoom);
|
||||
if (!pos) return;
|
||||
|
||||
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
||||
@@ -556,12 +374,12 @@
|
||||
if ('button' in event && (event as MouseEvent).button !== 0) return;
|
||||
|
||||
// Handle selection logic for left click
|
||||
const pos = getMousePosition(event, zoom.value);
|
||||
const pos = getMousePosition(event, props.zoom);
|
||||
if (pos) {
|
||||
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
||||
if (clickedSprite) {
|
||||
// Selection logic with multi-select mode check
|
||||
if (event.ctrlKey || event.metaKey || isMultiSelectMode.value) {
|
||||
if (event.ctrlKey || event.metaKey || props.isMultiSelectMode) {
|
||||
// Toggle selection
|
||||
if (selectedSpriteIds.value.has(clickedSprite.id)) {
|
||||
selectedSpriteIds.value.delete(clickedSprite.id);
|
||||
@@ -707,12 +525,6 @@
|
||||
});
|
||||
|
||||
// Watch for background color changes
|
||||
watch(
|
||||
() => settingsStore.backgroundColor,
|
||||
async () => {
|
||||
await nextTick();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<div
|
||||
ref="previewContainerRef"
|
||||
class="relative touch-manipulation inline-block"
|
||||
:class="{ 'ring-2 ring-blue-500 ring-offset-2': isDragOver }"
|
||||
:style="{
|
||||
transform: `scale(${zoom})`,
|
||||
transformOrigin: 'top left',
|
||||
@@ -24,6 +25,9 @@
|
||||
backgroundPosition: settingsStore.backgroundColor === 'transparent' ? '0 0, 0 10px, 10px -10px, -10px 0px' : '0 0',
|
||||
border: `1px solid ${settingsStore.darkMode ? '#4b5563' : '#e5e7eb'}`,
|
||||
}"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragleave="onDragLeave"
|
||||
@drop.prevent="onDrop"
|
||||
>
|
||||
<!-- Background sprites (dimmed for comparison) -->
|
||||
<template v-if="showAllSprites">
|
||||
@@ -81,6 +85,14 @@
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Drop zone overlay -->
|
||||
<div v-if="isDragOver" class="absolute inset-0 bg-blue-500/20 flex items-center justify-center pointer-events-none z-10 rounded-lg">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg px-4 py-3 flex items-center gap-2 border border-blue-300 dark:border-blue-600">
|
||||
<i class="fas fa-plus-circle text-blue-500"></i>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">Drop to add sprite at frame {{ currentFrameIndex + 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -247,6 +259,7 @@
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateSprite', id: string, x: number, y: number): void;
|
||||
(e: 'updateSpriteInLayer', layerId: string, spriteId: string, x: number, y: number): void;
|
||||
(e: 'dropSprite', layerId: string, frameIndex: number, files: File[]): void;
|
||||
}>();
|
||||
|
||||
const previewContainerRef = ref<HTMLDivElement | null>(null);
|
||||
@@ -283,6 +296,28 @@
|
||||
const isDraggable = ref(false);
|
||||
const repositionAllLayers = ref(false);
|
||||
const showAllSprites = ref(false);
|
||||
const isDragOver = ref(false);
|
||||
|
||||
// Drag and drop for new sprites
|
||||
const onDragOver = () => {
|
||||
isDragOver.value = true;
|
||||
};
|
||||
|
||||
const onDragLeave = () => {
|
||||
isDragOver.value = false;
|
||||
};
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
isDragOver.value = false;
|
||||
|
||||
if (!event.dataTransfer?.files.length) return;
|
||||
|
||||
const files = Array.from(event.dataTransfer.files).filter(file => file.type.startsWith('image/'));
|
||||
|
||||
if (files.length === 0) return;
|
||||
|
||||
emit('dropSprite', props.activeLayerId, currentFrameIndex.value, files);
|
||||
};
|
||||
|
||||
const compositeFrames = computed<Sprite[]>(() => {
|
||||
// Show frames from the active layer for the thumbnail list
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
const cellHeight = ref(64);
|
||||
const sensitivity = ref(50);
|
||||
const removeEmpty = ref(true);
|
||||
const preserveCellSize = ref(false);
|
||||
const preserveCellSize = ref(true);
|
||||
const previewSprites = ref<SpritePreview[]>([]);
|
||||
const isProcessing = ref(false);
|
||||
const imageElement = ref<HTMLImageElement | null>(null);
|
||||
|
||||
87
src/components/layout/Navbar.vue
Normal file
87
src/components/layout/Navbar.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<nav class="sticky top-0 z-50 w-full glass border-b border-gray-200/50 dark:border-gray-800/50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Logo & Brand -->
|
||||
<div class="flex-shrink-0 flex items-center gap-3">
|
||||
<router-link to="/" class="flex items-center gap-2 group">
|
||||
<div class="w-8 h-8 rounded-lg bg-gray-900 dark:bg-white flex items-center justify-center shadow-lg group-hover:scale-105 transition-transform duration-200">
|
||||
<i class="fas fa-layer-group text-white dark:text-gray-900 text-sm"></i>
|
||||
</div>
|
||||
<span class="font-bold text-xl tracking-tight text-gray-900 dark:text-white group-hover:opacity-80 transition-opacity"> Spritesheet generator </span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center gap-6">
|
||||
<div class="flex items-center gap-4 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
<router-link to="/" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Generator</router-link>
|
||||
<router-link to="/blog" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Blog</router-link>
|
||||
<router-link to="/about" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">About</router-link>
|
||||
<router-link to="/faq" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">FAQ</router-link>
|
||||
</div>
|
||||
|
||||
<div class="h-4 w-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="https://gitea.adhd.sh/root/spritesheet-generator" target="_blank" rel="noopener noreferrer" class="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors" title="Source Code">
|
||||
<i class="fab fa-github text-lg"></i>
|
||||
</a>
|
||||
<a href="https://discord.gg/JTev3nzeDa" target="_blank" rel="noopener noreferrer" class="text-gray-500 hover:text-[#5865F2] dark:text-gray-400 dark:hover:text-[#5865F2] transition-colors" title="Discord Community">
|
||||
<i class="fab fa-discord text-lg"></i>
|
||||
</a>
|
||||
<button @click="$emit('open-help')" class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400 transition-colors" title="Help">
|
||||
<i class="fas fa-question-circle text-lg"></i>
|
||||
</button>
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<div class="flex md:hidden">
|
||||
<DarkModeToggle />
|
||||
<button @click="isMobileMenuOpen = !isMobileMenuOpen" class="ml-4 p-2 rounded-md text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
|
||||
<i :class="isMobileMenuOpen ? 'fas fa-times' : 'fas fa-bars'" class="text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div v-show="isMobileMenuOpen" class="md:hidden border-t border-gray-200 dark:border-gray-800 bg-white/95 dark:bg-gray-950/95 backdrop-blur-xl">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||
<router-link to="/" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" @click="isMobileMenuOpen = false">Generator</router-link>
|
||||
<router-link to="/blog" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" @click="isMobileMenuOpen = false">Blog</router-link>
|
||||
<router-link to="/about" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" @click="isMobileMenuOpen = false">About</router-link>
|
||||
<router-link to="/faq" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" @click="isMobileMenuOpen = false">FAQ</router-link>
|
||||
<div class="border-t border-gray-200 dark:border-gray-800 my-2 pt-2 flex gap-4 px-3">
|
||||
<a href="https://gitea.adhd.sh/root/spritesheet-generator" target="_blank" class="text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white">
|
||||
<i class="fab fa-github text-xl"></i>
|
||||
</a>
|
||||
<a href="https://discord.gg/JTev3nzeDa" target="_blank" class="text-gray-500 hover:text-[#5865F2] dark:text-gray-400 dark:hover:text-[#5865F2]">
|
||||
<i class="fab fa-discord text-xl"></i>
|
||||
</a>
|
||||
<button
|
||||
@click="
|
||||
$emit('open-help');
|
||||
isMobileMenuOpen = false;
|
||||
"
|
||||
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
|
||||
>
|
||||
<i class="fas fa-question-circle text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import DarkModeToggle from '../utilities/DarkModeToggle.vue';
|
||||
|
||||
defineEmits(['open-help']);
|
||||
|
||||
const isMobileMenuOpen = ref(false);
|
||||
</script>
|
||||
@@ -14,10 +14,31 @@ export interface SpritesheetConfig {
|
||||
manualCellHeight: number;
|
||||
}
|
||||
|
||||
export interface SharedSprite {
|
||||
id: string;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
flipX: boolean;
|
||||
flipY: boolean;
|
||||
base64: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface SharedLayer {
|
||||
id: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
locked: boolean;
|
||||
sprites: SharedSprite[];
|
||||
}
|
||||
|
||||
export interface SpritesheetRecord {
|
||||
id: string;
|
||||
config: SpritesheetConfig;
|
||||
sprites: any[];
|
||||
sprites: SharedLayer[];
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 p-8 sm:p-12">
|
||||
<div class="glass-panel rounded-3xl shadow-2xl p-8 sm:p-12">
|
||||
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 dark:text-white mb-6">About Us</h1>
|
||||
<div class="space-y-6">
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">Welcome to Spritesheet Generator, a tool designed to help game developers and artists streamline their workflow.</p>
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div v-if="post">
|
||||
<div v-if="post" class="glass-panel rounded-3xl shadow-2xl p-8 sm:p-12">
|
||||
<RouterLink :to="{ name: 'blog-overview' }" class="inline-flex items-center text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-6 transition-colors" title="Return to blog overview" aria-label="Navigate back to blog overview page">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="w-full">
|
||||
<h1 class="text-4xl font-bold mb-8 text-gray-900 dark:text-white">Blog</h1>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<article v-for="post in posts" :key="post.slug" class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300 flex flex-col h-full">
|
||||
<article v-for="post in posts" :key="post.slug" class="glass-panel overflow-hidden hover:shadow-xl transition-all duration-300 flex flex-col h-full group border-0 ring-1 ring-gray-200 dark:ring-gray-700">
|
||||
<RouterLink :to="{ name: 'blog-detail', params: { slug: post.slug } }" class="flex flex-col h-full" :title="`Read more: ${post.title}`" :aria-label="`Read full blog post: ${post.title}`">
|
||||
<img :src="post.image" :alt="post.title" class="w-full h-48 object-cover" loading="lazy" decoding="async" />
|
||||
<div class="p-6 flex-1 flex flex-col">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 p-8 sm:p-12">
|
||||
<div class="glass-panel rounded-3xl shadow-2xl p-8 sm:p-12">
|
||||
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 dark:text-white mb-6">Contact Us</h1>
|
||||
<div class="space-y-6">
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">We'd love to hear from you! Whether you have a question, feedback, or just want to say hi, feel free to reach out.</p>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 p-8 sm:p-12">
|
||||
<div class="glass-panel rounded-3xl shadow-2xl p-8 sm:p-12">
|
||||
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 dark:text-white mb-6">Frequently Asked Questions</h1>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed mb-10">Got questions? We've got answers. If you can't find what you're looking for, feel free to <router-link to="/contact" class="text-blue-600 dark:text-blue-400 hover:underline">contact us</router-link>.</p>
|
||||
|
||||
|
||||
@@ -1,295 +1,404 @@
|
||||
<template>
|
||||
<main class="flex flex-col flex-1 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 transition-all duration-300" :class="{ 'lg:overflow-hidden': layers.some(l => l.sprites.length) }">
|
||||
<!-- Welcome state -->
|
||||
<div v-if="!layers.some(l => l.sprites.length)" class="p-6 sm:p-10">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between gap-4 mb-1">
|
||||
<p class="text-2xl font-bold text-gray-800 dark:text-gray-100">Upload sprites or single image</p>
|
||||
<a href="https://ko-fi.com/X8X416D44P" target="_blank" rel="noopener noreferrer">
|
||||
<img height="36" style="border: 0px; height: 36px" src="https://storage.ko-fi.com/cdn/kofi6.png?v=6" alt="Buy Me a Coffee at ko-fi.com" />
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Drag and drop images or import from JSON</p>
|
||||
</div>
|
||||
<main class="flex flex-col flex-1 h-full min-h-0 relative">
|
||||
<!-- Welcome / Empty State -->
|
||||
<div v-if="!layers.some(l => l.sprites.length)" class="flex-1 flex flex-col items-center justify-center pb-12">
|
||||
<div class="w-full max-w-[90rem] px-4 sm:px-6 lg:px-8 flex flex-col gap-8 lg:gap-12 items-start pt-4 sm:pt-8 lg:pt-16">
|
||||
<!-- Top Row: Upload Field & Video Side by Side -->
|
||||
<div class="w-full grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
|
||||
<!-- File Uploader Component -->
|
||||
<div class="glass-panel p-2 rounded-2xl shadow-xl shadow-indigo-500/10 border border-indigo-50/50 dark:border-gray-700 h-full flex flex-col">
|
||||
<file-uploader @upload-sprites="handleSpritesUpload" />
|
||||
</div>
|
||||
|
||||
<div class="mt-10">
|
||||
<div class="prose prose-lg dark:prose-invert max-w-none">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-4">Welcome to Spritesheet generator</h1>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">Create spritesheets for your game development and animation projects with our completely free, open-source Spritesheet generator.</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">This powerful online tool lets you upload individual sprite images and automatically arrange them into optimized sprite sheets with customizable layouts - perfect for indie developers, animators, and studios of any size.</p>
|
||||
<h3 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-4">Key features of this sprite editor</h3>
|
||||
<ul class="text-gray-700 dark:text-gray-300 mb-6 space-y-2 list-disc">
|
||||
<li><strong>Free sprite editor</strong>: Edit, organize, and optimize your game sprites directly in your browser</li>
|
||||
<li><strong>Automatic spritesheet generation</strong>: Convert multiple PNG, JPG, or GIF images into efficient sprite atlases</li>
|
||||
<li><strong>Customizable grid layouts</strong>: Adjust spacing, padding, and arrangement for pixel-perfect results</li>
|
||||
<li><strong>Animation preview</strong>: Test your sprite animations before exporting</li>
|
||||
<li><strong>Cross-platform compatibility</strong>: Works with Unity, Godot, Phaser, Pygame, and other game engines</li>
|
||||
<li><strong>Zero installation required</strong>: No downloads - use our web-based sprite sheet maker instantly</li>
|
||||
<li><strong>Batch processing</strong>: Upload and process multiple sprites simultaneously</li>
|
||||
<li><strong>Export options</strong>: Download spritesheet as PNG, JPG, GIF, ZIP or JSON.</li>
|
||||
</ul>
|
||||
<div>
|
||||
<h4 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-play-circle text-gray-800 dark:text-gray-200"></i>
|
||||
How it works
|
||||
</h4>
|
||||
<video controls playsinline class="w-full rounded-lg shadow-lg border border-gray-200 dark:border-gray-700" title="Spritesheet generator tutorial" aria-label="Spritesheet generator tutorial">
|
||||
<source src="@/assets/demo.mp4" type="video/mp4" />
|
||||
<!-- Video Showcase -->
|
||||
<div class="rounded-2xl overflow-hidden shadow-2xl border border-gray-200 dark:border-gray-800 bg-gray-900 relative group h-full min-h-[300px] lg:min-h-0">
|
||||
<video autoplay controls loop muted playsinline class="w-full h-full object-cover opacity-90 group-hover:opacity-100 transition-opacity duration-500">
|
||||
<source :src="tutVideo" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Two-column layout: Left controls, Right preview -->
|
||||
<div v-if="layers.some(l => l.sprites.length)" class="flex flex-col flex-1 lg:grid lg:grid-cols-[320px_1fr] xl:grid-cols-[360px_1fr] lg:overflow-hidden min-h-0">
|
||||
<!-- Left sidebar - Controls (TIGHT!) -->
|
||||
<div class="p-4 bg-gray-50/50 dark:bg-gray-900/30 border-b lg:border-b-0 lg:border-r border-gray-200 dark:border-gray-700 lg:overflow-y-auto lg:overflow-x-hidden lg:min-h-0">
|
||||
<div class="space-y-4">
|
||||
<!-- Add Sprites Section -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="flex items-center gap-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<i class="fas fa-plus-circle text-[10px]"></i>
|
||||
Add Sprites
|
||||
</h3>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="flex-1 p-3 text-center border border-dashed rounded-lg transition-all cursor-pointer focus:outline-none group"
|
||||
:class="[isDragging ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'border-gray-300 dark:border-gray-600 hover:border-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800/50']"
|
||||
@click="openFileDialog"
|
||||
@dragenter.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<i class="fas fa-image text-lg mb-1 transition-colors" :class="[isDragging ? 'text-blue-500' : 'text-gray-400 group-hover:text-gray-600']"></i>
|
||||
<p class="text-xs font-medium" :class="[isDragging ? 'text-blue-600' : 'text-gray-500 group-hover:text-gray-700']">
|
||||
{{ isDragging ? 'Drop here' : 'Images' }}
|
||||
<!-- Bottom Section: Hero Text & Features -->
|
||||
<div class="w-full grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
|
||||
<!-- Hero Text -->
|
||||
<div class="text-left">
|
||||
<h1 class="text-4xl sm:text-5xl md:text-6xl font-extrabold tracking-tight mb-6 text-gray-900 dark:text-gray-50 leading-[1.1]">Welcome to <span class="text-indigo-600 dark:text-indigo-400">Spritesheet generator</span></h1>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
Create spritesheets for your game development and animation projects with our completely free, open-source Spritesheet generator. This powerful online tool lets you upload individual sprite images and automatically arrange them into optimized sprite sheets with customizable layouts -
|
||||
perfect for indie developers, animators, and studios of any size.
|
||||
</p>
|
||||
</button>
|
||||
<button @click="openJSONImportDialog" class="flex-1 p-3 text-center border border-gray-300 dark:border-gray-600 rounded-lg hover:border-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-all cursor-pointer group" data-rybbit-event="import-json">
|
||||
<i class="fas fa-file-code text-lg mb-1 text-gray-400 group-hover:text-gray-600 transition-colors"></i>
|
||||
<p class="text-xs font-medium text-gray-500 group-hover:text-gray-700">Import JSON</p>
|
||||
</button>
|
||||
</div>
|
||||
<input ref="uploadInput" type="file" multiple accept="image/*" class="hidden" @change="handleUploadChange" />
|
||||
<input ref="jsonFileInput" type="file" accept=".json,application/json" class="hidden" @change="handleJSONFileChange" />
|
||||
</section>
|
||||
|
||||
<!-- Layers Section -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="flex items-center gap-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<i class="fas fa-layer-group text-[10px]"></i>
|
||||
Layers
|
||||
</h3>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
<button @click="addLayer()" class="text-xs px-2 py-0.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-gray-600 dark:text-gray-300 transition-colors cursor-pointer" title="Add new layer">
|
||||
<i class="fas fa-plus text-[9px]"></i>
|
||||
</button>
|
||||
<!-- Key Features Grid -->
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2"><i class="fas fa-star text-yellow-500 text-sm"></i> Key features</h2>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-left">
|
||||
<!-- Features List -->
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-edit text-sm"></i>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Free sprite editor</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Edit, organize, and optimize directly in browser.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-magic text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Automatic generation</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Convert images into efficient sprite atlases.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-th text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Customizable layouts</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Adjust spacing for pixel-perfect results.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-play-circle text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Animation preview</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Test animations before exporting.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-gamepad text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Cross-platform</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Works with Unity, Godot, etc.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-globe text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">No installation</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Use our web-based tool instantly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-layer-group text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Batch processing</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Upload/process multiple sprites.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-file-export text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Export options</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Download PNG, JSON, ZIP, or GIF.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Editor Interface -->
|
||||
<div v-else class="flex flex-col lg:flex-row gap-6 h-full min-h-[600px] lg:overflow-hidden">
|
||||
<!-- Sidebar Controls -->
|
||||
<aside class="flex flex-col w-full lg:w-[340px] gap-4 shrink-0 lg:overflow-hidden">
|
||||
<div class="glass-panel rounded-xl flex flex-col h-full lg:overflow-hidden border border-gray-200 dark:border-gray-700/60 shadow-lg">
|
||||
<!-- Sidebar Header -->
|
||||
<div class="px-5 py-4 border-b border-gray-200/50 dark:border-gray-700/50 flex items-center justify-between shrink-0 bg-gray-50/50 dark:bg-gray-800/10">
|
||||
<h2 class="text-sm font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">Editor Tools</h2>
|
||||
<button @click="openFileDialog" class="btn btn-primary btn-sm shadow-indigo-500/20" title="Add more sprites"><i class="fas fa-plus mr-1"></i> Add</button>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden p-5 space-y-6 scrollbar-thin">
|
||||
<!-- Layers -->
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">Layers</h3>
|
||||
<button @click="addLayer()" class="text-xs p-1 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors" title="Add Layer"><i class="fas fa-layer-group mr-1"></i> New</button>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="layer in layers"
|
||||
:key="layer.id"
|
||||
class="flex items-center gap-1.5 px-2 py-1.5 bg-white dark:bg-gray-800 border rounded-md transition-all text-sm"
|
||||
:class="[layer.id === activeLayerId ? 'border-gray-700 ring-1 ring-gray-700 dark:border-gray-400 dark:ring-gray-400' : 'border-gray-200 dark:border-gray-700', !layer.visible ? 'opacity-40' : '']"
|
||||
class="group flex items-center gap-2 p-2 rounded-lg border transition-all duration-200"
|
||||
:class="activeLayerId === layer.id ? 'bg-white dark:bg-gray-800 border-indigo-500 ring-1 ring-indigo-500 shadow-sm' : 'bg-gray-50 dark:bg-gray-800/40 border-transparent hover:border-gray-200 dark:hover:border-gray-700'"
|
||||
>
|
||||
<button @click.stop="layer.visible = !layer.visible" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer" :title="layer.visible ? 'Hide' : 'Show'">
|
||||
<i :class="[layer.visible ? 'fas fa-eye text-gray-600 dark:text-gray-300' : 'fas fa-eye-slash text-gray-400', 'text-xs']"></i>
|
||||
<!-- Visibility Toggle -->
|
||||
<button @click.stop="layer.visible = !layer.visible" class="w-6 h-6 flex items-center justify-center rounded text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors">
|
||||
<i :class="layer.visible ? 'fas fa-eye' : 'fas fa-eye-slash'" class="text-xs"></i>
|
||||
</button>
|
||||
|
||||
<!-- Layer Name -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<input
|
||||
v-if="editingLayerId === layer.id"
|
||||
type="text"
|
||||
v-model="editingLayerName"
|
||||
@blur="finishEditingLayer"
|
||||
@keyup.enter="finishEditingLayer"
|
||||
@keyup.esc="cancelEditingLayer"
|
||||
class="flex-1 px-1.5 py-0.5 text-xs border border-gray-700 dark:border-gray-400 dark:bg-gray-700 dark:text-gray-100 rounded outline-none min-w-0"
|
||||
ref="layerNameInput"
|
||||
@click.stop
|
||||
class="w-full text-sm bg-transparent border-b border-indigo-500 outline-none p-0 text-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
<button v-else @click="activeLayerId = layer.id" class="flex-1 text-xs font-medium text-left truncate cursor-pointer min-w-0" :class="layer.id === activeLayerId ? 'text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-400'">
|
||||
{{ layer.name }}<span v-if="layer.sprites.length" class="ml-1 opacity-50">({{ layer.sprites.length }})</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-0.5" v-if="editingLayerId !== layer.id">
|
||||
<button @click="startEditingLayer(layer.id, layer.name)" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Rename">
|
||||
<i class="fas fa-pen text-[9px] text-gray-400"></i>
|
||||
</button>
|
||||
<button @click="moveLayer(layer.id, 'up')" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Move up">
|
||||
<i class="fas fa-chevron-up text-[9px] text-gray-400"></i>
|
||||
</button>
|
||||
<button @click="moveLayer(layer.id, 'down')" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Move down">
|
||||
<i class="fas fa-chevron-down text-[9px] text-gray-400"></i>
|
||||
</button>
|
||||
<button v-if="layers.length > 1" @click="removeLayer(layer.id)" class="p-1 rounded hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors cursor-pointer" title="Delete">
|
||||
<i class="fas fa-trash text-[9px] text-red-400"></i>
|
||||
<button v-else @click="activeLayerId = layer.id" class="w-full text-left text-sm font-medium truncate" :class="activeLayerId === layer.id ? 'text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-400'">
|
||||
{{ layer.name }} <span class="text-xs opacity-50 font-normal ml-1">({{ layer.sprites.length }})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button @click="startEditingLayer(layer.id, layer.name)" class="p-1.5 text-gray-400 hover:text-indigo-500 transition-colors" title="Rename"><i class="fas fa-pen text-[10px]"></i></button>
|
||||
<button @click="moveLayer(layer.id, 'up')" class="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors" title="Move Up"><i class="fas fa-chevron-up text-[10px]"></i></button>
|
||||
<button @click="moveLayer(layer.id, 'down')" class="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors" title="Move Down"><i class="fas fa-chevron-down text-[10px]"></i></button>
|
||||
<button v-if="layers.length > 1" @click="removeLayer(layer.id)" class="p-1.5 text-gray-400 hover:text-red-500 transition-colors" title="Delete"><i class="fas fa-trash text-[10px]"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Grid & Cell Size Section (Combined) -->
|
||||
<!-- Canvas Grid Settings -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="flex items-center gap-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<i class="fas fa-th text-[10px]"></i>
|
||||
Grid Layout
|
||||
</h3>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-2.5 space-y-2">
|
||||
<!-- Columns -->
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">Grid Layout</h3>
|
||||
<div class="card p-3 bg-gray-50/50 dark:bg-gray-800/40 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label for="columns" class="text-xs font-medium text-gray-600 dark:text-gray-300">Columns</label>
|
||||
<input id="columns" type="number" v-model.number="columns" min="1" max="10" class="w-14 px-2 py-1 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 dark:text-gray-100 focus:ring-1 focus:ring-gray-500 outline-none" />
|
||||
<label class="text-sm text-gray-600 dark:text-gray-300">Columns</label>
|
||||
<input type="number" v-model.number="columns" min="1" max="20" class="input-field w-16 text-center" />
|
||||
</div>
|
||||
<!-- Cell Size -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center gap-1.5">
|
||||
Cell Size
|
||||
<input type="checkbox" v-model="settingsStore.manualCellSizeEnabled" class="w-3 h-3 rounded" title="Manual override" />
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-300">Force Size</label>
|
||||
<input type="checkbox" v-model="settingsStore.manualCellSizeEnabled" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
|
||||
</div>
|
||||
<div v-if="settingsStore.manualCellSizeEnabled" class="flex items-center gap-1">
|
||||
<input type="number" v-model.number="settingsStore.manualCellWidth" min="1" max="2048" class="w-12 px-1.5 py-1 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 dark:text-gray-100 outline-none" placeholder="W" />
|
||||
<span class="text-gray-400 text-xs">×</span>
|
||||
<input type="number" v-model.number="settingsStore.manualCellHeight" min="1" max="2048" class="w-12 px-1.5 py-1 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 dark:text-gray-100 outline-none" placeholder="H" />
|
||||
<input v-model.number="settingsStore.manualCellWidth" class="input-field w-14 text-center px-1" placeholder="W" />
|
||||
<span class="text-gray-400 text-xs">x</span>
|
||||
<input v-model.number="settingsStore.manualCellHeight" class="input-field w-14 text-center px-1" placeholder="H" />
|
||||
</div>
|
||||
<span v-else class="text-xs font-mono text-gray-400">{{ cellSize.width }}×{{ cellSize.height }}px</span>
|
||||
<span v-else class="text-xs font-mono text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">{{ cellSize.width }}×{{ cellSize.height }}px</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sprite Alignment Section -->
|
||||
<!-- View Options -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="flex items-center gap-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<i class="fas fa-align-center text-[10px]"></i>
|
||||
Align All Sprites
|
||||
</h3>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-6 gap-1">
|
||||
<button @click="alignSprites('left')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Align Left">
|
||||
<i class="fas fa-arrow-left text-xs text-gray-500"></i>
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">View Options</h3>
|
||||
<div class="card p-2 bg-gray-50/50 dark:bg-gray-800/40 grid grid-cols-2 gap-2">
|
||||
<Tooltip text="Disable anti-aliasing for crisp pixel art rendering">
|
||||
<button
|
||||
@click="settingsStore.pixelPerfect = !settingsStore.pixelPerfect"
|
||||
:class="settingsStore.pixelPerfect ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-th mr-2"></i>Pixel
|
||||
</button>
|
||||
<button @click="alignSprites('center')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Center Horizontally">
|
||||
<i class="fas fa-arrows-left-right text-xs text-gray-500"></i>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show checkerboard background">
|
||||
<button
|
||||
@click="settingsStore.checkerboardEnabled = !settingsStore.checkerboardEnabled"
|
||||
:class="settingsStore.checkerboardEnabled ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-chess-board mr-2"></i>Grid
|
||||
</button>
|
||||
<button @click="alignSprites('right')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Align Right">
|
||||
<i class="fas fa-arrow-right text-xs text-gray-500"></i>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show selection borders">
|
||||
<button
|
||||
@click="showActiveBorder = !showActiveBorder"
|
||||
:class="showActiveBorder ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-vector-square mr-2"></i>Borders
|
||||
</button>
|
||||
<button @click="alignSprites('top')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Align Top">
|
||||
<i class="fas fa-arrow-up text-xs text-gray-500"></i>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show sprite coordinates">
|
||||
<button
|
||||
@click="showOffsetLabels = !showOffsetLabels"
|
||||
:class="showOffsetLabels ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-tag mr-2"></i>Labels
|
||||
</button>
|
||||
<button @click="alignSprites('middle')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Center Vertically">
|
||||
<i class="fas fa-arrows-up-down text-xs text-gray-500"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('bottom')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Align Bottom">
|
||||
<i class="fas fa-arrow-down text-xs text-gray-500"></i>
|
||||
</Tooltip>
|
||||
<Tooltip text="Compare with ghost overlays" class="col-span-2">
|
||||
<button
|
||||
@click="showAllSprites = !showAllSprites"
|
||||
:class="showAllSprites ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-clone mr-2"></i>Ghost compare
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Export Section -->
|
||||
<!-- Tools -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="flex items-center gap-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<i class="fas fa-download text-[10px]"></i>
|
||||
Download & Share
|
||||
</h3>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">Tools</h3>
|
||||
<div class="card p-2 bg-gray-50/50 dark:bg-gray-800/40 space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<Tooltip text="Select multiple sprites" class="flex-1">
|
||||
<button
|
||||
@click="isMultiSelectMode = !isMultiSelectMode"
|
||||
:class="isMultiSelectMode ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-object-group mr-2"></i>Multi
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Swap cell positions" class="flex-1">
|
||||
<button
|
||||
@click="allowCellSwap = !allowCellSwap"
|
||||
:class="allowCellSwap ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-exchange-alt mr-2"></i>Swap
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-1.5 mb-2">
|
||||
<div class="flex items-center justify-between px-2 pt-1 border-t border-gray-100 dark:border-gray-700/50">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">Negative Spacing</span>
|
||||
<button
|
||||
@click="downloadSpritesheet"
|
||||
class="flex flex-col items-center justify-center p-2.5 bg-gray-700 hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500 rounded-lg text-white transition-colors cursor-pointer"
|
||||
data-rybbit-event="download-spritesheet"
|
||||
title="Download as PNG image"
|
||||
@click="settingsStore.negativeSpacingEnabled = !settingsStore.negativeSpacingEnabled"
|
||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
:class="settingsStore.negativeSpacingEnabled ? 'bg-indigo-600' : 'bg-gray-200 dark:bg-gray-700'"
|
||||
>
|
||||
<i class="fas fa-image text-sm mb-0.5"></i>
|
||||
<span class="text-[10px] font-medium">PNG</span>
|
||||
</button>
|
||||
<button
|
||||
@click="exportSpritesheetJSON"
|
||||
class="flex flex-col items-center justify-center p-2.5 bg-gray-700 hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500 rounded-lg text-white transition-colors cursor-pointer"
|
||||
data-rybbit-event="export-json"
|
||||
title="Export project data as JSON"
|
||||
>
|
||||
<i class="fas fa-file-code text-sm mb-0.5"></i>
|
||||
<span class="text-[10px] font-medium">JSON</span>
|
||||
</button>
|
||||
<button @click="openGifFpsModal" class="flex flex-col items-center justify-center p-2.5 bg-gray-700 hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500 rounded-lg text-white transition-colors cursor-pointer" data-rybbit-event="download-gif" title="Export as animated GIF">
|
||||
<i class="fas fa-film text-sm mb-0.5"></i>
|
||||
<span class="text-[10px] font-medium">GIF</span>
|
||||
</button>
|
||||
<button @click="downloadAsZip" class="flex flex-col items-center justify-center p-2.5 bg-gray-700 hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500 rounded-lg text-white transition-colors cursor-pointer" data-rybbit-event="download-zip" title="Download all sprites as ZIP">
|
||||
<i class="fas fa-file-archive text-sm mb-0.5"></i>
|
||||
<span class="text-[10px] font-medium">ZIP</span>
|
||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform" :class="settingsStore.negativeSpacingEnabled ? 'translate-x-5' : 'translate-x-1'" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="openShareModal"
|
||||
class="w-full flex items-center justify-center gap-2 p-2.5 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 rounded-lg text-white text-sm font-medium transition-all shadow-sm hover:shadow-md cursor-pointer"
|
||||
data-rybbit-event="share-spritesheet"
|
||||
title="Generate shareable link"
|
||||
>
|
||||
<i class="fas fa-share-alt text-sm"></i>
|
||||
Share Online
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Alignment Tools -->
|
||||
<section>
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">Alignment</h3>
|
||||
<div class="card p-2 bg-gray-50/50 dark:bg-gray-800/40 grid grid-cols-3 gap-2">
|
||||
<Tooltip text="Align sprites to the left edge">
|
||||
<button @click="alignSprites('left')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrow-left"></i>Left</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Center sprites horizontally">
|
||||
<button @click="alignSprites('center')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrows-left-right"></i>Center</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Align sprites to the right edge">
|
||||
<button @click="alignSprites('right')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrow-right"></i>Right</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Align sprites to the top edge">
|
||||
<button @click="alignSprites('top')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrow-up"></i>Top</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Center sprites vertically">
|
||||
<button @click="alignSprites('middle')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrows-up-down"></i>Middle</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Align sprites to the bottom edge">
|
||||
<button @click="alignSprites('bottom')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrow-down"></i>Bottom</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right panel - Tabs -->
|
||||
<div class="flex flex-col overflow-hidden min-h-0">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="bg-gray-50/50 dark:bg-gray-900/30 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between gap-1 px-3 py-2">
|
||||
<div class="flex gap-1">
|
||||
<!-- Sidebar Footer (Export) -->
|
||||
<div class="p-4 border-t border-gray-200/50 dark:border-gray-700/50 bg-gray-50/50 dark:bg-gray-800/10 space-y-3 shrink-0">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button @click="downloadSpritesheet" class="btn btn-secondary btn-sm justify-start" title="Download PNG"><i class="fas fa-image text-indigo-500 w-4"></i> PNG</button>
|
||||
<button @click="exportSpritesheetJSON" class="btn btn-secondary btn-sm justify-start" title="Download JSON"><i class="fas fa-code text-indigo-500 w-4"></i> JSON</button>
|
||||
<button @click="openGifFpsModal" class="btn btn-secondary btn-sm justify-start" title="Export GIF"><i class="fas fa-film text-pink-500 w-4"></i> GIF</button>
|
||||
<button @click="downloadAsZip" class="btn btn-secondary btn-sm justify-start" title="Download ZIP"><i class="fas fa-file-archive text-yellow-500 w-4"></i> ZIP</button>
|
||||
</div>
|
||||
<button @click="openShareModal" class="btn btn-secondary w-full justify-start"><i class="fas fa-share-alt mr-2 text-indigo-500"></i> Share Project</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Canvas Area -->
|
||||
<div class="flex-1 flex flex-col min-h-0 glass-panel rounded-xl border border-gray-200 dark:border-gray-700/60 shadow-lg overflow-hidden">
|
||||
<!-- Tabs / Toolbar -->
|
||||
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50/30 dark:bg-gray-800/20">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||
<button
|
||||
@click="activeTab = 'canvas'"
|
||||
:class="[
|
||||
'flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-all cursor-pointer border',
|
||||
activeTab === 'canvas' ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm border-gray-300 dark:border-gray-600' : 'text-gray-500 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-800/50 border-transparent',
|
||||
]"
|
||||
:class="activeTab === 'canvas' ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
|
||||
class="px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200"
|
||||
>
|
||||
<i class="fas fa-th text-xs"></i>
|
||||
<span>Canvas</span>
|
||||
<i class="fas fa-th mr-2"></i>Editor
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'preview'"
|
||||
:class="[
|
||||
'flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-all cursor-pointer border',
|
||||
activeTab === 'preview' ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm border-gray-300 dark:border-gray-600' : 'text-gray-500 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-800/50 border-transparent',
|
||||
]"
|
||||
data-rybbit-event="preview-animation"
|
||||
:class="activeTab === 'preview' ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
|
||||
class="px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200"
|
||||
>
|
||||
<i class="fas fa-play text-xs"></i>
|
||||
<span>Animation</span>
|
||||
<i class="fas fa-play mr-2"></i>Preview
|
||||
</button>
|
||||
</div>
|
||||
<button @click="openShareModal" class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md bg-gradient-to-r from-blue-500 to-purple-600 text-white shadow-sm hover:shadow-md transition-all cursor-pointer" data-rybbit-event="share-spritesheet-header">
|
||||
<i class="fas fa-share-alt text-[10px]"></i>
|
||||
<span>Share</span>
|
||||
</button>
|
||||
|
||||
<!-- Background Color (Compact) -->
|
||||
<div class="flex items-center gap-2 pl-3 border-l border-gray-200 dark:border-gray-700">
|
||||
<span class="text-xs font-medium text-gray-500 uppercase">Bg</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<select v-model="bgSelectValue" class="text-xs border-none bg-transparent dark:text-gray-300 focus:ring-0 cursor-pointer pr-8">
|
||||
<option value="transparent">None</option>
|
||||
<option value="#ffffff">White</option>
|
||||
<option value="#000000">Black</option>
|
||||
<option value="#f9fafb">Gray</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
<div v-if="bgSelectValue === 'custom'" class="relative w-5 h-5 rounded-full overflow-hidden border border-gray-300 dark:border-gray-600 shadow-sm">
|
||||
<input type="color" v-model="customColor" @input="updateCustomColor" class="absolute -top-1 -left-1 w-8 h-8 cursor-pointer p-0 border-0 opacity-0" />
|
||||
<div class="w-full h-full" :style="{ backgroundColor: customColor }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="p-4 lg:flex-1 lg:overflow-auto lg:min-h-0">
|
||||
<div v-if="activeTab === 'canvas'" class="h-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
||||
<button @click="zoomOut" class="p-1.5 hover:bg-white dark:hover:bg-gray-700 rounded text-gray-500 transition-colors"><i class="fas fa-minus text-xs"></i></button>
|
||||
<span class="text-xs font-mono w-12 text-center text-gray-600 dark:text-gray-300">{{ Math.round(zoom * 100) }}%</span>
|
||||
<button @click="zoomIn" class="p-1.5 hover:bg-white dark:hover:bg-gray-700 rounded text-gray-500 transition-colors"><i class="fas fa-plus text-xs"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Canvas Content -->
|
||||
<div class="flex-1 overflow-hidden relative bg-white dark:bg-gray-900/50">
|
||||
<!-- Grid Background Pattern -->
|
||||
<div
|
||||
class="absolute inset-0 opacity-5 pointer-events-none bg-gray-500"
|
||||
style="background-image: url('data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2220%22 height=%2220%22><circle cx=%2210%22 cy=%2210%22 r=%221%22 fill=%22%236b7280%22/></svg>'); background-size: 20px 20px"
|
||||
></div>
|
||||
|
||||
<div class="h-full overflow-auto custom-scrollbar p-4">
|
||||
<div v-if="activeTab === 'canvas'" class="h-full flex flex-col justify-center">
|
||||
<sprite-canvas
|
||||
:layers="layers"
|
||||
:active-layer-id="activeLayerId"
|
||||
:columns="columns"
|
||||
:zoom="zoom"
|
||||
:is-multi-select-mode="isMultiSelectMode"
|
||||
:show-active-border="showActiveBorder"
|
||||
:allow-cell-swap="allowCellSwap"
|
||||
:show-all-sprites="showAllSprites"
|
||||
:show-offset-labels="showOffsetLabels"
|
||||
@update-sprite="updateSpritePosition"
|
||||
@update-sprite-cell="updateSpriteCell"
|
||||
@remove-sprite="removeSprite"
|
||||
@@ -300,27 +409,33 @@
|
||||
@flip-sprite="flipSprite"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="activeTab === 'preview'" class="h-full">
|
||||
<sprite-preview :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-in-layer="updateSpriteInLayer" />
|
||||
<div v-if="activeTab === 'preview'" class="h-full flex items-center justify-center">
|
||||
<sprite-preview :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-in-layer="updateSpriteInLayer" @drop-sprite="handleDropSprite" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modals & Hidden Inputs -->
|
||||
<SpritesheetSplitter :is-open="isSpritesheetSplitterOpen" :image-url="spritesheetImageUrl" :image-file="spritesheetImageFile" @close="closeSpritesheetSplitter" @split="handleSplitSpritesheet" />
|
||||
<GifFpsModal :is-open="isGifFpsModalOpen" @close="closeGifFpsModal" @confirm="downloadAsGif" :default-fps="10" />
|
||||
<ShareModal :is-open="isShareModalOpen" :share-function="shareFunction" @close="closeShareModal" />
|
||||
|
||||
<input ref="uploadInput" type="file" multiple accept="image/*" class="hidden" @change="handleUploadChange" />
|
||||
<input ref="jsonFileInput" type="file" accept=".json,application/json" class="hidden" @change="handleJSONFileChange" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, toRef, computed } from 'vue';
|
||||
import { ref, toRef, computed, watch, nextTick } from 'vue';
|
||||
import FileUploader from '@/components/FileUploader.vue';
|
||||
import SpriteCanvas from '@/components/SpriteCanvas.vue';
|
||||
import SpritePreview from '@/components/SpritePreview.vue';
|
||||
import SpritesheetSplitter from '@/components/SpritesheetSplitter.vue';
|
||||
import GifFpsModal from '@/components/GifFpsModal.vue';
|
||||
import ShareModal from '@/components/ShareModal.vue';
|
||||
import Tooltip from '@/components/utilities/Tooltip.vue';
|
||||
import { useExportLayers } from '@/composables/useExportLayers';
|
||||
import { useShare } from '@/composables/useShare';
|
||||
import { useLayers } from '@/composables/useLayers';
|
||||
@@ -328,9 +443,10 @@
|
||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
|
||||
import { useHomeViewSEO } from './HomeView.seo';
|
||||
import { useZoom } from '@/composables/useZoom';
|
||||
import type { SpriteFile } from '@/types/sprites';
|
||||
import tutVideo from '@/assets/tut2.mp4';
|
||||
|
||||
// Initialize SEO
|
||||
useHomeViewSEO();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -347,13 +463,69 @@
|
||||
toRef(settingsStore, 'manualCellHeight')
|
||||
);
|
||||
|
||||
// Zoom Control
|
||||
const {
|
||||
zoom,
|
||||
increase: zoomIn,
|
||||
decrease: zoomOut,
|
||||
reset: resetZoom,
|
||||
} = useZoom({
|
||||
min: 0.5,
|
||||
max: 3,
|
||||
step: 0.25,
|
||||
initial: 1,
|
||||
});
|
||||
|
||||
// View Options & Tools
|
||||
const isMultiSelectMode = ref(false);
|
||||
const showActiveBorder = ref(true);
|
||||
const allowCellSwap = ref(false);
|
||||
const showAllSprites = ref(false);
|
||||
const showOffsetLabels = ref(false);
|
||||
const customColor = ref('#ffffff');
|
||||
const isCustomMode = ref(false);
|
||||
|
||||
// Background Color Logic
|
||||
const presetBgColors = ['transparent', '#ffffff', '#000000', '#f9fafb'] as const;
|
||||
const isHexColor = (val: string) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val);
|
||||
|
||||
if (isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any)) {
|
||||
customColor.value = settingsStore.backgroundColor;
|
||||
isCustomMode.value = true;
|
||||
}
|
||||
|
||||
const bgSelectValue = computed<string>({
|
||||
get() {
|
||||
if (isCustomMode.value) return 'custom';
|
||||
const val = settingsStore.backgroundColor;
|
||||
if (presetBgColors.includes(val as any)) return val;
|
||||
if (isHexColor(val)) {
|
||||
customColor.value = val;
|
||||
isCustomMode.value = true;
|
||||
return 'custom';
|
||||
}
|
||||
return 'transparent';
|
||||
},
|
||||
set(v: string) {
|
||||
if (v === 'custom') {
|
||||
isCustomMode.value = true;
|
||||
settingsStore.setBackgroundColor(customColor.value);
|
||||
} else {
|
||||
isCustomMode.value = false;
|
||||
settingsStore.setBackgroundColor(v);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const updateCustomColor = () => {
|
||||
settingsStore.setBackgroundColor(customColor.value);
|
||||
};
|
||||
|
||||
const getCellSize = () => {
|
||||
if (!visibleLayers.value.length) return { width: 0, height: 0 };
|
||||
|
||||
if (settingsStore.manualCellSizeEnabled) {
|
||||
return { width: settingsStore.manualCellWidth, height: settingsStore.manualCellHeight };
|
||||
}
|
||||
|
||||
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value);
|
||||
const allSprites = visibleLayers.value.flatMap(l => l.sprites);
|
||||
const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled);
|
||||
@@ -362,18 +534,21 @@
|
||||
|
||||
const cellSize = computed(getCellSize);
|
||||
const activeTab = ref<'canvas' | 'preview'>('canvas');
|
||||
|
||||
const isSpritesheetSplitterOpen = ref(false);
|
||||
const isGifFpsModalOpen = ref(false);
|
||||
const isShareModalOpen = ref(false);
|
||||
|
||||
const uploadInput = ref<HTMLInputElement | null>(null);
|
||||
const jsonFileInput = ref<HTMLInputElement | null>(null);
|
||||
const spritesheetImageUrl = ref('');
|
||||
const spritesheetImageFile = ref<File | null>(null);
|
||||
|
||||
const editingLayerId = ref<string | null>(null);
|
||||
const editingLayerName = ref('');
|
||||
const layerNameInput = ref<HTMLInputElement | null>(null);
|
||||
const isDragging = ref(false);
|
||||
|
||||
// Upload Handlers
|
||||
const handleSpritesUpload = async (files: File[]) => {
|
||||
const jsonFile = files.find(file => file.type === 'application/json' || file.name.endsWith('.json'));
|
||||
|
||||
@@ -395,7 +570,6 @@
|
||||
isSpritesheetSplitterOpen.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
processImageFiles([file]);
|
||||
};
|
||||
img.onerror = () => {
|
||||
@@ -403,9 +577,6 @@
|
||||
};
|
||||
img.src = url;
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.error('Failed to read image file:', file.name);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
return;
|
||||
}
|
||||
@@ -413,13 +584,6 @@
|
||||
processImageFiles(files);
|
||||
};
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
isDragging.value = false;
|
||||
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
|
||||
handleSpritesUpload(Array.from(event.dataTransfer.files));
|
||||
}
|
||||
};
|
||||
|
||||
const handleJSONImport = async (jsonFile: File) => {
|
||||
try {
|
||||
await importSpritesheetJSON(jsonFile);
|
||||
@@ -448,13 +612,9 @@
|
||||
isGifFpsModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeGifFpsModal = () => {
|
||||
isGifFpsModalOpen.value = false;
|
||||
};
|
||||
const closeGifFpsModal = () => (isGifFpsModalOpen.value = false);
|
||||
|
||||
// Share functionality
|
||||
const { share } = useShare(layers, columns, toRef(settingsStore, 'negativeSpacingEnabled'), toRef(settingsStore, 'backgroundColor'), toRef(settingsStore, 'manualCellSizeEnabled'), toRef(settingsStore, 'manualCellWidth'), toRef(settingsStore, 'manualCellHeight'));
|
||||
|
||||
const shareFunction = () => share();
|
||||
|
||||
const openShareModal = () => {
|
||||
@@ -465,17 +625,13 @@
|
||||
isShareModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeShareModal = () => {
|
||||
isShareModalOpen.value = false;
|
||||
};
|
||||
const closeShareModal = () => (isShareModalOpen.value = false);
|
||||
|
||||
const handleSplitSpritesheet = (spriteFiles: SpriteFile[]) => {
|
||||
processImageFiles(spriteFiles.map(s => s.file));
|
||||
};
|
||||
|
||||
const openJSONImportDialog = () => {
|
||||
jsonFileInput.value?.click();
|
||||
};
|
||||
const openFileDialog = () => uploadInput.value?.click();
|
||||
|
||||
const handleJSONFileChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
@@ -485,10 +641,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const openFileDialog = () => {
|
||||
uploadInput.value?.click();
|
||||
};
|
||||
|
||||
const handleUploadChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
@@ -497,10 +649,10 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Layer Editing
|
||||
const startEditingLayer = (layerId: string, currentName: string) => {
|
||||
editingLayerId.value = layerId;
|
||||
editingLayerName.value = currentName;
|
||||
// Focus the input on next tick
|
||||
setTimeout(() => {
|
||||
layerNameInput.value?.focus();
|
||||
layerNameInput.value?.select();
|
||||
@@ -510,9 +662,7 @@
|
||||
const finishEditingLayer = () => {
|
||||
if (editingLayerId.value && editingLayerName.value.trim()) {
|
||||
const layer = layers.value.find(l => l.id === editingLayerId.value);
|
||||
if (layer) {
|
||||
layer.name = editingLayerName.value.trim();
|
||||
}
|
||||
if (layer) layer.name = editingLayerName.value.trim();
|
||||
}
|
||||
editingLayerId.value = null;
|
||||
editingLayerName.value = '';
|
||||
@@ -522,4 +672,57 @@
|
||||
editingLayerId.value = null;
|
||||
editingLayerName.value = '';
|
||||
};
|
||||
|
||||
const handleDropSprite = (layerId: string, frameIndex: number, files: File[]) => {
|
||||
const layer = layers.value.find(l => l.id === layerId);
|
||||
if (!layer) return;
|
||||
|
||||
files.forEach((file, fileIdx) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const url = e.target?.result as string;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const sprite = {
|
||||
id: crypto.randomUUID(),
|
||||
file,
|
||||
img,
|
||||
url,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
};
|
||||
const insertIndex = frameIndex + fileIdx;
|
||||
if (insertIndex < layer.sprites.length) {
|
||||
layer.sprites = [...layer.sprites.slice(0, insertIndex), sprite, ...layer.sprites.slice(insertIndex + 1)];
|
||||
} else {
|
||||
while (layer.sprites.length < insertIndex) {
|
||||
layer.sprites.push({
|
||||
id: crypto.randomUUID(),
|
||||
file: new File([], 'empty'),
|
||||
img: new Image(),
|
||||
url: '',
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
});
|
||||
}
|
||||
layer.sprites = [...layer.sprites, sprite];
|
||||
}
|
||||
};
|
||||
img.src = url;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 p-8 sm:p-12">
|
||||
<div class="glass-panel rounded-3xl shadow-2xl p-8 sm:p-12">
|
||||
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 dark:text-white mb-6">Privacy Policy</h1>
|
||||
<div class="space-y-6">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-8">Last updated: November 26, 2025</p>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<main class="flex-1 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 transition-all duration-300">
|
||||
<main class="flex-1 glass-panel rounded-3xl shadow-2xl transition-all duration-300">
|
||||
<div class="p-6 sm:p-10 max-w-4xl mx-auto">
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center py-24">
|
||||
<div class="relative">
|
||||
<div class="w-16 h-16 border-4 border-gray-200 dark:border-gray-700 rounded-full"></div>
|
||||
<div class="absolute top-0 left-0 w-16 h-16 border-4 border-transparent border-t-blue-500 rounded-full animate-spin"></div>
|
||||
<div class="absolute top-0 left-0 w-16 h-16 border-4 border-transparent border-t-blue-500 rounded-full"></div>
|
||||
</div>
|
||||
<p class="mt-6 text-lg font-medium text-gray-600 dark:text-gray-300">Loading shared spritesheet...</p>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Please wait while we fetch your spritesheet</p>
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-2">Oops! Something went wrong</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6 text-center max-w-md">{{ error }}</p>
|
||||
<router-link to="/" class="btn btn-dark px-6 py-3">
|
||||
<router-link to="/" class="btn btn-primary px-6 py-3">
|
||||
<i class="fas fa-home mr-2"></i>
|
||||
Back to Home
|
||||
</router-link>
|
||||
@@ -28,37 +28,37 @@
|
||||
<div v-else-if="spritesheetData">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-10">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl shadow-lg mb-4">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-indigo-600 rounded-2xl shadow-lg mb-4">
|
||||
<i class="fas fa-share-alt text-2xl text-white"></i>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-gray-900 to-gray-700 dark:from-white dark:to-gray-300 mb-2">Shared spritesheet</h1>
|
||||
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 dark:text-white mb-2">Shared spritesheet</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400">Shared {{ formatDate(spritesheetData.created) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
|
||||
<div class="group p-5 bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-2xl border border-blue-200/50 dark:border-blue-700/30">
|
||||
<div class="group p-5 bg-blue-50 dark:bg-blue-900/20 rounded-2xl border border-blue-200/50 dark:border-blue-700/30">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-blue-500/20 dark:bg-blue-500/30 rounded-xl mb-3">
|
||||
<i class="fas fa-columns text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
<p class="text-xs font-medium text-blue-600/80 dark:text-blue-400/80 uppercase tracking-wider">Columns</p>
|
||||
<p class="text-2xl font-bold text-blue-700 dark:text-blue-300">{{ spritesheetData.config.columns }}</p>
|
||||
</div>
|
||||
<div class="group p-5 bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-2xl border border-purple-200/50 dark:border-purple-700/30">
|
||||
<div class="group p-5 bg-purple-50 dark:bg-purple-900/20 rounded-2xl border border-purple-200/50 dark:border-purple-700/30">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-purple-500/20 dark:bg-purple-500/30 rounded-xl mb-3">
|
||||
<i class="fas fa-layer-group text-purple-600 dark:text-purple-400"></i>
|
||||
</div>
|
||||
<p class="text-xs font-medium text-purple-600/80 dark:text-purple-400/80 uppercase tracking-wider">Layers</p>
|
||||
<p class="text-2xl font-bold text-purple-700 dark:text-purple-300">{{ spritesheetData.sprites.length }}</p>
|
||||
</div>
|
||||
<div class="group p-5 bg-gradient-to-br from-emerald-50 to-emerald-100 dark:from-emerald-900/20 dark:to-emerald-800/20 rounded-2xl border border-emerald-200/50 dark:border-emerald-700/30">
|
||||
<div class="group p-5 bg-emerald-50 dark:bg-emerald-900/20 rounded-2xl border border-emerald-200/50 dark:border-emerald-700/30">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-emerald-500/20 dark:bg-emerald-500/30 rounded-xl mb-3">
|
||||
<i class="fas fa-images text-emerald-600 dark:text-emerald-400"></i>
|
||||
</div>
|
||||
<p class="text-xs font-medium text-emerald-600/80 dark:text-emerald-400/80 uppercase tracking-wider">Sprites</p>
|
||||
<p class="text-2xl font-bold text-emerald-700 dark:text-emerald-300">{{ totalSprites }}</p>
|
||||
</div>
|
||||
<div class="group p-5 bg-gradient-to-br from-amber-50 to-amber-100 dark:from-amber-900/20 dark:to-amber-800/20 rounded-2xl border border-amber-200/50 dark:border-amber-700/30">
|
||||
<div class="group p-5 bg-amber-50 dark:bg-amber-900/20 rounded-2xl border border-amber-200/50 dark:border-amber-700/30">
|
||||
<div class="flex items-center justify-center w-10 h-10 bg-amber-500/20 dark:bg-amber-500/30 rounded-xl mb-3">
|
||||
<i class="fas fa-fill-drip text-amber-600 dark:text-amber-400"></i>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@
|
||||
<span class="text-[10px] font-bold text-white">{{ spriteIndex + 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="layer.sprites.length > 12" class="w-16 h-16 flex flex-col items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-700 dark:to-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600">
|
||||
<div v-if="layer.sprites.length > 12" class="w-16 h-16 flex flex-col items-center justify-center bg-gray-100 dark:bg-gray-700 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600">
|
||||
<span class="text-lg font-bold text-gray-500 dark:text-gray-400">+{{ layer.sprites.length - 12 }}</span>
|
||||
<span class="text-[10px] text-gray-400 dark:text-gray-500">more</span>
|
||||
</div>
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<button @click="loadIntoEditor" class="btn btn-dark px-8 py-3 text-base font-semibold shadow-lg hover:shadow-xl transition-shadow">
|
||||
<button @click="loadIntoEditor" class="btn btn-primary px-8 py-3 text-base font-semibold shadow-lg hover:shadow-xl transition-shadow shadow-indigo-500/20">
|
||||
<i class="fas fa-edit mr-2"></i>
|
||||
Open in editor
|
||||
</button>
|
||||
@@ -139,7 +139,7 @@
|
||||
|
||||
const totalSprites = computed(() => {
|
||||
if (!spritesheetData.value) return 0;
|
||||
return spritesheetData.value.sprites.reduce((sum, layer) => sum + (layer.sprites?.length || 0), 0);
|
||||
return spritesheetData.value.sprites.reduce((sum: number, layer) => sum + (layer.sprites?.length || 0), 0);
|
||||
});
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
|
||||
@@ -6,62 +6,154 @@ export default {
|
||||
extend: {
|
||||
colors: {
|
||||
gray: {
|
||||
900: '#111827', // bg-primary
|
||||
800: '#1F2937', // bg-secondary
|
||||
700: '#374151', // bg-tertiary
|
||||
600: '#4B5563', // border
|
||||
400: '#9CA3AF', // text-secondary
|
||||
200: '#E5E7EB', // text-primary
|
||||
50: '#fafafa',
|
||||
100: '#f4f4f5',
|
||||
200: '#e4e4e7', // border-light
|
||||
300: '#d4d4d8',
|
||||
400: '#a1a1aa', // text-secondary
|
||||
500: '#71717a',
|
||||
600: '#52525b', // border-dark
|
||||
700: '#3f3f46',
|
||||
800: '#27272a', // bg-secondary
|
||||
900: '#18181b', // bg-primary
|
||||
950: '#09090b', // bg-deep
|
||||
},
|
||||
blue: {
|
||||
500: '#3B82F6', // accent
|
||||
600: '#2563EB', // accent-hover
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6', // accent
|
||||
600: '#2563eb', // accent-hover
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
950: '#172554',
|
||||
},
|
||||
red: {
|
||||
500: '#EF4444', // danger
|
||||
600: '#DC2626', // danger-hover
|
||||
d: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
950: '#450a0a',
|
||||
},
|
||||
green: {
|
||||
500: '#10B981', // success
|
||||
600: '#059669', // success-hover
|
||||
s: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
950: '#052e16',
|
||||
}
|
||||
},
|
||||
boxShadow: {
|
||||
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
|
||||
'glass': '0 8px 32px 0 rgba(0, 0, 0, 0.08)',
|
||||
'glow': '0 0 15px rgba(59, 130, 246, 0.5)',
|
||||
},
|
||||
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'Inter',
|
||||
'ui-sans-serif',
|
||||
'system-ui',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'"Noto Sans"',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
'"Noto Color Emoji"'
|
||||
],
|
||||
mono: [
|
||||
'ui-monospace',
|
||||
'SFMono-Regular',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Consolas',
|
||||
'"Liberation Mono"',
|
||||
'"Courier New"',
|
||||
'monospace'
|
||||
],
|
||||
},
|
||||
typography: (theme) => ({
|
||||
DEFAULT: {
|
||||
css: {
|
||||
maxWidth: 'none',
|
||||
color: theme('colors.gray.700'),
|
||||
a: {
|
||||
color: theme('colors.primary.600'),
|
||||
'&:hover': {
|
||||
color: theme('colors.primary.500'),
|
||||
},
|
||||
},
|
||||
h1: {
|
||||
color: theme('colors.gray.900'),
|
||||
letterSpacing: '-0.025em',
|
||||
},
|
||||
h2: {
|
||||
color: theme('colors.gray.900'),
|
||||
letterSpacing: '-0.025em',
|
||||
},
|
||||
h3: {
|
||||
color: theme('colors.gray.900'),
|
||||
},
|
||||
h4: {
|
||||
color: theme('colors.gray.900'),
|
||||
},
|
||||
code: {
|
||||
color: theme('colors.primary.600'),
|
||||
backgroundColor: theme('colors.gray.100'),
|
||||
borderRadius: '0.25rem',
|
||||
paddingLeft: '0.25rem',
|
||||
paddingRight: '0.25rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
css: {
|
||||
color: theme('colors.gray.200'),
|
||||
color: theme('colors.gray.300'),
|
||||
a: {
|
||||
color: theme('colors.blue.500'),
|
||||
color: theme('colors.primary.400'),
|
||||
'&:hover': {
|
||||
color: theme('colors.blue.400'),
|
||||
color: theme('colors.primary.300'),
|
||||
},
|
||||
},
|
||||
h1: {
|
||||
color: theme('colors.gray.200'),
|
||||
color: theme('colors.gray.50'),
|
||||
},
|
||||
h2: {
|
||||
color: theme('colors.gray.200'),
|
||||
color: theme('colors.gray.50'),
|
||||
},
|
||||
h3: {
|
||||
color: theme('colors.gray.200'),
|
||||
color: theme('colors.gray.100'),
|
||||
},
|
||||
h4: {
|
||||
color: theme('colors.gray.200'),
|
||||
color: theme('colors.gray.100'),
|
||||
},
|
||||
code: {
|
||||
color: theme('colors.gray.200'),
|
||||
color: theme('colors.primary.300'),
|
||||
backgroundColor: theme('colors.gray.800'),
|
||||
},
|
||||
pre: {
|
||||
backgroundColor: theme('colors.gray.800'),
|
||||
backgroundColor: theme('colors.gray.900'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user