Almost working

This commit is contained in:
Dennis Postma 2025-04-04 01:59:15 +02:00
parent 3b8d066cd3
commit 9a4cb2d2a1
3 changed files with 670 additions and 103 deletions

190
package-lock.json generated
View File

@ -108,6 +108,16 @@
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/generator": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
@ -155,6 +165,16 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz",
@ -177,6 +197,16 @@
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz",
@ -920,19 +950,6 @@
}
}
},
"node_modules/@ianvs/prettier-plugin-sort-imports/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@ -1296,43 +1313,43 @@
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.1.tgz",
"integrity": "sha512-xvlh4pvfG/bkv0fEtJDABAm1tjtSmSyi2QmS4zyj1EKNI1UiOYiUq1IphSwDsNJ5vJ9cWEGs4rJXpUdCN2kujQ==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.2.tgz",
"integrity": "sha512-ZwFnxH+1z8Ehh8bNTMX3YFrYdzAv7JLY5X5X7XSFY+G9QGJVce/P9xb2mh+j5hKt8NceuHmdtllJvAHWKtsNrQ==",
"license": "MIT",
"dependencies": {
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "1.29.2",
"tailwindcss": "4.1.1"
"tailwindcss": "4.1.2"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.1.tgz",
"integrity": "sha512-7+YBgnPQ4+jv6B6WVOerJ6WOzDzNJXrRKDts674v6TKAqFqYRr9+EBtSziO7nNcwQ8JtoZNMeqA+WJDjtCM/7w==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.2.tgz",
"integrity": "sha512-Zwz//1QKo6+KqnCKMT7lA4bspGfwEgcPAHlSthmahtgrpKDfwRGk8PKQrW8Zg/ofCDIlg6EtjSTKSxxSufC+CQ==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.1",
"@tailwindcss/oxide-darwin-arm64": "4.1.1",
"@tailwindcss/oxide-darwin-x64": "4.1.1",
"@tailwindcss/oxide-freebsd-x64": "4.1.1",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.1",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.1",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.1",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.1",
"@tailwindcss/oxide-linux-x64-musl": "4.1.1",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.1",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.1"
"@tailwindcss/oxide-android-arm64": "4.1.2",
"@tailwindcss/oxide-darwin-arm64": "4.1.2",
"@tailwindcss/oxide-darwin-x64": "4.1.2",
"@tailwindcss/oxide-freebsd-x64": "4.1.2",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.2",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.2",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.2",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.2",
"@tailwindcss/oxide-linux-x64-musl": "4.1.2",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.2",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.2"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.1.tgz",
"integrity": "sha512-gTyRzfdParpoCU1yyUC/iN6XK6T0Ra4bDlF8Aeul5NP9cLzKEZDogdNVNGv5WZmCDkVol7qlex7TMmcfytMmmw==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.2.tgz",
"integrity": "sha512-IxkXbntHX8lwGmwURUj4xTr6nezHhLYqeiJeqa179eihGv99pRlKV1W69WByPJDQgSf4qfmwx904H6MkQqTA8w==",
"cpu": [
"arm64"
],
@ -1346,9 +1363,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.1.tgz",
"integrity": "sha512-dI0QbdMWBvLB3MtaTKetzUKG9CUUQow8JSP4Nm+OxVokeZ+N+f1OmZW/hW1LzMxpx9RQCBgSRL+IIvKRat5Wdg==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.2.tgz",
"integrity": "sha512-ZRtiHSnFYHb4jHKIdzxlFm6EDfijTCOT4qwUhJ3GWxfDoW2yT3z/y8xg0nE7e72unsmSj6dtfZ9Y5r75FIrlpA==",
"cpu": [
"arm64"
],
@ -1362,9 +1379,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.1.tgz",
"integrity": "sha512-2Y+NPQOTRBCItshPgY/CWg4bKi7E9evMg4bgdb6h9iZObCZLOe3doPcuSxGS3DB0dKyMFKE8pTdWtFUbxZBMSA==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.2.tgz",
"integrity": "sha512-BiKUNZf1A0pBNzndBvnPnBxonCY49mgbOsPfILhcCE5RM7pQlRoOgN7QnwNhY284bDbfQSEOWnFR0zbPo6IDTw==",
"cpu": [
"x64"
],
@ -1378,9 +1395,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.1.tgz",
"integrity": "sha512-N97NGMsB/7CHShbc5ube4dcsW/bYENkBrg8yWi8ieN9boYVRdw3cZviVryV/Nfu9bKbBV9kUvduFF2qBI7rEqg==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.2.tgz",
"integrity": "sha512-Z30VcpUfRGkiddj4l5NRCpzbSGjhmmklVoqkVQdkEC0MOelpY+fJrVhzSaXHmWrmSvnX8yiaEqAbdDScjVujYQ==",
"cpu": [
"x64"
],
@ -1394,9 +1411,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.1.tgz",
"integrity": "sha512-33Lk6KbHnUZbXqza6RWNFo9wqPQ4+H5BAn1CkUUfC1RZ1vYbyDN6+iJPj53wmnWJ3mhRI8jWt3Jt1fO02IVdUQ==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.2.tgz",
"integrity": "sha512-w3wsK1ChOLeQ3gFOiwabtWU5e8fY3P1Ss8jR3IFIn/V0va3ir//hZ8AwURveS4oK1Pu6b8i+yxesT4qWnLVUow==",
"cpu": [
"arm"
],
@ -1410,9 +1427,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.1.tgz",
"integrity": "sha512-LyW35RzSUy+80WYScv03HKasAUmMFDaSbNpWfk1gG5gEE9kuRGnDzSrqMoLAmY/kzMCYP/1kqmUiAx8EFLkI2A==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.2.tgz",
"integrity": "sha512-oY/u+xJHpndTj7B5XwtmXGk8mQ1KALMfhjWMMpE8pdVAznjJsF5KkCceJ4Fmn5lS1nHMCwZum5M3/KzdmwDMdw==",
"cpu": [
"arm64"
],
@ -1426,9 +1443,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.1.tgz",
"integrity": "sha512-1KPnDMlHdqjPTUSFjx55pafvs8RZXRgxfeRgUrukwDKkuj7gFk28vW3Mx65YdiugAc9NWs3VgueZWaM1Po6uGw==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.2.tgz",
"integrity": "sha512-k7G6vcRK/D+JOWqnKzKN/yQq1q4dCkI49fMoLcfs2pVcaUAXEqCP9NmA8Jv+XahBv5DtDjSAY3HJbjosEdKczg==",
"cpu": [
"arm64"
],
@ -1442,9 +1459,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.1.tgz",
"integrity": "sha512-4WdzA+MRlsinEEE6yxNMLJxpw0kE9XVipbAKdTL8BeUpyC2TdA3TL46lBulXzKp3BIxh3nqyR/UCqzl5o+3waQ==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.2.tgz",
"integrity": "sha512-fLL+c678TkYKgkDLLNxSjPPK/SzTec7q/E5pTwvpTqrth867dftV4ezRyhPM5PaiCqX651Y8Yk0wRQMcWUGnmQ==",
"cpu": [
"x64"
],
@ -1458,9 +1475,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.1.tgz",
"integrity": "sha512-q7Ugbw3ARcjCW2VMUYrcMbJ6aMQuWPArBBE2EqC/swPZTdGADvMQSlvR0VKusUM4HoSsO7ZbvcZ53YwR57+AKw==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.2.tgz",
"integrity": "sha512-0tU1Vjd1WucZ2ooq6y4nI9xyTSaH2g338bhrqk+2yzkMHskBm+pMsOCfY7nEIvALkA1PKPOycR4YVdlV7Czo+A==",
"cpu": [
"x64"
],
@ -1474,9 +1491,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.1.tgz",
"integrity": "sha512-0KpqsovgHcIzm7eAGzzEZsEs0/nPYXnRBv+aPq/GehpNQuE/NAQu+YgZXIIof+VflDFuyXOEnaFr7T5MZ1INhA==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.2.tgz",
"integrity": "sha512-r8QaMo3QKiHqUcn+vXYCypCEha+R0sfYxmaZSgZshx9NfkY+CHz91aS2xwNV/E4dmUDkTPUag7sSdiCHPzFVTg==",
"cpu": [
"arm64"
],
@ -1490,9 +1507,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.1.tgz",
"integrity": "sha512-B1mjeXNS26kBOHv5sXARf6Wd0PWHV9x1TDlW0ummrBUOUAxAy5wcy4Nii1wzNvCdvC448hgiL06ylhwAbNthmg==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.2.tgz",
"integrity": "sha512-lYCdkPxh9JRHXoBsPE8Pu/mppUsC2xihYArNAESub41PKhHTnvn6++5RpmFM+GLSt3ewyS8fwCVvht7ulWm6cw==",
"cpu": [
"x64"
],
@ -1506,14 +1523,14 @@
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.1.tgz",
"integrity": "sha512-tFTkRZwXq4XKr3S2dUZBxy80wbWYHdDSsu4QOB1yE1HJFKjfxKVpXtup4dyTVdQcLInoHC9lZXFPHnjoBP774g==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.2.tgz",
"integrity": "sha512-3r/ZdMW0gxY8uOx1To0lpYa4coq4CzINcCX4laM1rS340Kcn0ac4A/MMFfHN8qba51aorZMYwMcOxYk4wJ9FYg==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.1",
"@tailwindcss/oxide": "4.1.1",
"tailwindcss": "4.1.1"
"@tailwindcss/node": "4.1.2",
"@tailwindcss/oxide": "4.1.2",
"tailwindcss": "4.1.2"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6"
@ -1997,9 +2014,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001707",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
"version": "1.0.30001709",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001709.tgz",
"integrity": "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==",
"dev": true,
"funding": [
{
@ -2161,9 +2178,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.130",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.130.tgz",
"integrity": "sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA==",
"version": "1.5.131",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.131.tgz",
"integrity": "sha512-fJFRYXVEJgDCiqFOgRGJm8XR97hZ13tw7FXI9k2yC5hgY+nyzC2tMO8baq1cQR7Ur58iCkASx2zrkZPZUnfzPg==",
"dev": true,
"license": "ISC"
},
@ -3256,13 +3273,16 @@
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
@ -3373,9 +3393,9 @@
}
},
"node_modules/tailwindcss": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.1.tgz",
"integrity": "sha512-QNbdmeS979Efzim2g/bEvfuh+fTcIdp1y7gA+sb6OYSW74rt7Cr7M78AKdf6HqWT3d5AiTb7SwTT3sLQxr4/qw==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.2.tgz",
"integrity": "sha512-VCsK+fitIbQF7JlxXaibFhxrPq4E2hDcG8apzHUdWFMCQWD8uLdlHg4iSkZ53cgLCCcZ+FZK7vG8VjvLcnBgKw==",
"license": "MIT"
},
"node_modules/tapable": {
@ -3473,9 +3493,9 @@
}
},
"node_modules/vite": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",

View File

@ -2,7 +2,17 @@
<!-- Make the outer container always pointer-events-none so clicks pass through -->
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none">
<!-- Apply pointer-events-auto ONLY to the modal itself so it can be interacted with -->
<div class="bg-gray-800 rounded-lg max-w-4xl max-h-[90vh] overflow-auto scrollbar-hide shadow-lg pointer-events-auto" :class="{ invisible: !isModalOpen, visible: isModalOpen }" :style="{ transform: `translate3d(${position.x}px, ${position.y + (isModalOpen ? 0 : -20)}px, 0)` }">
<div
class="bg-gray-800 rounded-lg overflow-auto scrollbar-hide shadow-lg pointer-events-auto relative"
:class="{ invisible: !isModalOpen, visible: isModalOpen }"
:style="{
transform: `translate3d(${position.x}px, ${position.y + (isModalOpen ? 0 : -20)}px, 0)`,
width: `${modalSize.width}px`,
height: `${modalSize.height}px`,
maxWidth: '90vw',
maxHeight: '90vh',
}"
>
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600 cursor-move" @mousedown="startDrag">
<div class="flex items-center gap-2 text-lg font-semibold select-none">
<i class="fas fa-film text-blue-500"></i>
@ -13,7 +23,7 @@
</button>
</div>
<div class="p-6">
<div class="p-6 flex flex-col h-[calc(100%-64px)]">
<div class="flex flex-wrap items-center gap-4 mb-6">
<div class="flex gap-2">
<button
@ -55,16 +65,82 @@
</div>
</div>
<div class="flex justify-center bg-gray-700 p-6 rounded mb-6">
<canvas ref="animCanvas" class="block" style="image-rendering: pixelated"></canvas>
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
<!-- Zoom controls -->
<div class="flex items-center gap-2">
<button @click="zoomOut" :disabled="previewZoom <= 1" class="flex items-center justify-center w-8 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
<i class="fas fa-search-minus"></i>
</button>
<span class="text-sm text-gray-300">{{ Math.round(previewZoom * 100) }}%</span>
<button @click="zoomIn" :disabled="previewZoom >= 5" class="flex items-center justify-center w-8 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
<i class="fas fa-search-plus"></i>
</button>
<button @click="resetZoom" :disabled="previewZoom === 1" class="flex items-center justify-center px-2 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded text-xs transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">Reset Zoom</button>
</div>
<!-- Controls -->
<div class="flex items-center gap-4">
<!-- Show all sprites toggle -->
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
<input type="checkbox" v-model="showAllSprites" class="form-checkbox h-4 w-4 text-blue-500 rounded border-gray-600 bg-gray-700 focus:ring-blue-500" />
Show all frames
</label>
<!-- Reset sprite position button -->
<button
@click="resetSpritePosition"
:disabled="!hasSpriteOffset"
class="flex items-center gap-1 px-2 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded text-xs transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
title="Reset sprite to original position"
>
<i class="fas fa-crosshairs"></i>
Reset Position
</button>
</div>
</div>
<div class="flex flex-col justify-center items-center bg-gray-700 p-6 rounded mb-6 relative overflow-auto flex-grow">
<!-- Tooltip for dragging instructions -->
<div class="text-xs text-gray-400 mb-2" v-if="hasSpriteOffset || sprites.length > 0">
<span v-if="hasSpriteOffset">Sprite offset: {{ Math.round(spriteOffset.value.x) }}px, {{ Math.round(spriteOffset.value.y) }}px</span>
<span v-else>Click and drag the sprite to move it within the cell</span>
</div>
<div
class="canvas-container relative transition-transform duration-100 flex items-center justify-center"
:style="{
minWidth: `${store.cellSize.width * previewZoom}px`,
minHeight: `${store.cellSize.height * previewZoom}px`,
cursor: previewZoom > 1 ? (isViewportDragging ? 'grabbing' : 'grab') : 'default',
}"
@mousedown="startViewportDrag"
@wheel="handleCanvasWheel"
>
<div
class="sprite-wrapper"
:style="{
transform: `scale(${previewZoom}) translate(${viewportOffset.x}px, ${viewportOffset.y}px)`,
cursor: isCanvasDragging ? 'grabbing' : 'move',
}"
@mousedown.stop="startCanvasDrag"
title="Drag to move sprite within cell"
>
<canvas ref="animCanvas" class="block" style="image-rendering: pixelated"></canvas>
</div>
</div>
</div>
</div>
<!-- Resize handle - larger and more noticeable -->
<div class="absolute bottom-0 right-0 w-8 h-8 cursor-se-resize flex items-end justify-end bg-gradient-to-br from-transparent to-gray-700 hover:to-blue-500 transition-colors duration-200" @mousedown="startResize">
<i class="fas fa-grip-lines-diagonal text-gray-400 hover:text-white p-1"></i>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
const store = useSpritesheetStore();
@ -80,12 +156,55 @@
const isDragging = ref(false);
const dragOffset = ref({ x: 0, y: 0 });
// New state for added features
const previewZoom = ref(1);
const showAllSprites = ref(false);
// Use a computed property to access and modify the store's sprite offset for the current frame
const spriteOffset = computed({
get: () => {
// Get the offset for the current frame
const frameOffset = store.getSpriteOffset(currentFrame.value);
// Update the current offset for UI display
store.currentSpriteOffset.x = frameOffset.x;
store.currentSpriteOffset.y = frameOffset.y;
return store.currentSpriteOffset;
},
set: val => {
// Update both the current offset and the frame-specific offset
store.currentSpriteOffset.x = val.x;
store.currentSpriteOffset.y = val.y;
// Get the frame-specific offset and update it
const frameOffset = store.getSpriteOffset(currentFrame.value);
frameOffset.x = val.x;
frameOffset.y = val.y;
},
});
const isCanvasDragging = ref(false);
const canvasDragStart = ref({ x: 0, y: 0 });
// Canvas viewport navigation state
const viewportOffset = ref({ x: 0, y: 0 });
const isViewportDragging = ref(false);
const viewportDragStart = ref({ x: 0, y: 0 });
// Modal size state for resize functionality
const modalSize = ref({ width: 800, height: 600 });
const isResizing = ref(false);
const initialSize = ref({ width: 0, height: 0 });
const resizeStart = ref({ x: 0, y: 0 });
const currentFrameDisplay = computed(() => {
const totalFrames = Math.max(1, sprites.value.length);
const frame = Math.min(currentFrame.value + 1, totalFrames);
return `${frame} / ${totalFrames}`;
});
// Computed property to check if sprite has been moved from original position
const hasSpriteOffset = computed(() => {
return spriteOffset.value.x !== 0 || spriteOffset.value.y !== 0;
});
const handleKeyDown = (e: KeyboardEvent) => {
if (!isModalOpen.value) return;
@ -107,6 +226,51 @@
// Previous frame
currentFrame.value = (currentFrame.value - 1 + sprites.value.length) % sprites.value.length;
updateFrame();
} else if (e.key === '+' || e.key === '=') {
// Zoom in
zoomIn();
e.preventDefault();
} else if (e.key === '-' || e.key === '_') {
// Zoom out
zoomOut();
e.preventDefault();
} else if (e.key === '0') {
// Reset zoom
resetZoom();
e.preventDefault();
} else if (e.key === 'r') {
if (e.shiftKey) {
// Shift+R: Reset sprite position only
resetSpritePosition();
} else {
// R: Reset both sprite position and viewport
// Reset the sprite offset for the current frame
const frameOffset = store.getSpriteOffset(currentFrame.value);
frameOffset.x = 0;
frameOffset.y = 0;
store.currentSpriteOffset.x = 0;
store.currentSpriteOffset.y = 0;
viewportOffset.value = { x: 0, y: 0 };
updateFrame();
store.showNotification('View and position reset');
}
e.preventDefault();
} else if (previewZoom.value > 1) {
// Arrow key navigation for panning when zoomed in
const panAmount = 10;
if (e.key === 'ArrowUp') {
viewportOffset.value.y += panAmount / previewZoom.value;
e.preventDefault();
} else if (e.key === 'ArrowDown') {
viewportOffset.value.y -= panAmount / previewZoom.value;
e.preventDefault();
} else if (e.key === 'ArrowLeft' && animation.value.isPlaying) {
viewportOffset.value.x += panAmount / previewZoom.value;
e.preventDefault();
} else if (e.key === 'ArrowRight' && animation.value.isPlaying) {
viewportOffset.value.x -= panAmount / previewZoom.value;
e.preventDefault();
}
}
};
@ -116,8 +280,20 @@
return;
}
// Reset position when opening
position.value = { x: 0, y: 0 };
// Center the modal in the viewport
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
position.value = {
x: (viewportWidth - modalSize.value.width) / 2,
y: (viewportHeight - modalSize.value.height) / 2,
};
// Reset zoom but keep sprite offset if it exists
previewZoom.value = 1;
viewportOffset.value = { x: 0, y: 0 };
// Reset modal size to default
modalSize.value = { width: 800, height: 600 };
// Reset to first frame
currentFrame.value = 0;
@ -138,7 +314,15 @@
// Force render the first frame
if (sprites.value.length > 0) {
store.renderAnimationFrame(0);
// Get the frame-specific offset for the first frame
const frameOffset = store.getSpriteOffset(0);
// Update the current offset for UI display
store.currentSpriteOffset.x = frameOffset.x;
store.currentSpriteOffset.y = frameOffset.y;
// Render with the frame-specific offset
store.renderAnimationFrame(0, showAllSprites.value, frameOffset);
}
};
@ -146,6 +330,9 @@
if (animCanvas.value && store.cellSize.width && store.cellSize.height) {
animCanvas.value.width = store.cellSize.width;
animCanvas.value.height = store.cellSize.height;
// Also update container size
updateCanvasContainerSize();
}
};
@ -178,7 +365,12 @@
const updateFrame = () => {
animation.value.currentFrame = currentFrame.value;
animation.value.manualUpdate = true;
store.renderAnimationFrame(currentFrame.value);
// Get the frame-specific offset
const frameOffset = store.getSpriteOffset(currentFrame.value);
// Render with the frame-specific offset
store.renderAnimationFrame(currentFrame.value, showAllSprites.value, frameOffset);
};
const handleFrameRateChange = () => {
@ -189,7 +381,11 @@
}
};
// Modal drag functionality
const startDrag = (e: MouseEvent) => {
// Don't allow drag if currently resizing
if (isResizing.value) return;
isDragging.value = true;
dragOffset.value = {
x: e.clientX - position.value.x,
@ -218,6 +414,45 @@
window.removeEventListener('mouseup', stopDrag);
};
// NEW: Modal resize functionality
const startResize = (e: MouseEvent) => {
isResizing.value = true;
initialSize.value = { ...modalSize.value };
resizeStart.value = { x: e.clientX, y: e.clientY };
// Add temporary event listeners
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', stopResize);
// Prevent default to avoid text selection
e.preventDefault();
};
const handleResize = (e: MouseEvent) => {
if (!isResizing.value) return;
const deltaX = e.clientX - resizeStart.value.x;
const deltaY = e.clientY - resizeStart.value.y;
requestAnimationFrame(() => {
// Calculate new size with minimum constraints
const newWidth = Math.max(400, initialSize.value.width + deltaX);
const newHeight = Math.max(400, initialSize.value.height + deltaY);
// Update modal size
modalSize.value = {
width: newWidth,
height: newHeight,
};
});
};
const stopResize = () => {
isResizing.value = false;
window.removeEventListener('mousemove', handleResize);
window.removeEventListener('mouseup', stopResize);
};
const initializeCanvas = async () => {
if (!animCanvas.value) {
console.error('PreviewModal: Animation canvas not found');
@ -238,6 +473,197 @@
}
};
// Zoom functions
const zoomIn = () => {
if (previewZoom.value < 5) {
previewZoom.value = Math.min(5, previewZoom.value + 0.5);
// Adjust container size after zoom change
nextTick(() => {
updateCanvasContainerSize();
});
}
};
const zoomOut = () => {
if (previewZoom.value > 1) {
previewZoom.value = Math.max(1, previewZoom.value - 0.5);
// Adjust container size after zoom change
nextTick(() => {
updateCanvasContainerSize();
});
}
};
const resetZoom = () => {
previewZoom.value = 1;
viewportOffset.value = { x: 0, y: 0 };
// Adjust container size after zoom change
nextTick(() => {
updateCanvasContainerSize();
});
};
// Reset sprite position to original
const resetSpritePosition = () => {
// Reset the sprite offset for the current frame to zero
const frameOffset = store.getSpriteOffset(currentFrame.value);
frameOffset.x = 0;
frameOffset.y = 0;
// Also update the current offset
store.currentSpriteOffset.x = 0;
store.currentSpriteOffset.y = 0;
// Update the frame to reflect the change
updateFrame();
// Show a notification
store.showNotification('Sprite position reset to original');
};
// Update canvas container size based on zoom level
const updateCanvasContainerSize = () => {
// This ensures the container grows with zoom while keeping the sprite visible
if (!store.cellSize.width || !store.cellSize.height) return;
// We'll update the container if needed through the reactive bindings
};
// Canvas drag functions for moving the sprite within its cell
const startCanvasDrag = (e: MouseEvent) => {
if (sprites.value.length === 0) return;
// Don't start sprite dragging if we're already dragging the viewport
if (isViewportDragging.value) return;
isCanvasDragging.value = true;
canvasDragStart.value = {
x: e.clientX,
y: e.clientY,
};
// Add temporary event listeners
window.addEventListener('mousemove', handleCanvasDrag);
window.addEventListener('mouseup', stopCanvasDrag);
// Prevent default to avoid text selection
e.preventDefault();
};
const handleCanvasDrag = (e: MouseEvent) => {
if (!isCanvasDragging.value) return;
const deltaX = e.clientX - canvasDragStart.value.x;
const deltaY = e.clientY - canvasDragStart.value.y;
// Update sprite offset with the delta, scaled by zoom level
// Use requestAnimationFrame for smoother dragging
requestAnimationFrame(() => {
// Get the frame-specific offset
const frameOffset = store.getSpriteOffset(currentFrame.value);
// Update both the current offset and the frame-specific offset
const newX = frameOffset.x + deltaX / previewZoom.value;
const newY = frameOffset.y + deltaY / previewZoom.value;
frameOffset.x = newX;
frameOffset.y = newY;
store.currentSpriteOffset.x = newX;
store.currentSpriteOffset.y = newY;
// Reset drag start position
canvasDragStart.value = {
x: e.clientX,
y: e.clientY,
};
// Update the frame with the new offset
updateFrame();
});
};
const stopCanvasDrag = () => {
isCanvasDragging.value = false;
window.removeEventListener('mousemove', handleCanvasDrag);
window.removeEventListener('mouseup', stopCanvasDrag);
};
// Canvas viewport navigation functions
const startViewportDrag = (e: MouseEvent) => {
// Only enable viewport dragging when zoomed in
if (previewZoom.value <= 1 || isCanvasDragging.value) return;
isViewportDragging.value = true;
viewportDragStart.value = {
x: e.clientX,
y: e.clientY,
};
// Add temporary event listeners
window.addEventListener('mousemove', handleViewportDrag);
window.addEventListener('mouseup', stopViewportDrag);
// Prevent default to avoid text selection
e.preventDefault();
};
const handleViewportDrag = (e: MouseEvent) => {
if (!isViewportDragging.value) return;
const deltaX = e.clientX - viewportDragStart.value.x;
const deltaY = e.clientY - viewportDragStart.value.y;
// Update viewport offset with the delta, scaled by zoom level
viewportOffset.value = {
x: viewportOffset.value.x + deltaX / previewZoom.value,
y: viewportOffset.value.y + deltaY / previewZoom.value,
};
// Reset drag start position
viewportDragStart.value = {
x: e.clientX,
y: e.clientY,
};
};
const stopViewportDrag = () => {
isViewportDragging.value = false;
window.removeEventListener('mousemove', handleViewportDrag);
window.removeEventListener('mouseup', stopViewportDrag);
};
// Handle mouse wheel zooming and panning
const handleCanvasWheel = (e: WheelEvent) => {
// Prevent the default scroll behavior
e.preventDefault();
if (e.ctrlKey) {
// Ctrl + wheel = zoom in/out
if (e.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
} else {
// Just wheel = pan when zoomed in
if (previewZoom.value > 1) {
// Adjust pan amount by zoom level
const panFactor = 0.5 / previewZoom.value;
if (e.shiftKey) {
// Shift + wheel = horizontal pan
viewportOffset.value.x -= e.deltaY * panFactor;
} else {
// Regular wheel = vertical pan
viewportOffset.value.y -= e.deltaY * panFactor;
}
}
}
};
onMounted(() => {
initializeCanvas();
window.addEventListener('keydown', handleKeyDown);
@ -247,6 +673,12 @@
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('mousemove', handleDrag);
window.removeEventListener('mouseup', stopDrag);
window.removeEventListener('mousemove', handleCanvasDrag);
window.removeEventListener('mouseup', stopCanvasDrag);
window.removeEventListener('mousemove', handleResize);
window.removeEventListener('mouseup', stopResize);
window.removeEventListener('mousemove', handleViewportDrag);
window.removeEventListener('mouseup', stopViewportDrag);
});
// Keep currentFrame in sync with animation.currentFrame
@ -263,23 +695,42 @@
newSprites => {
if (isModalOpen.value && newSprites.length > 0) {
updateCanvasSize();
store.renderAnimationFrame(currentFrame.value);
updateCanvasContainerSize();
store.renderAnimationFrame(currentFrame.value, showAllSprites.value, spriteOffset.value);
}
},
{ deep: true }
);
// Watch zoom changes to update container size
watch(
() => previewZoom.value,
() => {
updateCanvasContainerSize();
}
);
// Watch for changes in border settings to update the preview
watch(
() => previewBorder.value,
() => {
if (isModalOpen.value && sprites.value.length > 0) {
store.renderAnimationFrame(currentFrame.value);
store.renderAnimationFrame(currentFrame.value, showAllSprites.value, spriteOffset.value);
}
},
{ deep: true }
);
// Watch for changes in showAllSprites to update the preview
watch(
() => showAllSprites.value,
() => {
if (isModalOpen.value && sprites.value.length > 0) {
store.renderAnimationFrame(currentFrame.value, showAllSprites.value, spriteOffset.value);
}
}
);
// Expose openModal for external use
defineExpose({ openModal });
</script>
@ -316,4 +767,21 @@
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
/* Cursor styles */
.cursor-se-resize {
cursor: se-resize;
}
/* Canvas container styles */
.canvas-container {
margin: auto;
transition:
min-width 0.2s,
min-height 0.2s;
}
.sprite-wrapper {
transform-origin: center;
}
</style>

View File

@ -423,6 +423,7 @@ export function useSpritesheetStore() {
animation.canvas.width = cellSize.width;
animation.canvas.height = cellSize.height;
// Start the animation loop without resetting sprite offset
animationLoop();
}
@ -435,7 +436,7 @@ export function useSpritesheetStore() {
}
}
function renderAnimationFrame(frameIndex: number) {
function renderAnimationFrame(frameIndex: number, showAllSprites = false, spriteOffset = { x: 0, y: 0 }) {
if (sprites.value.length === 0 || !animation.canvas || !animation.ctx) return;
if (animation.canvas.width !== cellSize.width || animation.canvas.height !== cellSize.height) {
@ -449,17 +450,68 @@ export function useSpritesheetStore() {
animation.ctx.fillStyle = 'transparent';
animation.ctx.fillRect(0, 0, animation.canvas.width, animation.canvas.height);
// Keep pixel art sharp
animation.ctx.imageSmoothingEnabled = false;
// If showAllSprites is enabled, draw all sprites with transparency
if (showAllSprites && sprites.value.length > 1) {
// Save the current context state
animation.ctx.save();
// Set global alpha for background sprites
animation.ctx.globalAlpha = 0.3;
// Draw all sprites except the current one
sprites.value.forEach((sprite, index) => {
if (index !== frameIndex) {
const spriteCellX = Math.floor(sprite.x / cellSize.width);
const spriteCellY = Math.floor(sprite.y / cellSize.height);
// Calculate precise offset for pixel-perfect rendering
const spriteOffsetX = Math.round(sprite.x - spriteCellX * cellSize.width);
const spriteOffsetY = Math.round(sprite.y - spriteCellY * cellSize.height);
// Draw the sprite with transparency
animation.ctx.drawImage(sprite.img, spriteOffsetX, spriteOffsetY);
}
});
// Restore the context to full opacity
animation.ctx.restore();
}
// Get the current sprite
const currentSprite = sprites.value[frameIndex % sprites.value.length];
const cellX = Math.floor(currentSprite.x / cellSize.width);
const cellY = Math.floor(currentSprite.y / cellSize.height);
// Calculate precise offset for pixel-perfect rendering
const offsetX = Math.round(currentSprite.x - cellX * cellSize.width);
const offsetY = Math.round(currentSprite.y - cellY * cellSize.height);
// Calculate the original position (without user offset)
const originalOffsetX = Math.round(currentSprite.x - cellX * cellSize.width);
const originalOffsetY = Math.round(currentSprite.y - cellY * cellSize.height);
// Keep pixel art sharp
animation.ctx.imageSmoothingEnabled = false;
// Calculate precise offset for pixel-perfect rendering, including the user's drag offset
const offsetX = originalOffsetX + spriteOffset.x;
const offsetY = originalOffsetY + spriteOffset.y;
// If the sprite has been moved from its original position, draw a faint outline at the original position
if (spriteOffset.x !== 0 || spriteOffset.y !== 0) {
// Save context state
animation.ctx.save();
// Draw a faint outline or indicator at the original position
animation.ctx.globalAlpha = 0.2;
animation.ctx.strokeStyle = '#00FFFF'; // Cyan color for the original position indicator
animation.ctx.lineWidth = 1;
// Draw a rectangle representing the sprite's original position
animation.ctx.strokeRect(originalOffsetX, originalOffsetY, currentSprite.width, currentSprite.height);
// Restore context state
animation.ctx.restore();
}
// Draw the current sprite at full opacity at the new position
animation.ctx.drawImage(currentSprite.img, offsetX, offsetY);
// Draw border around the cell if enabled (only for preview, not included in download)
@ -477,6 +529,21 @@ export function useSpritesheetStore() {
}
}
// Store the current sprite offset for animation playback
// We'll use a Map to store offsets for each frame, so they're preserved when switching frames
const spriteOffsets = reactive(new Map<number, { x: number; y: number }>());
// Current sprite offset is a reactive object that will be used for the current frame
const currentSpriteOffset = reactive({ x: 0, y: 0 });
// Helper function to get the offset for a specific frame
function getSpriteOffset(frameIndex: number) {
if (!spriteOffsets.has(frameIndex)) {
spriteOffsets.set(frameIndex, { x: 0, y: 0 });
}
return spriteOffsets.get(frameIndex)!;
}
function animationLoop(timestamp?: number) {
if (!animation.isPlaying) return;
@ -488,8 +555,17 @@ export function useSpritesheetStore() {
animation.lastFrameTime = currentTime;
if (sprites.value.length > 0) {
renderAnimationFrame(animation.currentFrame);
// Get the stored offset for the current frame
const frameOffset = getSpriteOffset(animation.currentFrame);
// Update the current offset for rendering
currentSpriteOffset.x = frameOffset.x;
currentSpriteOffset.y = frameOffset.y;
// Render the current frame with its offset
renderAnimationFrame(animation.currentFrame, false, frameOffset);
// Move to the next frame
animation.currentFrame = (animation.currentFrame + 1) % sprites.value.length;
if (animation.slider) {
@ -549,6 +625,9 @@ export function useSpritesheetStore() {
notification,
zoomLevel,
previewBorder,
currentSpriteOffset,
spriteOffsets,
getSpriteOffset,
addSprites,
updateCellSize,
updateCanvasSize,