mirror of
https://github.com/abusoww/tuxmate.git
synced 2026-04-17 15:53:24 +02:00
feat: added sidebar, removed verbose comments, fixed npm audits
This commit is contained in:
384
package-lock.json
generated
384
package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"framer-motion": "^12.23.26",
|
||||
"gsap": "^3.14.2",
|
||||
"lucide-react": "^0.561.0",
|
||||
"next": "16.0.10",
|
||||
"next": "^16.1.6",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
@@ -26,7 +26,7 @@
|
||||
"@types/react-dom": "^19",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.10",
|
||||
"eslint-config-next": "^16.1.6",
|
||||
"jsdom": "^27.4.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5",
|
||||
@@ -1018,9 +1018,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
||||
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1125,9 +1125,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.39.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
|
||||
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
|
||||
"version": "9.39.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz",
|
||||
"integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1761,15 +1761,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
|
||||
"integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
|
||||
"integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.10.tgz",
|
||||
"integrity": "sha512-b2NlWN70bbPLmfyoLvvidPKWENBYYIe017ZGUpElvQjDytCWgxPJx7L9juxHt0xHvNVA08ZHJdOyhGzon/KJuw==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz",
|
||||
"integrity": "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1777,9 +1777,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz",
|
||||
"integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
|
||||
"integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1793,9 +1793,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz",
|
||||
"integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
|
||||
"integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1809,9 +1809,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz",
|
||||
"integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
|
||||
"integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1825,9 +1825,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz",
|
||||
"integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
|
||||
"integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1841,9 +1841,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz",
|
||||
"integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
|
||||
"integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1857,9 +1857,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz",
|
||||
"integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
|
||||
"integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1873,9 +1873,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz",
|
||||
"integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
|
||||
"integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1889,9 +1889,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz",
|
||||
"integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
|
||||
"integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2782,20 +2782,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
|
||||
"integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
|
||||
"integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/type-utils": "8.50.0",
|
||||
"@typescript-eslint/utils": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
||||
"ignore": "^7.0.0",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/type-utils": "8.56.0",
|
||||
"@typescript-eslint/utils": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2805,8 +2805,8 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.50.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"@typescript-eslint/parser": "^8.56.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
@@ -2821,17 +2821,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
|
||||
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz",
|
||||
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
||||
"debug": "^4.3.4"
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2841,20 +2841,20 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
|
||||
"integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz",
|
||||
"integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.50.0",
|
||||
"@typescript-eslint/types": "^8.50.0",
|
||||
"debug": "^4.3.4"
|
||||
"@typescript-eslint/tsconfig-utils": "^8.56.0",
|
||||
"@typescript-eslint/types": "^8.56.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2868,14 +2868,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
|
||||
"integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz",
|
||||
"integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0"
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2886,9 +2886,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
|
||||
"integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz",
|
||||
"integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2903,17 +2903,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
|
||||
"integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz",
|
||||
"integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
||||
"@typescript-eslint/utils": "8.50.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0",
|
||||
"@typescript-eslint/utils": "8.56.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2923,14 +2923,14 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz",
|
||||
"integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz",
|
||||
"integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2942,21 +2942,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
|
||||
"integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz",
|
||||
"integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.50.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
||||
"debug": "^4.3.4",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"@typescript-eslint/project-service": "8.56.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^9.0.5",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2969,36 +2969,10 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
@@ -3009,16 +2983,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz",
|
||||
"integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz",
|
||||
"integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.50.0",
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0"
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3028,19 +3002,19 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
|
||||
"integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz",
|
||||
"integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.50.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3050,6 +3024,19 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
|
||||
@@ -3485,9 +3472,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3769,17 +3756,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz",
|
||||
"integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.8",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.8.tgz",
|
||||
"integrity": "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
@@ -3796,14 +3785,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
|
||||
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@@ -3995,13 +3986,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -4558,9 +4542,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.39.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"version": "9.39.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz",
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4570,7 +4554,7 @@
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@eslint/js": "9.39.3",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -4618,13 +4602,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-next": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.10.tgz",
|
||||
"integrity": "sha512-BxouZUm0I45K4yjOOIzj24nTi0H2cGo0y7xUmk+Po/PYtJXFBYVDS1BguE7t28efXjKdcN0tmiLivxQy//SsZg==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz",
|
||||
"integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "16.0.10",
|
||||
"@next/eslint-plugin-next": "16.1.6",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
"eslint-import-resolver-typescript": "^3.5.2",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
@@ -5053,9 +5037,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -6618,16 +6602,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
|
||||
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
@@ -6704,13 +6691,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
|
||||
"integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==",
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
|
||||
"integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "16.0.10",
|
||||
"@next/env": "16.1.6",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
"styled-jsx": "5.1.6"
|
||||
@@ -6722,14 +6710,14 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "16.0.10",
|
||||
"@next/swc-darwin-x64": "16.0.10",
|
||||
"@next/swc-linux-arm64-gnu": "16.0.10",
|
||||
"@next/swc-linux-arm64-musl": "16.0.10",
|
||||
"@next/swc-linux-x64-gnu": "16.0.10",
|
||||
"@next/swc-linux-x64-musl": "16.0.10",
|
||||
"@next/swc-win32-arm64-msvc": "16.0.10",
|
||||
"@next/swc-win32-x64-msvc": "16.0.10",
|
||||
"@next/swc-darwin-arm64": "16.1.6",
|
||||
"@next/swc-darwin-x64": "16.1.6",
|
||||
"@next/swc-linux-arm64-gnu": "16.1.6",
|
||||
"@next/swc-linux-arm64-musl": "16.1.6",
|
||||
"@next/swc-linux-x64-gnu": "16.1.6",
|
||||
"@next/swc-linux-x64-musl": "16.1.6",
|
||||
"@next/swc-win32-arm64-msvc": "16.1.6",
|
||||
"@next/swc-win32-x64-msvc": "16.1.6",
|
||||
"sharp": "^0.34.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -8137,9 +8125,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -8287,16 +8275,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.50.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz",
|
||||
"integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz",
|
||||
"integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.50.0",
|
||||
"@typescript-eslint/parser": "8.50.0",
|
||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
||||
"@typescript-eslint/utils": "8.50.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.56.0",
|
||||
"@typescript-eslint/parser": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0",
|
||||
"@typescript-eslint/utils": "8.56.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -8306,7 +8294,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"framer-motion": "^12.23.26",
|
||||
"gsap": "^3.14.2",
|
||||
"lucide-react": "^0.561.0",
|
||||
"next": "16.0.10",
|
||||
"next": "^16.1.6",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
@@ -31,10 +31,13 @@
|
||||
"@types/react-dom": "^19",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.10",
|
||||
"eslint-config-next": "^16.1.6",
|
||||
"jsdom": "^27.4.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"overrides": {
|
||||
"minimatch": "^10.2.1"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ===== WARM PAPER AESTHETIC THEMES ===== */
|
||||
|
||||
|
||||
:root {
|
||||
/* Dark theme - lighter warm charcoal per user request */
|
||||
zoom: 1.07;
|
||||
--bg-primary: #262522;
|
||||
--bg-secondary: #2f2e2a;
|
||||
--bg-tertiary: #3a3934;
|
||||
@@ -18,7 +18,6 @@
|
||||
}
|
||||
|
||||
.light {
|
||||
/* Light theme - warm paper/parchment */
|
||||
--bg-primary: #f5f2ed;
|
||||
--bg-secondary: #ebe8e2;
|
||||
--bg-tertiary: #e0dcd4;
|
||||
@@ -71,7 +70,6 @@ body::before {
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
/* Smooth scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@@ -86,10 +84,8 @@ body::before {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Scroll margin to keep focused items visible during keyboard navigation */
|
||||
[data-nav-id] {
|
||||
scroll-margin-block: 100px 150px;
|
||||
/* top: 100px, bottom: 150px for shortcuts bar */
|
||||
}
|
||||
|
||||
html {
|
||||
@@ -101,8 +97,6 @@ html {
|
||||
background: rgba(128, 120, 100, 0.25);
|
||||
}
|
||||
|
||||
/* ===== THEME SWITCH ===== */
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -249,7 +243,6 @@ html {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
/* Checked state (dark mode) */
|
||||
.switch__input:checked {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
@@ -312,14 +305,10 @@ html {
|
||||
transform: translateX(-50%) rotate(315deg) translateY(0.625em) scale(0);
|
||||
}
|
||||
|
||||
/* Set switch size */
|
||||
.switch {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* ===== ANIMATIONS ===== */
|
||||
|
||||
/* Checkbox pop */
|
||||
@keyframes checkPop {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
@@ -338,7 +327,6 @@ html {
|
||||
animation: checkPop 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
/* Checkmark entrance */
|
||||
@keyframes checkIn {
|
||||
0% {
|
||||
transform: scale(0) rotate(-45deg);
|
||||
@@ -355,12 +343,10 @@ html {
|
||||
animation: checkIn 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
}
|
||||
|
||||
/* Spring chevron */
|
||||
.chevron-spring {
|
||||
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
/* Stagger items */
|
||||
@keyframes staggerIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -377,7 +363,6 @@ html {
|
||||
animation: staggerIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
/* Tooltip fade */
|
||||
@keyframes tooltipIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -394,7 +379,6 @@ html {
|
||||
animation: tooltipIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
/* Dropdown entrance */
|
||||
@keyframes dropIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -411,7 +395,6 @@ html {
|
||||
animation: dropIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
/* Button press */
|
||||
.btn-press {
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
@@ -420,15 +403,11 @@ html {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Focus row highlight */
|
||||
.focus-row {
|
||||
background: var(--bg-focus);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
/* ===== ENTRANCE ANIMATION INITIAL STATES ===== */
|
||||
/* These prevent the flash of content before GSAP animates */
|
||||
|
||||
.category-header {
|
||||
clip-path: inset(0 100% 0 0);
|
||||
}
|
||||
@@ -448,8 +427,6 @@ html {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
/* ===== HOW IT WORKS POPUP ===== */
|
||||
|
||||
@keyframes popupSlideIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -482,8 +459,6 @@ html {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ===== SLIDE-UP DRAWER ANIMATIONS ===== */
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -574,7 +549,6 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth spring-like animations for floating cards */
|
||||
@keyframes cardSlideIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -619,7 +593,6 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
/* Slide in from right edge */
|
||||
@keyframes slideInFromRight {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -676,18 +649,14 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== COMMAND BAR SCROLLBAR ===== */
|
||||
|
||||
.command-scroll {
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
/* IE/Edge */
|
||||
}
|
||||
|
||||
.command-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
/* Chrome/Safari/Opera */
|
||||
}
|
||||
|
||||
/* ===== SEARCH POPUP ANIMATION ===== */
|
||||
@@ -704,8 +673,6 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== THEME FLASH ANIMATION ===== */
|
||||
|
||||
@keyframes themeFlash {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -730,8 +697,6 @@ body.theme-flash::after {
|
||||
animation: themeFlash 0.15s ease-out forwards;
|
||||
}
|
||||
|
||||
/* ===== SKELETON LOADING ANIMATION ===== */
|
||||
|
||||
@keyframes skeletonPulse {
|
||||
|
||||
0%,
|
||||
@@ -749,13 +714,11 @@ body.theme-flash::after {
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
/* Staggered skeleton items for wave effect */
|
||||
.skeleton-item {
|
||||
animation: skeletonPulse 1.5s ease-in-out infinite;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
/* Force GPU acceleration for animated elements */
|
||||
.category-header,
|
||||
.app-item,
|
||||
.header-animate,
|
||||
@@ -765,3 +728,87 @@ body.theme-flash::after {
|
||||
-webkit-perspective: 1000px;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
@keyframes sidebarSlideIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
animation: sidebarSlideIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
background: var(--bg-primary);
|
||||
border-right: 1px solid color-mix(in srgb, var(--border-primary), transparent 50%);
|
||||
}
|
||||
|
||||
.sidebar-scroll::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.sidebar-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-scroll::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--text-muted), transparent 70%);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in srgb, var(--text-muted), transparent 40%);
|
||||
}
|
||||
|
||||
.sidebar-action-btn {
|
||||
transition: background-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-action-btn:hover:not(:disabled) {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
.sidebar-action-btn:active:not(:disabled) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.sidebar-search {
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-search:focus-within {
|
||||
border-color: var(--accent);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.sidebar-command-preview {
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-command-preview:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.sidebar-distro-btn {
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-distro-btn:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.main-with-sidebar {
|
||||
margin-left: 380px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.main-with-sidebar {
|
||||
margin-left: 420px;
|
||||
}
|
||||
}
|
||||
161
src/app/page.tsx
161
src/app/page.tsx
@@ -3,22 +3,19 @@
|
||||
import { useState, useMemo, useCallback, useRef, useLayoutEffect, useEffect } from 'react';
|
||||
import gsap from 'gsap';
|
||||
|
||||
// Hooks
|
||||
import { useLinuxInit } from '@/hooks/useLinuxInit';
|
||||
import { useTooltip } from '@/hooks/useTooltip';
|
||||
import { useKeyboardNavigation, type NavItem } from '@/hooks/useKeyboardNavigation';
|
||||
import { useVerification } from '@/hooks/useVerification';
|
||||
|
||||
// Data
|
||||
import { categories, getAppsByCategory } from '@/lib/data';
|
||||
|
||||
// Components
|
||||
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||
import { HowItWorks, GitHubLink, ContributeLink } from '@/components/header';
|
||||
import { DistroSelector } from '@/components/distro';
|
||||
import { CategorySection } from '@/components/app';
|
||||
import { CommandFooter } from '@/components/command';
|
||||
import { Tooltip, GlobalStyles, LoadingSkeleton } from '@/components/common';
|
||||
import { Sidebar } from '@/components/sidebar';
|
||||
|
||||
export default function Home() {
|
||||
|
||||
@@ -44,40 +41,104 @@ export default function Home() {
|
||||
unfreeAppNames,
|
||||
} = useLinuxInit();
|
||||
|
||||
// Search state
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Verification status for Flatpak/Snap apps
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [drawerClosing, setDrawerClosing] = useState(false);
|
||||
|
||||
const closeDrawer = useCallback(() => {
|
||||
setDrawerClosing(true);
|
||||
setTimeout(() => {
|
||||
setDrawerOpen(false);
|
||||
setDrawerClosing(false);
|
||||
}, 250);
|
||||
}, []);
|
||||
|
||||
const openDrawer = useCallback(() => {
|
||||
if (selectedCount > 0) setDrawerOpen(true);
|
||||
}, [selectedCount]);
|
||||
|
||||
const [activeShortcut, setActiveShortcut] = useState<string | null>(null);
|
||||
|
||||
const toggleThemeWithFlash = useCallback(() => {
|
||||
document.body.classList.add('theme-flash');
|
||||
setTimeout(() => document.body.classList.remove('theme-flash'), 150);
|
||||
const themeBtn = document.querySelector('[aria-label="Toggle theme"]') as HTMLButtonElement;
|
||||
if (themeBtn) themeBtn.click();
|
||||
}, []);
|
||||
|
||||
const { isVerified, getVerificationSource } = useVerification();
|
||||
|
||||
// Handle "/" key to focus search
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Skip if already in input
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
|
||||
|
||||
// Skip if modifier keys are pressed (prevents conflicts with browser shortcuts)
|
||||
if (e.ctrlKey || e.altKey || e.metaKey) return;
|
||||
|
||||
if (e.key === '/') {
|
||||
e.preventDefault();
|
||||
searchInputRef.current?.focus();
|
||||
const inputs = document.querySelectorAll<HTMLInputElement>('input[placeholder="Search apps..."]');
|
||||
const visibleInput = Array.from(inputs).find(input => input.offsetParent !== null);
|
||||
if (visibleInput) visibleInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const alwaysEnabled = ['t', 'c', '?'];
|
||||
if (selectedCount === 0 && !alwaysEnabled.includes(e.key)) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'y':
|
||||
setActiveShortcut('y');
|
||||
setTimeout(() => setActiveShortcut(null), 150);
|
||||
const copyBtns = document.querySelectorAll<HTMLButtonElement>('[data-action="copy"]');
|
||||
const visibleCopyBtn = Array.from(copyBtns).find(b => b.offsetParent !== null);
|
||||
if (visibleCopyBtn) visibleCopyBtn.click();
|
||||
break;
|
||||
case 'd':
|
||||
setActiveShortcut('d');
|
||||
setTimeout(() => setActiveShortcut(null), 150);
|
||||
const dlBtns = document.querySelectorAll<HTMLButtonElement>('[data-action="download"]');
|
||||
const visibleDlBtn = Array.from(dlBtns).find(b => b.offsetParent !== null);
|
||||
if (visibleDlBtn) visibleDlBtn.click();
|
||||
break;
|
||||
case 't':
|
||||
setActiveShortcut('t');
|
||||
setTimeout(() => setActiveShortcut(null), 150);
|
||||
toggleThemeWithFlash();
|
||||
break;
|
||||
case 'c':
|
||||
setActiveShortcut('c');
|
||||
setTimeout(() => setActiveShortcut(null), 150);
|
||||
clearAll();
|
||||
break;
|
||||
case '1':
|
||||
if (hasAurPackages) setSelectedHelper('yay');
|
||||
break;
|
||||
case '2':
|
||||
if (hasAurPackages) setSelectedHelper('paru');
|
||||
break;
|
||||
case 'Tab':
|
||||
e.preventDefault();
|
||||
if (selectedCount > 0) {
|
||||
if (drawerOpen) {
|
||||
closeDrawer();
|
||||
} else {
|
||||
openDrawer();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
}, [selectedCount, clearAll, hasAurPackages, setSelectedHelper, drawerOpen, closeDrawer, openDrawer, toggleThemeWithFlash]);
|
||||
|
||||
|
||||
// Distribute apps into a nice grid
|
||||
const allCategoriesWithApps = useMemo(() => {
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
return categories
|
||||
.map(cat => {
|
||||
const categoryApps = getAppsByCategory(cat);
|
||||
// Filter apps if there's a search query (match name or id only)
|
||||
const filteredApps = query
|
||||
? categoryApps.filter(app =>
|
||||
app.name.toLowerCase().includes(query) ||
|
||||
@@ -89,10 +150,8 @@ export default function Home() {
|
||||
.filter(c => c.apps.length > 0);
|
||||
}, [searchQuery]);
|
||||
|
||||
// 5 columns looks good on most screens
|
||||
const COLUMN_COUNT = 5;
|
||||
const COLUMN_COUNT = 4;
|
||||
|
||||
// Pack categories into shortest column while preserving order
|
||||
const columns = useMemo(() => {
|
||||
const cols: Array<typeof allCategoriesWithApps> = Array.from({ length: COLUMN_COUNT }, () => []);
|
||||
const heights = Array(COLUMN_COUNT).fill(0);
|
||||
@@ -106,9 +165,6 @@ export default function Home() {
|
||||
return cols;
|
||||
}, [allCategoriesWithApps]);
|
||||
|
||||
// Category expansion - all open by default because hiding stuff is annoying
|
||||
// ========================================================================
|
||||
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(() => new Set(categories));
|
||||
|
||||
const toggleCategoryExpanded = useCallback((cat: string) => {
|
||||
@@ -123,8 +179,6 @@ export default function Home() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
// Build nav items for keyboard navigation
|
||||
const navItems = useMemo(() => {
|
||||
const items: NavItem[][] = [];
|
||||
columns.forEach((colCategories) => {
|
||||
@@ -146,9 +200,6 @@ export default function Home() {
|
||||
toggleApp
|
||||
);
|
||||
|
||||
// Header animation - makes the logo look fancy on first load
|
||||
// ========================================================================
|
||||
|
||||
const headerRef = useRef<HTMLElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -158,7 +209,6 @@ export default function Home() {
|
||||
const title = header.querySelector('.header-animate');
|
||||
const controls = header.querySelector('.header-controls');
|
||||
|
||||
// Fancy clip-path reveal for the logo
|
||||
gsap.fromTo(title,
|
||||
{ clipPath: 'inset(0 100% 0 0)' },
|
||||
{
|
||||
@@ -172,7 +222,6 @@ export default function Home() {
|
||||
}
|
||||
);
|
||||
|
||||
// Animate controls with fade-in
|
||||
gsap.fromTo(controls,
|
||||
{ opacity: 0, y: -10 },
|
||||
{
|
||||
@@ -185,17 +234,10 @@ export default function Home() {
|
||||
);
|
||||
}, [isHydrated]);
|
||||
|
||||
|
||||
// Don't render until we've loaded from localStorage (avoids flash)
|
||||
|
||||
// Show loading skeleton until localStorage is hydrated
|
||||
if (!isHydrated) {
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
|
||||
// Finally, the actual page
|
||||
// ========================================================================
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)] relative"
|
||||
@@ -205,14 +247,30 @@ export default function Home() {
|
||||
<GlobalStyles />
|
||||
<Tooltip tooltip={tooltip} onMouseEnter={tooltipMouseEnter} onMouseLeave={tooltipMouseLeave} setRef={setTooltipRef} />
|
||||
|
||||
{/* Header */}
|
||||
<header ref={headerRef} className="pt-8 sm:pt-12 pb-8 sm:pb-10 px-4 sm:px-6 relative" style={{ zIndex: 1 }}>
|
||||
<Sidebar
|
||||
selectedDistro={selectedDistro}
|
||||
onDistroSelect={setSelectedDistro}
|
||||
selectedApps={selectedApps}
|
||||
selectedCount={selectedCount}
|
||||
clearAll={clearAll}
|
||||
command={generatedCommand}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
searchInputRef={searchInputRef}
|
||||
hasAurPackages={hasAurPackages}
|
||||
aurAppNames={aurAppNames}
|
||||
selectedHelper={selectedHelper}
|
||||
setSelectedHelper={setSelectedHelper}
|
||||
hasUnfreePackages={hasUnfreePackages}
|
||||
unfreeAppNames={unfreeAppNames}
|
||||
onOpenDrawer={openDrawer}
|
||||
/>
|
||||
|
||||
<header ref={headerRef} className="lg:hidden pt-8 sm:pt-12 pb-8 sm:pb-10 px-4 sm:px-6 relative" style={{ zIndex: 1 }}>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
{/* Logo & Title */}
|
||||
<div className="header-animate">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="/tuxmate.png"
|
||||
alt="TuxMate Logo"
|
||||
@@ -235,11 +293,9 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header Controls */}
|
||||
<div className="header-controls flex items-center justify-between sm:justify-end gap-3 sm:gap-4">
|
||||
{/* Left side on mobile: Help + Links */}
|
||||
<div className="flex items-center gap-3 sm:gap-4">
|
||||
{/* Help - mobile only here, desktop is in title area */}
|
||||
<div className="sm:hidden">
|
||||
<HowItWorks />
|
||||
</div>
|
||||
@@ -247,7 +303,6 @@ export default function Home() {
|
||||
<ContributeLink />
|
||||
</div>
|
||||
|
||||
{/* Right side: Theme + Distro (with separator on desktop) */}
|
||||
<div className="flex items-center gap-2 pl-2 sm:pl-3 border-l border-[var(--border-primary)]">
|
||||
<ThemeToggle />
|
||||
<DistroSelector selectedDistro={selectedDistro} onSelect={setSelectedDistro} />
|
||||
@@ -257,13 +312,10 @@ export default function Home() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* App Grid */}
|
||||
<main className="px-4 sm:px-6 pb-40 relative" style={{ zIndex: 1 }}>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Mobile: 2-column grid with balanced distribution */}
|
||||
<div className="grid grid-cols-2 gap-x-4 md:hidden items-start">
|
||||
<main className="main-with-sidebar px-4 sm:px-6 pb-40 relative" style={{ zIndex: 1 }}>
|
||||
<div className="max-w-7xl mx-auto lg:pt-8">
|
||||
<div className="grid grid-cols-2 gap-x-4 lg:hidden items-start">
|
||||
{(() => {
|
||||
// Pack into 2 columns on mobile
|
||||
const mobileColumns: Array<typeof allCategoriesWithApps> = [[], []];
|
||||
const heights = [0, 0];
|
||||
allCategoriesWithApps.forEach(catData => {
|
||||
@@ -300,16 +352,13 @@ export default function Home() {
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Desktop: Grid with Tetris packing */}
|
||||
<div className="hidden md:grid md:grid-cols-4 lg:grid-cols-5 gap-x-8 items-start">
|
||||
<div className="hidden lg:grid lg:grid-cols-3 xl:grid-cols-4 gap-x-8 items-start">
|
||||
{columns.map((columnCategories, colIdx) => {
|
||||
// Calculate starting index for this column (for staggered animation)
|
||||
let globalIdx = 0;
|
||||
for (let c = 0; c < colIdx; c++) {
|
||||
globalIdx += columns[c].length;
|
||||
}
|
||||
|
||||
// Generate stable key based on column content to ensure proper reconciliation
|
||||
const columnKey = `col-${colIdx}-${columnCategories.map(c => c.category).join('-')}`;
|
||||
|
||||
return (
|
||||
@@ -343,7 +392,6 @@ export default function Home() {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Command Footer */}
|
||||
<CommandFooter
|
||||
command={generatedCommand}
|
||||
selectedCount={selectedCount}
|
||||
@@ -361,6 +409,11 @@ export default function Home() {
|
||||
setSelectedHelper={setSelectedHelper}
|
||||
hasUnfreePackages={hasUnfreePackages}
|
||||
unfreeAppNames={unfreeAppNames}
|
||||
drawerOpen={drawerOpen}
|
||||
drawerClosing={drawerClosing}
|
||||
onDrawerOpen={() => setDrawerOpen(true)}
|
||||
onDrawerClose={closeDrawer}
|
||||
activeShortcut={activeShortcut}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ export function AppIcon({ url, name }: { url: string; name: string }) {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="w-[18px] h-[18px] rounded bg-[var(--accent)] flex items-center justify-center text-[10px] font-bold">
|
||||
<div className="w-[22px] h-[22px] rounded bg-[var(--accent)] flex items-center justify-center text-[11px] font-bold">
|
||||
{name[0]}
|
||||
</div>
|
||||
);
|
||||
@@ -20,9 +20,9 @@ export function AppIcon({ url, name }: { url: string; name: string }) {
|
||||
src={url}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width={16}
|
||||
height={16}
|
||||
className="w-[18px] h-[18px] object-contain opacity-75"
|
||||
width={22}
|
||||
height={22}
|
||||
className="w-[22px] h-[22px] object-contain opacity-75"
|
||||
onError={() => setError(true)}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
@@ -5,15 +5,7 @@ import { Check } from 'lucide-react';
|
||||
import { distros, type DistroId, type AppData } from '@/lib/data';
|
||||
import { isAurPackage } from '@/lib/aur';
|
||||
import { AppIcon } from './AppIcon';
|
||||
// import { analytics } from '@/lib/analytics'; // Uncomment to enable app selection tracking
|
||||
|
||||
/**
|
||||
* Individual app row in the category list.
|
||||
* Memoized because we render hundreds of these and React was having a moment.
|
||||
* Handles selection state, availability indicators, AUR badges, and tooltips.
|
||||
*/
|
||||
|
||||
// Tailwind colors as hex - because CSS variables don't work in inline styles
|
||||
// Individual app item.
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
'orange': '#f97316',
|
||||
'blue': '#3b82f6',
|
||||
@@ -43,7 +35,6 @@ interface AppItemProps {
|
||||
onTooltipLeave: () => void;
|
||||
onFocus?: () => void;
|
||||
color?: string;
|
||||
// Flatpak/Snap verification status
|
||||
isVerified?: boolean;
|
||||
verificationSource?: 'flathub' | 'snap' | null;
|
||||
}
|
||||
@@ -62,16 +53,12 @@ export const AppItem = memo(function AppItem({
|
||||
isVerified = false,
|
||||
verificationSource = null,
|
||||
}: AppItemProps) {
|
||||
// Why isn't this app available? Tell the user.
|
||||
const getUnavailableText = () => {
|
||||
const distroName = distros.find(d => d.id === selectedDistro)?.name || '';
|
||||
return app.unavailableReason || `Not available in ${distroName} repos`;
|
||||
};
|
||||
|
||||
// Special styling for AUR packages (Arch users love their badges)
|
||||
const isAur = selectedDistro === 'arch' && app.targets?.arch && isAurPackage(app.targets.arch);
|
||||
|
||||
// AUR gets its special Arch blue, everything else uses category color
|
||||
const hexColor = COLOR_MAP[color] || COLOR_MAP['gray'];
|
||||
const checkboxColor = isAur ? '#1793d1' : hexColor;
|
||||
|
||||
@@ -82,7 +69,7 @@ export const AppItem = memo(function AppItem({
|
||||
aria-checked={isSelected}
|
||||
aria-label={`${app.name}${!isAvailable ? ' (unavailable)' : ''}`}
|
||||
aria-disabled={!isAvailable}
|
||||
className={`app-item group w-full flex items-center gap-2.5 py-1.5 px-2 outline-none transition-all duration-150
|
||||
className={`app-item group w-full flex items-center gap-3 py-[7px] px-3 outline-none transition-all duration-150
|
||||
${isFocused ? 'bg-[var(--bg-secondary)] border-l-2 shadow-sm' : 'border-l-2 border-transparent'}
|
||||
${!isAvailable
|
||||
? 'opacity-40 grayscale-[30%]'
|
||||
@@ -91,7 +78,7 @@ export const AppItem = memo(function AppItem({
|
||||
style={{
|
||||
transition: 'background-color 0.15s, color 0.5s',
|
||||
borderColor: isFocused ? hexColor : 'transparent',
|
||||
backgroundColor: isFocused ? `color-mix(in srgb, ${hexColor}, transparent 85%)` : undefined, // Stronger tint on focus (15% opacity)
|
||||
backgroundColor: isFocused ? `color-mix(in srgb, ${hexColor}, transparent 85%)` : undefined,
|
||||
'--item-color': hexColor,
|
||||
} as React.CSSProperties}
|
||||
onClick={(e) => {
|
||||
@@ -99,23 +86,17 @@ export const AppItem = memo(function AppItem({
|
||||
onFocus?.();
|
||||
if (isAvailable) {
|
||||
onToggle();
|
||||
// Umami tracking disabled to save quota
|
||||
// if (isSelected) {
|
||||
// analytics.appDeselected(app.name, app.category || '', selectedDistro);
|
||||
// } else {
|
||||
// analytics.appSelected(app.name, app.category || '', selectedDistro);
|
||||
// }
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-colors duration-150 ${!isAvailable ? 'border-dashed' : ''}`}
|
||||
className={`w-5 h-5 rounded border-2 flex items-center justify-center flex-shrink-0 transition-colors duration-150 ${!isAvailable ? 'border-dashed' : ''}`}
|
||||
style={{
|
||||
borderColor: isSelected || isAur ? checkboxColor : 'var(--border-secondary)',
|
||||
backgroundColor: isSelected ? checkboxColor : 'transparent',
|
||||
}}
|
||||
>
|
||||
{isSelected && <Check className="w-2.5 h-2.5 text-white" strokeWidth={3} />}
|
||||
{isSelected && <Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />}
|
||||
</div>
|
||||
<AppIcon url={app.iconUrl} name={app.name} />
|
||||
<div className="flex-1 flex items-baseline gap-1.5 min-w-0 overflow-hidden">
|
||||
@@ -123,7 +104,8 @@ export const AppItem = memo(function AppItem({
|
||||
className={`truncate cursor-help ${!isAvailable ? 'text-[var(--text-muted)]' : isSelected ? 'text-[var(--text-primary)]' : 'text-[var(--text-secondary)]'}`}
|
||||
style={{
|
||||
fontFamily: 'var(--font-open-sans), sans-serif',
|
||||
fontSize: '16px',
|
||||
fontSize: '20px',
|
||||
fontWeight: 500,
|
||||
transition: 'color 0.5s',
|
||||
textRendering: 'geometricPrecision',
|
||||
WebkitFontSmoothing: 'antialiased'
|
||||
@@ -162,7 +144,6 @@ export const AppItem = memo(function AppItem({
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{/* Exclamation mark icon for unavailable apps */}
|
||||
{!isAvailable && (
|
||||
<div
|
||||
className="relative group flex-shrink-0 cursor-help"
|
||||
@@ -170,8 +151,8 @@ export const AppItem = memo(function AppItem({
|
||||
onMouseLeave={(e) => { e.stopPropagation(); onTooltipLeave(); }}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 text-[var(--text-muted)] transition-[color,transform] duration-300 hover:rotate-[360deg] hover:scale-110"
|
||||
style={{ color: isFocused ? hexColor : undefined }} // Use category color on hover/focus
|
||||
className="w-[18px] h-[18px] text-[var(--text-muted)] transition-[color,transform] duration-300 hover:rotate-[360deg] hover:scale-110"
|
||||
style={{ color: isFocused ? hexColor : undefined }}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Network, Lock, Share2, Cpu, type LucideIcon
|
||||
} from 'lucide-react';
|
||||
|
||||
// Map category names to their icons. If you add a category, add an icon here.
|
||||
const CATEGORY_ICONS: Record<string, LucideIcon> = {
|
||||
'Web Browsers': Globe,
|
||||
'Communication': MessageCircle,
|
||||
@@ -25,7 +24,6 @@ const CATEGORY_ICONS: Record<string, LucideIcon> = {
|
||||
'System': Cpu,
|
||||
};
|
||||
|
||||
// Tailwind colors as hex
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
'orange': '#f97316',
|
||||
'blue': '#3b82f6',
|
||||
@@ -44,10 +42,7 @@ const COLOR_MAP: Record<string, string> = {
|
||||
'gray': '#6b7280',
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapsible category header with icon, chevron, and selection badge.
|
||||
* Uses color-mix for dynamic tinting because we're fancy like that.
|
||||
*/
|
||||
// Category header.
|
||||
export function CategoryHeader({
|
||||
category,
|
||||
isExpanded,
|
||||
@@ -74,33 +69,32 @@ export function CategoryHeader({
|
||||
tabIndex={-1}
|
||||
aria-expanded={isExpanded}
|
||||
aria-label={`${category} category, ${selectedCount} apps selected`}
|
||||
// AccessGuide-style: subtle bg with colored left border accent
|
||||
className={`category-header group w-full h-8 flex items-center gap-2 text-sm font-semibold
|
||||
className={`category-header group w-full py-2 flex items-center gap-2.5 text-[15px] font-semibold
|
||||
border-l-4
|
||||
px-3 mb-3
|
||||
px-3 mb-2
|
||||
transition-all duration-200 outline-none
|
||||
hover:bg-[color-mix(in_srgb,var(--header-color),transparent_80%)]`}
|
||||
style={{
|
||||
color: 'var(--text-primary)',
|
||||
borderColor: hexColor,
|
||||
backgroundColor: isFocused
|
||||
? `color-mix(in srgb, ${hexColor}, transparent 75%)` // 25% opacity for focus
|
||||
: `color-mix(in srgb, ${hexColor}, transparent 90%)`, // 10% opacity for default
|
||||
? `color-mix(in srgb, ${hexColor}, transparent 75%)`
|
||||
: `color-mix(in srgb, ${hexColor}, transparent 90%)`,
|
||||
'--header-color': hexColor,
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<ChevronRight
|
||||
className={`w-4 h-4 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
|
||||
className={`w-[18px] h-[18px] transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
|
||||
style={{ color: hexColor }}
|
||||
/>
|
||||
{(() => {
|
||||
const Icon = CATEGORY_ICONS[category] || Terminal;
|
||||
return <Icon className="w-4 h-4" style={{ color: hexColor }} />;
|
||||
return <Icon className="w-[18px] h-[18px]" style={{ color: hexColor }} />;
|
||||
})()}
|
||||
<span className="flex-1 text-left">{category}</span>
|
||||
{selectedCount > 0 && (
|
||||
<span
|
||||
className="text-xs font-bold ml-1.5 px-1.5 py-0.5 rounded"
|
||||
className="text-sm font-bold ml-1.5 px-2 py-0.5 rounded"
|
||||
style={{
|
||||
transition: 'color 0.5s',
|
||||
color: hexColor,
|
||||
|
||||
@@ -7,11 +7,7 @@ import { analytics } from '@/lib/analytics';
|
||||
import { CategoryHeader } from './CategoryHeader';
|
||||
import { AppItem } from './AppItem';
|
||||
|
||||
/**
|
||||
* A collapsible category section containing a list of apps.
|
||||
* Handles its own GSAP animations because CSS transitions just weren't cutting it.
|
||||
* Memoized to hell and back because React was re-rendering everything.
|
||||
*/
|
||||
// Category section.
|
||||
interface CategorySectionProps {
|
||||
category: Category;
|
||||
categoryApps: AppData[];
|
||||
@@ -28,15 +24,10 @@ interface CategorySectionProps {
|
||||
categoryIndex: number;
|
||||
onCategoryFocus?: () => void;
|
||||
onAppFocus?: (appId: string) => void;
|
||||
// Flatpak/Snap verification status
|
||||
isVerified?: (distro: DistroId, packageName: string) => boolean;
|
||||
getVerificationSource?: (distro: DistroId, packageName: string) => 'flathub' | 'snap' | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Color palette for categories. Vibrant ones go to user-facing stuff,
|
||||
* boring grays go to developer tools because we're used to suffering.
|
||||
*/
|
||||
const categoryColors: Record<Category, string> = {
|
||||
'Web Browsers': 'orange',
|
||||
'Communication': 'blue',
|
||||
@@ -80,10 +71,8 @@ function CategorySectionComponent({
|
||||
const hasAnimated = useRef(false);
|
||||
const prevAppCount = useRef(categoryApps.length);
|
||||
|
||||
// Get color for this category
|
||||
const color = categoryColors[category] || 'gray';
|
||||
|
||||
// Initial entrance animation
|
||||
useLayoutEffect(() => {
|
||||
if (!sectionRef.current || hasAnimated.current) return;
|
||||
hasAnimated.current = true;
|
||||
@@ -92,16 +81,12 @@ function CategorySectionComponent({
|
||||
const header = section.querySelector('.category-header');
|
||||
const items = section.querySelectorAll('.app-item');
|
||||
|
||||
// Use requestAnimationFrame for smoother initial setup
|
||||
requestAnimationFrame(() => {
|
||||
// Initial state with GPU-accelerated transforms
|
||||
gsap.set(header, { clipPath: 'inset(0 100% 0 0)' });
|
||||
gsap.set(items, { y: -15, opacity: 0, force3D: true });
|
||||
|
||||
// Staggered delay based on category index (reduced for faster feel)
|
||||
const delay = categoryIndex * 0.05;
|
||||
|
||||
// Animate header with clip-path reveal
|
||||
gsap.to(header, {
|
||||
clipPath: 'inset(0 0% 0 0)',
|
||||
duration: 0.6,
|
||||
@@ -109,7 +94,6 @@ function CategorySectionComponent({
|
||||
delay: delay + 0.05
|
||||
});
|
||||
|
||||
// Animate items with GPU-accelerated transforms
|
||||
gsap.to(items, {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
@@ -121,11 +105,9 @@ function CategorySectionComponent({
|
||||
});
|
||||
}, [categoryIndex]);
|
||||
|
||||
// When app count changes (after search clears), ensure all items are visible
|
||||
useEffect(() => {
|
||||
if (categoryApps.length !== prevAppCount.current && sectionRef.current) {
|
||||
const items = sectionRef.current.querySelectorAll('.app-item');
|
||||
// Reset any hidden items to visible
|
||||
gsap.set(items, { y: 0, opacity: 1, clearProps: 'all' });
|
||||
}
|
||||
prevAppCount.current = categoryApps.length;
|
||||
@@ -151,7 +133,7 @@ function CategorySectionComponent({
|
||||
color={color}
|
||||
/>
|
||||
<div
|
||||
className={`overflow-hidden transition-[max-height,opacity] duration-500 ${isExpanded ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0'}`}
|
||||
className={`overflow-hidden transition-[max-height,opacity] duration-500 pt-0.5 ${isExpanded ? 'max-h-[2000px] opacity-100' : 'max-h-0 opacity-0'}`}
|
||||
style={{ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }}
|
||||
>
|
||||
{categoryApps.map((app) => (
|
||||
@@ -183,17 +165,13 @@ function CategorySectionComponent({
|
||||
);
|
||||
}
|
||||
|
||||
// Custom memo comparison - React's shallow compare was killing perf
|
||||
export const CategorySection = memo(CategorySectionComponent, (prevProps, nextProps) => {
|
||||
// Always re-render if app count changes
|
||||
if (prevProps.categoryApps.length !== nextProps.categoryApps.length) return false;
|
||||
|
||||
// Check if app IDs are the same
|
||||
const prevIds = prevProps.categoryApps.map(a => a.id).join(',');
|
||||
const nextIds = nextProps.categoryApps.map(a => a.id).join(',');
|
||||
if (prevIds !== nextIds) return false;
|
||||
|
||||
// Check other important props
|
||||
if (prevProps.category !== nextProps.category) return false;
|
||||
if (prevProps.isExpanded !== nextProps.isExpanded) return false;
|
||||
if (prevProps.selectedDistro !== nextProps.selectedDistro) return false;
|
||||
@@ -201,11 +179,9 @@ export const CategorySection = memo(CategorySectionComponent, (prevProps, nextPr
|
||||
if (prevProps.focusedType !== nextProps.focusedType) return false;
|
||||
if (prevProps.categoryIndex !== nextProps.categoryIndex) return false;
|
||||
|
||||
// Re-render when verification functions change (Flathub data loads)
|
||||
if (prevProps.isVerified !== nextProps.isVerified) return false;
|
||||
if (prevProps.getVerificationSource !== nextProps.getVerificationSource) return false;
|
||||
|
||||
// Check if selection state changed for any app in this category
|
||||
for (const app of nextProps.categoryApps) {
|
||||
if (prevProps.selectedApps.has(app.id) !== nextProps.selectedApps.has(app.id)) {
|
||||
return false;
|
||||
|
||||
@@ -8,12 +8,7 @@ interface AurDrawerSettingsProps {
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* AUR package settings panel for Arch users.
|
||||
* Lets you pick between yay and paru, and whether to install the helper.
|
||||
* The naming of hasYayInstalled is a bit misleading - it actually means
|
||||
* "user already has an AUR helper" regardless of which one. Tech debt, I know.
|
||||
*/
|
||||
// AUR package settings panel.
|
||||
export function AurDrawerSettings({
|
||||
aurAppNames,
|
||||
hasYayInstalled,
|
||||
@@ -23,8 +18,8 @@ export function AurDrawerSettings({
|
||||
distroColor,
|
||||
}: AurDrawerSettingsProps & { distroColor: string }) {
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-[var(--border-primary)] bg-[var(--bg-secondary)] overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[var(--border-primary)]/50 flex">
|
||||
<div className="mb-4 rounded-lg bg-[var(--bg-secondary)]/30 border border-[var(--border-primary)]/20 overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[var(--border-primary)]/10 flex">
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)] whitespace-nowrap mr-2">AUR Packages:</span>
|
||||
<span className="text-xs text-[var(--text-muted)] truncate">{aurAppNames.join(', ')}</span>
|
||||
</div>
|
||||
@@ -32,7 +27,7 @@ export function AurDrawerSettings({
|
||||
<div className="p-4 grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[var(--text-secondary)] font-medium">AUR Helper</span>
|
||||
<div className="flex w-full bg-[var(--bg-primary)] rounded-md border border-[var(--border-primary)] p-1 h-10">
|
||||
<div className="flex w-full bg-[var(--bg-tertiary)]/40 rounded-md border border-[var(--border-primary)]/30 p-1 h-10">
|
||||
<button
|
||||
onClick={() => setSelectedHelper('yay')}
|
||||
className={`flex-1 rounded-sm font-medium transition-all ${selectedHelper === 'yay' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'}`}
|
||||
@@ -52,7 +47,7 @@ export function AurDrawerSettings({
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[var(--text-secondary)] font-medium">Install helper?</span>
|
||||
<div className="flex w-full bg-[var(--bg-primary)] rounded-md border border-[var(--border-primary)] p-1 h-10">
|
||||
<div className="flex w-full bg-[var(--bg-tertiary)]/40 rounded-md border border-[var(--border-primary)]/30 p-1 h-10">
|
||||
<button
|
||||
onClick={() => setHasYayInstalled(true)}
|
||||
className={`flex-1 rounded-sm font-medium transition-all ${hasYayInstalled ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'}`}
|
||||
|
||||
@@ -12,11 +12,7 @@ interface AurFloatingCardProps {
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Floating card wizard for Arch users with AUR packages.
|
||||
* Asks whether they have an AUR helper, then which one.
|
||||
* The yay vs paru debate is the real holy war of our time.
|
||||
*/
|
||||
// AUR setup prompt.
|
||||
export function AurFloatingCard({
|
||||
show,
|
||||
aurAppNames,
|
||||
@@ -27,14 +23,11 @@ export function AurFloatingCard({
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [isExiting, setIsExiting] = useState(false);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
// Track if user has answered the first question
|
||||
const [hasAnswered, setHasAnswered] = useState<boolean | null>(null);
|
||||
// Track if user has selected a helper (completed flow)
|
||||
const [helperChosen, setHelperChosen] = useState(false);
|
||||
// Tracks if user has answered - use ref to survive re-renders
|
||||
const userInteractedRef = useRef(false);
|
||||
|
||||
// Reset when new AUR packages appear, BUT ONLY if user hasn't interacted yet
|
||||
// Reset on new AUR packages if no interaction
|
||||
useEffect(() => {
|
||||
if (show && aurAppNames.length > 0 && !userInteractedRef.current) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
@@ -56,9 +49,8 @@ export function AurFloatingCard({
|
||||
const handleHelperSelect = (helper: 'yay' | 'paru') => {
|
||||
setSelectedHelper(helper);
|
||||
setHelperChosen(true);
|
||||
userInteractedRef.current = true; // Don't ask again
|
||||
userInteractedRef.current = true;
|
||||
|
||||
// Start exit animation after a brief moment
|
||||
setTimeout(() => {
|
||||
setIsExiting(true);
|
||||
setTimeout(() => {
|
||||
@@ -68,7 +60,7 @@ export function AurFloatingCard({
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
userInteractedRef.current = true; // Don't ask again
|
||||
userInteractedRef.current = true;
|
||||
setIsExiting(true);
|
||||
setTimeout(() => {
|
||||
setDismissed(true);
|
||||
@@ -76,9 +68,7 @@ export function AurFloatingCard({
|
||||
}, 200);
|
||||
};
|
||||
|
||||
// Show confirmation message after selecting helper, auto-dismiss after 3s
|
||||
if (showConfirmation) {
|
||||
// Auto dismiss after 3 seconds
|
||||
setTimeout(() => {
|
||||
setDismissed(true);
|
||||
}, 3000);
|
||||
@@ -97,20 +87,19 @@ export function AurFloatingCard({
|
||||
);
|
||||
}
|
||||
|
||||
// Hide cards while exiting
|
||||
if (isExiting && helperChosen) {
|
||||
return (
|
||||
<div className="fixed top-4 left-1/2 -translate-x-1/2 md:left-auto md:right-4 md:translate-x-0 z-30 flex flex-col gap-3 items-center md:items-end">
|
||||
<div
|
||||
className="w-72 bg-[var(--bg-secondary)] backdrop-blur-xl border-l-4 shadow-lg overflow-hidden"
|
||||
style={{ borderLeftColor: 'var(--accent)', animation: 'slideOutToRight 0.25s ease-out forwards' }}
|
||||
className="w-72 bg-[var(--bg-primary)] border border-[var(--border-primary)]/30 rounded-xl shadow-lg overflow-hidden"
|
||||
style={{ animation: 'slideOutToRight 0.25s ease-out forwards' }}
|
||||
>
|
||||
<div className="p-4" />
|
||||
</div>
|
||||
{hasAnswered !== null && (
|
||||
<div
|
||||
className="w-72 bg-[var(--bg-secondary)] backdrop-blur-xl border-l-4 shadow-lg overflow-hidden"
|
||||
style={{ borderLeftColor: 'var(--accent)', animation: 'slideOutToRight 0.2s ease-out forwards' }}
|
||||
className="w-72 bg-[var(--bg-primary)] border border-[var(--border-primary)]/30 rounded-xl shadow-lg overflow-hidden"
|
||||
style={{ animation: 'slideOutToRight 0.2s ease-out forwards' }}
|
||||
>
|
||||
<div className="p-4" />
|
||||
</div>
|
||||
@@ -121,22 +110,18 @@ export function AurFloatingCard({
|
||||
|
||||
return (
|
||||
<div className="fixed top-4 left-1/2 -translate-x-1/2 md:left-auto md:right-4 md:translate-x-0 z-30 flex flex-col gap-3 items-center md:items-end">
|
||||
{/* Card 1: Do you have an AUR helper? - AccessGuide style */}
|
||||
<div
|
||||
className={`
|
||||
w-72 bg-[var(--bg-secondary)] backdrop-blur-xl
|
||||
border-l-4 shadow-lg
|
||||
w-72 bg-[var(--bg-primary)] border border-[var(--border-primary)]/30 rounded-xl shadow-lg
|
||||
overflow-hidden
|
||||
transition-[box-shadow] duration-200
|
||||
`}
|
||||
style={{
|
||||
borderLeftColor: 'var(--accent)',
|
||||
animation: isExiting
|
||||
? 'slideOutToRight 0.2s ease-out forwards'
|
||||
: 'slideInFromRight 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards'
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 flex items-start justify-between gap-3">
|
||||
<div className="flex-1">
|
||||
<p className="text-[11px] text-[var(--text-muted)] tracking-wide uppercase mb-1">
|
||||
@@ -155,16 +140,15 @@ export function AurFloatingCard({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="px-4 pb-3 flex gap-2">
|
||||
<button
|
||||
onClick={() => handleFirstAnswer(true)}
|
||||
className={`
|
||||
flex-1 py-2 px-4 text-sm font-medium
|
||||
flex-1 py-2 px-4 rounded-lg text-sm font-medium
|
||||
transition-[background-color,color] duration-200 ease-out
|
||||
${hasAnswered === true
|
||||
? 'bg-[var(--accent)]/20 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
? 'bg-[var(--bg-secondary)] border border-[var(--border-primary)]/40 text-[var(--text-primary)] shadow-sm'
|
||||
: 'bg-[var(--bg-tertiary)]/70 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -173,11 +157,11 @@ export function AurFloatingCard({
|
||||
<button
|
||||
onClick={() => handleFirstAnswer(false)}
|
||||
className={`
|
||||
flex-1 py-2 px-4 text-sm font-medium
|
||||
flex-1 py-2 px-4 rounded-lg text-sm font-medium
|
||||
transition-[background-color,color] duration-200 ease-out
|
||||
${hasAnswered === false
|
||||
? 'bg-[var(--accent)]/20 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
? 'bg-[var(--bg-secondary)] border border-[var(--border-primary)]/40 text-[var(--text-primary)] shadow-sm'
|
||||
: 'bg-[var(--bg-tertiary)]/70 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -185,7 +169,6 @@ export function AurFloatingCard({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Hint */}
|
||||
<div className="px-4 pb-3 -mt-1">
|
||||
<p className="text-[10px] text-[var(--text-muted)]/50 leading-relaxed">
|
||||
Change anytime in preview window
|
||||
@@ -193,23 +176,19 @@ export function AurFloatingCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2: Which helper? (appears after first answer) - AccessGuide style */}
|
||||
{hasAnswered !== null && (
|
||||
<div
|
||||
className={`
|
||||
w-72 bg-[var(--bg-secondary)] backdrop-blur-xl
|
||||
border-l-4 shadow-lg
|
||||
w-72 bg-[var(--bg-primary)] border border-[var(--border-primary)]/30 rounded-xl shadow-lg
|
||||
overflow-hidden
|
||||
`}
|
||||
style={{
|
||||
borderLeftColor: 'var(--accent)',
|
||||
animation: isExiting
|
||||
? 'slideOutToRight 0.15s ease-out forwards'
|
||||
: 'slideInFromRightSecond 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) 0.05s forwards',
|
||||
opacity: 0
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 flex items-center justify-between gap-3">
|
||||
<p className="text-[15px] text-[var(--text-primary)] font-medium">
|
||||
{hasAnswered
|
||||
@@ -226,16 +205,15 @@ export function AurFloatingCard({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Helper selection */}
|
||||
<div className="px-4 pb-4 flex gap-2">
|
||||
<button
|
||||
onClick={() => handleHelperSelect('yay')}
|
||||
className={`
|
||||
flex-1 py-2.5 px-4 text-sm font-medium
|
||||
flex-1 py-2.5 px-4 rounded-lg text-sm font-medium
|
||||
transition-[background-color,color] duration-200 ease-out
|
||||
${selectedHelper === 'yay' && helperChosen
|
||||
? 'bg-[var(--accent)]/20 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
? 'bg-[var(--bg-secondary)] border border-[var(--border-primary)]/40 text-[var(--text-primary)] shadow-sm'
|
||||
: 'bg-[var(--bg-tertiary)]/70 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -247,11 +225,11 @@ export function AurFloatingCard({
|
||||
<button
|
||||
onClick={() => handleHelperSelect('paru')}
|
||||
className={`
|
||||
flex-1 py-2.5 px-4 text-sm font-medium
|
||||
flex-1 py-2.5 px-4 rounded-lg text-sm font-medium
|
||||
transition-[background-color,color] duration-200 ease-out
|
||||
${selectedHelper === 'paru' && helperChosen
|
||||
? 'bg-[var(--accent)]/20 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
? 'bg-[var(--bg-secondary)] border border-[var(--border-primary)]/40 text-[var(--text-primary)] shadow-sm'
|
||||
: 'bg-[var(--bg-tertiary)]/70 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
||||
@@ -50,11 +50,11 @@ export function AurPopover({
|
||||
{/* Popover */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute bottom-full left-0 mb-2 w-64 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-xl shadow-2xl overflow-hidden"
|
||||
className="absolute bottom-full left-0 mb-2 w-64 bg-[var(--bg-primary)] border border-[var(--border-primary)]/30 rounded-xl shadow-lg overflow-hidden"
|
||||
style={{ animation: 'tooltipSlideUp 0.2s ease-out' }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2 border-b border-[var(--border-primary)] bg-[var(--bg-tertiary)]">
|
||||
<div className="px-3 py-2.5 border-b border-[var(--border-primary)]/15">
|
||||
<p className="text-xs font-medium text-[var(--text-primary)]">AUR Packages</p>
|
||||
<p className="text-xs text-[var(--text-muted)]">
|
||||
{hasYayInstalled ? 'Using yay' : 'Will install yay first'}
|
||||
@@ -74,7 +74,7 @@ export function AurPopover({
|
||||
</div>
|
||||
|
||||
{/* Yay Checkbox */}
|
||||
<div className="px-3 py-2 border-t border-[var(--border-primary)]">
|
||||
<div className="px-3 py-2.5 border-t border-[var(--border-primary)]/15">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none group">
|
||||
<div className="relative">
|
||||
<input
|
||||
|
||||
@@ -23,13 +23,12 @@ interface CommandDrawerProps {
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
distroColor: string;
|
||||
distroId: DistroId;
|
||||
// Nix unfree warning
|
||||
hasUnfreePackages?: boolean;
|
||||
unfreeAppNames?: string[];
|
||||
activeShortcut?: string | null;
|
||||
}
|
||||
|
||||
// Command drawer - bottom sheet on mobile, modal on desktop.
|
||||
// Nix gets special treatment: shows config file instead of terminal command.
|
||||
// Command drawer modal/bottom sheet.
|
||||
export function CommandDrawer({
|
||||
isOpen,
|
||||
isClosing,
|
||||
@@ -49,13 +48,14 @@ export function CommandDrawer({
|
||||
distroId,
|
||||
hasUnfreePackages = false,
|
||||
unfreeAppNames = [],
|
||||
activeShortcut,
|
||||
}: CommandDrawerProps) {
|
||||
const isNix = distroId === 'nix';
|
||||
// Swipe-to-dismiss for mobile users who hate tapping tiny X buttons
|
||||
// Swipe to dismiss
|
||||
const [dragOffset, setDragOffset] = useState(0);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragStartY = useRef(0);
|
||||
const DISMISS_THRESHOLD = 100; // Feels right™
|
||||
const DISMISS_THRESHOLD = 100;
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
dragStartY.current = e.touches[0].clientY;
|
||||
@@ -79,7 +79,7 @@ export function CommandDrawer({
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Copy command and auto-close after a celebratory 3 seconds
|
||||
// Copy command and close after delay
|
||||
const handleCopyAndClose = () => {
|
||||
onCopy();
|
||||
setTimeout(onClose, 3000);
|
||||
@@ -87,7 +87,6 @@ export function CommandDrawer({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/30 backdrop-blur-sm z-40"
|
||||
onClick={onClose}
|
||||
@@ -95,26 +94,24 @@ export function CommandDrawer({
|
||||
style={{ animation: isClosing ? 'fadeOut 0.3s ease-out forwards' : 'fadeIn 0.3s ease-out' }}
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="drawer-title"
|
||||
className="fixed z-50 bg-[var(--bg-secondary)] rounded-t-xl md:rounded-lg shadow-2xl
|
||||
className="fixed z-50 bg-[var(--bg-primary)] rounded-t-xl md:rounded-xl shadow-lg
|
||||
bottom-0 left-0 right-0
|
||||
md:bottom-auto md:top-1/2 md:left-1/2 md:-translate-x-1/2 md:-translate-y-1/2 md:max-w-2xl md:w-[90vw]"
|
||||
style={{
|
||||
boxShadow: '0 0 0 1px var(--border-primary), 0 20px 60px -10px rgba(0, 0, 0, 0.5)',
|
||||
border: '1px solid color-mix(in srgb, var(--border-primary), transparent 70%)',
|
||||
animation: isClosing
|
||||
? 'slideDown 0.3s cubic-bezier(0.32, 0, 0.67, 0) forwards'
|
||||
: dragOffset > 0 ? 'none' : 'slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
maxHeight: '80vh',
|
||||
transform: dragOffset > 0 ? `translateY(${dragOffset}px)` : undefined,
|
||||
transition: isDragging ? 'none' : 'transform 0.3s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
border: '1px solid var(--border-primary)',
|
||||
boxShadow: '0 16px 48px -8px rgba(0, 0, 0, 0.35)',
|
||||
}}
|
||||
>
|
||||
{/* Drawer Handle - mobile only, draggable */}
|
||||
<div
|
||||
className="flex justify-center pt-3 pb-2 md:hidden cursor-grab active:cursor-grabbing touch-none"
|
||||
onTouchStart={handleTouchStart}
|
||||
@@ -127,7 +124,7 @@ export function CommandDrawer({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border-primary)] bg-[var(--bg-secondary)]">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border-primary)]/15">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1 h-5 rounded-full" style={{ backgroundColor: distroColor }}></div>
|
||||
@@ -150,7 +147,7 @@ export function CommandDrawer({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 overflow-y-auto bg-[var(--bg-primary)]/50" style={{ maxHeight: 'calc(80vh - 140px)' }}>
|
||||
<div className="p-4 overflow-y-auto" style={{ maxHeight: 'calc(80vh - 140px)' }}>
|
||||
{showAur && (
|
||||
<AurDrawerSettings
|
||||
aurAppNames={aurAppNames}
|
||||
@@ -162,7 +159,6 @@ export function CommandDrawer({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Nix unfree packages warning */}
|
||||
{isNix && hasUnfreePackages && (
|
||||
<div className="mb-4 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30">
|
||||
<div className="flex items-start gap-2">
|
||||
@@ -177,24 +173,27 @@ export function CommandDrawer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Terminal preview - where the magic gets displayed */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-primary)] overflow-hidden shadow-sm">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-[var(--bg-tertiary)] border-b border-[var(--border-primary)]">
|
||||
<div className="bg-[var(--bg-secondary)]/60 rounded-lg border border-[var(--border-primary)]/15 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-[var(--border-primary)]/10">
|
||||
<span className="text-xs font-mono text-[var(--text-muted)]">{isNix ? 'nix' : 'bash'}</span>
|
||||
|
||||
{/* Desktop action buttons */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<button
|
||||
data-action="download"
|
||||
onClick={onDownload}
|
||||
className="h-8 px-4 flex items-center gap-2 rounded-md hover:bg-[var(--bg-primary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all text-xs font-medium"
|
||||
className={`h-8 px-4 flex items-center gap-2 rounded-md transition-all text-xs font-medium ${activeShortcut === 'd' ? 'bg-[var(--bg-primary)] text-[var(--text-primary)] scale-[0.98]' : 'hover:bg-[var(--bg-primary)] text-[var(--text-muted)] hover:text-[var(--text-primary)]'}`}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>{isNix ? 'configuration.nix' : 'Script'}</span>
|
||||
</button>
|
||||
<button
|
||||
data-action="copy"
|
||||
onClick={handleCopyAndClose}
|
||||
className={`h-8 px-4 flex items-center gap-2 rounded-md text-xs font-medium transition-all ${copied
|
||||
? 'shadow-sm'
|
||||
? 'shadow-sm text-black'
|
||||
: activeShortcut === 'y'
|
||||
? 'bg-[var(--bg-primary)] text-[var(--text-primary)] scale-[0.98]'
|
||||
: 'hover:bg-[var(--bg-primary)] text-[var(--text-muted)] hover:text-[var(--text-primary)]'
|
||||
}`}
|
||||
style={{
|
||||
@@ -226,18 +225,22 @@ export function CommandDrawer({
|
||||
</div>
|
||||
|
||||
{/* Mobile Actions */}
|
||||
<div className="md:hidden flex items-stretch gap-3 px-4 py-3 border-t border-[var(--border-primary)] bg-[var(--bg-secondary)]">
|
||||
<div className="md:hidden flex items-stretch gap-3 px-4 py-3 border-t border-[var(--border-primary)]/15">
|
||||
<button
|
||||
data-action="download"
|
||||
onClick={onDownload}
|
||||
className="flex-1 h-11 flex items-center justify-center gap-2 rounded-md bg-[var(--bg-tertiary)] border border-[var(--border-primary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] active:scale-[0.98] transition-all font-medium text-sm"
|
||||
className={`flex-1 h-11 flex items-center justify-center gap-2 rounded-md border text-sm font-medium transition-all ${activeShortcut === 'd' ? 'bg-[var(--bg-hover)] text-[var(--text-primary)] border-[var(--border-secondary)] scale-[0.98]' : 'bg-[var(--bg-tertiary)] border-[var(--border-primary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]'}`}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
data-action="copy"
|
||||
onClick={handleCopyAndClose}
|
||||
className={`flex-1 h-11 flex items-center justify-center gap-2 rounded-md font-medium text-sm active:scale-[0.98] transition-all shadow-sm ${copied
|
||||
className={`flex-1 h-11 flex items-center justify-center gap-2 rounded-md font-medium text-sm transition-all shadow-sm ${copied
|
||||
? 'text-black'
|
||||
: activeShortcut === 'y'
|
||||
? 'bg-[var(--bg-hover)] border-[var(--border-secondary)] scale-[0.98]'
|
||||
: 'text-[var(--text-primary)] bg-[var(--bg-tertiary)] border border-[var(--border-primary)]'
|
||||
}`}
|
||||
style={{
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Check, Copy, ChevronUp, Download, X } from 'lucide-react';
|
||||
import { distros, type DistroId } from '@/lib/data';
|
||||
import { generateInstallScript } from '@/lib/generateInstallScript';
|
||||
import { analytics } from '@/lib/analytics';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
import { ShortcutsBar } from './ShortcutsBar';
|
||||
import { AurFloatingCard } from './AurFloatingCard';
|
||||
import { CommandDrawer } from './CommandDrawer';
|
||||
@@ -25,9 +25,13 @@ interface CommandFooterProps {
|
||||
clearAll: () => void;
|
||||
selectedHelper: 'yay' | 'paru';
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
// Nix unfree
|
||||
hasUnfreePackages?: boolean;
|
||||
unfreeAppNames?: string[];
|
||||
drawerOpen?: boolean;
|
||||
drawerClosing?: boolean;
|
||||
onDrawerOpen?: () => void;
|
||||
onDrawerClose?: () => void;
|
||||
activeShortcut?: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,17 +52,21 @@ export function CommandFooter({
|
||||
setSelectedHelper,
|
||||
hasUnfreePackages,
|
||||
unfreeAppNames,
|
||||
drawerOpen: externalDrawerOpen,
|
||||
drawerClosing: externalDrawerClosing,
|
||||
onDrawerOpen: externalOnDrawerOpen,
|
||||
onDrawerClose: externalOnDrawerClose,
|
||||
activeShortcut,
|
||||
}: CommandFooterProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [drawerClosing, setDrawerClosing] = useState(false);
|
||||
const [internalDrawerOpen, setInternalDrawerOpen] = useState(false);
|
||||
const [internalDrawerClosing, setInternalDrawerClosing] = useState(false);
|
||||
const [hasEverHadSelection, setHasEverHadSelection] = useState(false);
|
||||
const initialCountRef = useRef(selectedCount);
|
||||
|
||||
const { toggle: toggleTheme } = useTheme();
|
||||
const drawerOpen = externalDrawerOpen ?? internalDrawerOpen;
|
||||
const drawerClosing = externalDrawerClosing ?? internalDrawerClosing;
|
||||
|
||||
// Track if user has actually interacted - hide the bar until then.
|
||||
// Otherwise it just sits there looking sad with "No apps selected".
|
||||
useEffect(() => {
|
||||
if (selectedCount !== initialCountRef.current && !hasEverHadSelection) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
@@ -67,12 +75,16 @@ export function CommandFooter({
|
||||
}, [selectedCount, hasEverHadSelection]);
|
||||
|
||||
const closeDrawer = useCallback(() => {
|
||||
setDrawerClosing(true);
|
||||
if (externalOnDrawerClose) {
|
||||
externalOnDrawerClose();
|
||||
} else {
|
||||
setInternalDrawerClosing(true);
|
||||
setTimeout(() => {
|
||||
setDrawerOpen(false);
|
||||
setDrawerClosing(false);
|
||||
setInternalDrawerOpen(false);
|
||||
setInternalDrawerClosing(false);
|
||||
}, 250);
|
||||
}, []);
|
||||
}
|
||||
}, [externalOnDrawerClose]);
|
||||
|
||||
// Close drawer on Escape
|
||||
useEffect(() => {
|
||||
@@ -117,45 +129,6 @@ export function CommandFooter({
|
||||
analytics.scriptDownloaded(distroName, selectedCount);
|
||||
}, [selectedCount, selectedDistro, selectedApps, selectedHelper]);
|
||||
|
||||
// Global keyboard shortcuts (vim-like)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore modifier keys (prevents conflicts with browser shortcuts)
|
||||
if (e.ctrlKey || e.altKey || e.metaKey) return;
|
||||
|
||||
const alwaysEnabled = ['t', 'c'];
|
||||
if (selectedCount === 0 && !alwaysEnabled.includes(e.key)) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'y': handleCopy(); break;
|
||||
case 'd': handleDownload(); break;
|
||||
case 't':
|
||||
document.body.classList.add('theme-flash');
|
||||
setTimeout(() => document.body.classList.remove('theme-flash'), 150);
|
||||
toggleTheme();
|
||||
break;
|
||||
case 'c': clearAll(); break;
|
||||
case '1': if (showAur) setSelectedHelper('yay'); break;
|
||||
case '2': if (showAur) setSelectedHelper('paru'); break;
|
||||
case 'Tab':
|
||||
e.preventDefault();
|
||||
if (selectedCount > 0) setDrawerOpen(prev => !prev);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [selectedCount, toggleTheme, clearAll, showAur, setSelectedHelper, handleCopy, handleDownload]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* AUR Floating Card */}
|
||||
@@ -168,7 +141,6 @@ export function CommandFooter({
|
||||
setSelectedHelper={setSelectedHelper}
|
||||
/>
|
||||
|
||||
{/* Command Drawer */}
|
||||
<CommandDrawer
|
||||
isOpen={drawerOpen}
|
||||
isClosing={drawerClosing}
|
||||
@@ -190,17 +162,16 @@ export function CommandFooter({
|
||||
unfreeAppNames={unfreeAppNames}
|
||||
/>
|
||||
|
||||
{/* Animated footer container - only shows after first selection */}
|
||||
{/* Animated footer container - shows after first selection */}
|
||||
{hasEverHadSelection && (
|
||||
<div
|
||||
className="fixed bottom-0 left-0 right-0 p-3"
|
||||
className="fixed bottom-0 left-0 right-0 p-3 lg:hidden"
|
||||
style={{
|
||||
zIndex: 10,
|
||||
animation: 'footerSlideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) both'
|
||||
}}
|
||||
>
|
||||
<div className="relative w-[85%] mx-auto">
|
||||
{/* Soft glow behind bars */}
|
||||
<div
|
||||
className="absolute -inset-12 pointer-events-none"
|
||||
style={{
|
||||
@@ -225,13 +196,20 @@ export function CommandFooter({
|
||||
setSelectedHelper={setSelectedHelper}
|
||||
/>
|
||||
|
||||
{/* Command Bar - AccessGuide style */}
|
||||
<div className="bg-[var(--bg-tertiary)] font-mono text-xs overflow-hidden border-l-4 shadow-2xl"
|
||||
style={{ borderLeftColor: distroColor }}>
|
||||
<div className="flex items-stretch">
|
||||
{/* Preview button (hidden on mobile) */}
|
||||
{/* Preview button */}
|
||||
<button
|
||||
onClick={() => selectedCount > 0 && setDrawerOpen(true)}
|
||||
onClick={() => {
|
||||
if (selectedCount > 0) {
|
||||
if (externalOnDrawerOpen) {
|
||||
externalOnDrawerOpen();
|
||||
} else {
|
||||
setInternalDrawerOpen(true);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={selectedCount === 0}
|
||||
className={`hidden md:flex items-center gap-2 px-5 py-3 border-r border-[var(--border-primary)]/20 transition-all shrink-0 font-medium ${selectedCount === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
title="Toggle Preview (Tab)"
|
||||
@@ -257,22 +235,28 @@ export function CommandFooter({
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Command text */}
|
||||
<div
|
||||
className="flex-1 min-w-0 flex items-center justify-center px-4 py-4 overflow-hidden bg-[var(--bg-secondary)] cursor-pointer hover:bg-[var(--bg-hover)] transition-colors group"
|
||||
onClick={() => selectedCount > 0 && setDrawerOpen(true)}
|
||||
onClick={() => {
|
||||
if (selectedCount > 0) {
|
||||
if (externalOnDrawerOpen) {
|
||||
externalOnDrawerOpen();
|
||||
} else {
|
||||
setInternalDrawerOpen(true);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<code className={`whitespace-nowrap overflow-x-auto command-scroll leading-none text-sm font-semibold ${selectedCount > 0 ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)]'}`}>
|
||||
{command}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{/* Clear button (hidden on mobile) */}
|
||||
<button
|
||||
onClick={clearAll}
|
||||
disabled={selectedCount === 0}
|
||||
className={`hidden md:flex items-center gap-2 px-4 py-3 border-l border-[var(--border-primary)]/20 transition-all duration-150 font-sans text-sm ${selectedCount > 0
|
||||
? 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] active:scale-[0.97]'
|
||||
? activeShortcut === 'c' ? 'bg-red-500/20 text-red-400 scale-[0.97]' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] active:scale-[0.97]'
|
||||
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
title="Clear All (c)"
|
||||
@@ -291,12 +275,11 @@ export function CommandFooter({
|
||||
<span className="hidden sm:inline whitespace-nowrap">Clear</span>
|
||||
</button>
|
||||
|
||||
{/* Download button (hidden on mobile) */}
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={selectedCount === 0}
|
||||
className={`hidden md:flex items-center gap-2 px-4 py-3 border-l border-[var(--border-primary)]/20 transition-all duration-150 font-sans text-sm ${selectedCount > 0
|
||||
? 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] active:scale-[0.97]'
|
||||
? activeShortcut === 'd' ? 'bg-[var(--bg-hover)] opacity-80 scale-[0.97]' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] active:scale-[0.97]'
|
||||
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
title="Download Script (d)"
|
||||
@@ -315,14 +298,13 @@ export function CommandFooter({
|
||||
<span className="hidden sm:inline whitespace-nowrap">Download</span>
|
||||
</button>
|
||||
|
||||
{/* Copy button (hidden on mobile) */}
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={selectedCount === 0}
|
||||
className={`hidden md:flex items-center gap-2 px-4 py-3 border-l border-[var(--border-primary)]/20 transition-all duration-150 font-sans text-sm ${selectedCount > 0
|
||||
? (copied
|
||||
? 'text-emerald-400 font-medium'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] active:scale-[0.97]')
|
||||
: activeShortcut === 'y' ? 'bg-[var(--bg-hover)] opacity-80 scale-[0.97]' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] active:scale-[0.97]')
|
||||
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
title="Copy Command (y)"
|
||||
|
||||
@@ -14,11 +14,7 @@ interface ShortcutsBarProps {
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Neovim-style statusline at the bottom.
|
||||
* Shows search, app count, AUR helper toggle, and keyboard shortcuts.
|
||||
* If you use vim, you'll feel right at home.
|
||||
*/
|
||||
// Bottom statusbar with controls and shortcuts.
|
||||
export function ShortcutsBar({
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
@@ -42,9 +38,7 @@ export function ShortcutsBar({
|
||||
<div className="bg-[var(--bg-tertiary)] border-l-4 font-mono text-xs overflow-hidden"
|
||||
style={{ borderLeftColor: distroColor }}>
|
||||
<div className="flex items-stretch justify-between">
|
||||
{/* LEFT SECTION */}
|
||||
<div className="flex items-stretch">
|
||||
{/* Mode Badge - like nvim NORMAL/INSERT (hidden on mobile) */}
|
||||
<div
|
||||
className="hidden md:flex text-white px-3 py-1 font-bold items-center whitespace-nowrap"
|
||||
style={{ backgroundColor: distroColor }}
|
||||
@@ -52,7 +46,6 @@ export function ShortcutsBar({
|
||||
{distroName.toUpperCase()}
|
||||
</div>
|
||||
|
||||
{/* Search Section */}
|
||||
<div className="flex items-center gap-1.5 px-3 py-1 bg-[var(--bg-secondary)] border-r border-[var(--border-primary)]/30">
|
||||
<span className="text-[var(--text-muted)]">/</span>
|
||||
<input
|
||||
@@ -80,14 +73,12 @@ export function ShortcutsBar({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* App count */}
|
||||
{selectedCount > 0 && (
|
||||
<div className="flex items-center px-3 py-1 text-[var(--text-muted)] border-r border-[var(--border-primary)]/30 whitespace-nowrap">
|
||||
[{selectedCount} app{selectedCount !== 1 ? 's' : ''}]
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AUR Helper Switch */}
|
||||
{showAur && (
|
||||
<div className="flex items-stretch border-r border-[var(--border-primary)]/30">
|
||||
<button
|
||||
@@ -108,13 +99,10 @@ export function ShortcutsBar({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* RIGHT SECTION - Compact Shortcuts (hidden on mobile) */}
|
||||
<div className="hidden md:flex items-stretch">
|
||||
<div className="hidden sm:flex items-center gap-3 px-3 py-1 text-[var(--text-muted)] text-[10px] border-l border-[var(--border-primary)]/30">
|
||||
{/* Navigation */}
|
||||
<span className="hidden lg:inline"><b className="text-[var(--text-secondary)]">←↓↑→ </b>/<b className="text-[var(--text-secondary)]"> hjkl</b> Navigation</span>
|
||||
<span className="hidden lg:inline opacity-30">·</span>
|
||||
{/* Actions */}
|
||||
<span><b className="text-[var(--text-secondary)]">/</b> search</span>
|
||||
<span className="opacity-30">·</span>
|
||||
<span><b className="text-[var(--text-secondary)]">Space</b> toggle</span>
|
||||
@@ -124,7 +112,6 @@ export function ShortcutsBar({
|
||||
<span><b className="text-[var(--text-secondary)]">?</b> help</span>
|
||||
</div>
|
||||
|
||||
{/* End badge - like nvim line:col */}
|
||||
<div
|
||||
className="text-white px-3 py-1 flex items-center font-bold text-xs tracking-wider"
|
||||
style={{ backgroundColor: distroColor }}
|
||||
|
||||
@@ -10,11 +10,10 @@ interface TooltipProps {
|
||||
setRef?: (el: HTMLDivElement | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow-cursor tooltip that appears on hover.
|
||||
* Desktop only - mobile users don't have a cursor to follow.
|
||||
* Supports markdown-ish formatting: **bold**, `code`, and [links](url).
|
||||
*/
|
||||
const TOOLTIP_WIDTH = 300;
|
||||
const ARROW_SIZE = 12;
|
||||
|
||||
// Tooltip component.
|
||||
export function Tooltip({ tooltip, onMouseEnter, onMouseLeave, setRef }: TooltipProps) {
|
||||
const [current, setCurrent] = useState<TooltipState | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -66,15 +65,34 @@ export function Tooltip({ tooltip, onMouseEnter, onMouseLeave, setRef }: Tooltip
|
||||
});
|
||||
};
|
||||
|
||||
const isRightAnchored = typeof window !== 'undefined' && (current.x + 300 > window.innerWidth);
|
||||
const contentTransform = isRightAnchored ? 'translateX(-278px)' : 'translateX(-22px)';
|
||||
const zoom = typeof window !== 'undefined' ? (parseFloat(getComputedStyle(document.documentElement).zoom) || 1) : 1;
|
||||
const viewportWidth = typeof window !== 'undefined' ? window.innerWidth / zoom : 1920;
|
||||
const mouseX = current.x;
|
||||
|
||||
const tooltipLeftNormal = mouseX - 22;
|
||||
const overflowsRight = tooltipLeftNormal + TOOLTIP_WIDTH > viewportWidth - 12;
|
||||
|
||||
let tooltipLeft: number;
|
||||
let arrowLeft: number;
|
||||
|
||||
if (overflowsRight) {
|
||||
tooltipLeft = mouseX - TOOLTIP_WIDTH + 22;
|
||||
if (tooltipLeft < 12) tooltipLeft = 12;
|
||||
arrowLeft = mouseX - tooltipLeft - ARROW_SIZE / 2;
|
||||
} else {
|
||||
tooltipLeft = tooltipLeftNormal;
|
||||
if (tooltipLeft < 12) tooltipLeft = 12;
|
||||
arrowLeft = mouseX - tooltipLeft - ARROW_SIZE / 2;
|
||||
}
|
||||
|
||||
arrowLeft = Math.max(8, Math.min(arrowLeft, TOOLTIP_WIDTH - 20));
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setRef}
|
||||
role="tooltip"
|
||||
className="fixed hidden md:block pointer-events-auto z-[9999]"
|
||||
style={{ left: current.x, top: current.y - 12 }} // Moved up slightly to clear cursor
|
||||
style={{ left: tooltipLeft, top: current.y - 12 }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
@@ -83,16 +101,13 @@ export function Tooltip({ tooltip, onMouseEnter, onMouseLeave, setRef }: Tooltip
|
||||
transition-opacity duration-75
|
||||
${visible ? 'opacity-100' : 'opacity-0 pointer-events-none'}
|
||||
`}>
|
||||
{/* AccessGuide-style tooltip - rectangular with left border accent */}
|
||||
<div
|
||||
className="px-3.5 py-2.5 shadow-lg overflow-hidden border-l-4 relative"
|
||||
style={{
|
||||
minWidth: '300px',
|
||||
maxWidth: '300px',
|
||||
width: `${TOOLTIP_WIDTH}px`,
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
borderLeftColor: 'var(--accent)',
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.2)',
|
||||
transform: contentTransform, // Shift tooltip so arrow aligns with mouse
|
||||
}}
|
||||
>
|
||||
<p className="text-[13px] leading-[1.55] text-[var(--text-secondary)] break-words" style={{ wordBreak: 'break-word' }}>
|
||||
@@ -100,9 +115,10 @@ export function Tooltip({ tooltip, onMouseEnter, onMouseLeave, setRef }: Tooltip
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Arrow aligned to mouse position */}
|
||||
{/* Arrow at left: 16px matches the visual design, so we shift wrapper -22px to align 16px + 6px(half arrow) approx to 0 */}
|
||||
<div className="absolute left-0 -bottom-[6px]" style={{ transform: 'translateX(-6px)' }}>
|
||||
<div
|
||||
className="absolute -bottom-[6px]"
|
||||
style={{ left: `${arrowLeft}px` }}
|
||||
>
|
||||
<div
|
||||
className="w-3 h-3 rotate-45"
|
||||
style={{
|
||||
|
||||
@@ -7,10 +7,7 @@ import { distros, type DistroId } from '@/lib/data';
|
||||
import { analytics } from '@/lib/analytics';
|
||||
import { DistroIcon } from './DistroIcon';
|
||||
|
||||
/**
|
||||
* Distro picker dropdown. Uses portal rendering so the dropdown isn't
|
||||
* clipped by parent overflow. Learned that lesson the hard way.
|
||||
*/
|
||||
// Distro picker dropdown.
|
||||
export function DistroSelector({
|
||||
selectedDistro,
|
||||
onSelect
|
||||
@@ -43,11 +40,8 @@ export function DistroSelector({
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
// Portal the dropdown to body so it's not affected by parent styles.
|
||||
// The positioning math looks scary but it's just "anchor to button bottom-right".
|
||||
const dropdown = isOpen && mounted ? (
|
||||
<>
|
||||
{/* Backdrop with subtle blur */}
|
||||
<div
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="backdrop-blur-[2px]"
|
||||
@@ -58,7 +52,6 @@ export function DistroSelector({
|
||||
background: 'rgba(0,0,0,0.05)',
|
||||
}}
|
||||
/>
|
||||
{/* Dropdown - AccessGuide style: rectangular with left border */}
|
||||
<div
|
||||
className="distro-dropdown bg-[var(--bg-secondary)] border-l-4 rounded-md"
|
||||
style={{
|
||||
@@ -74,7 +67,6 @@ export function DistroSelector({
|
||||
animation: 'distroDropdownOpen 0.25s ease-out',
|
||||
}}
|
||||
>
|
||||
{/* Distro List */}
|
||||
<div>
|
||||
{distros.map((distro, i) => (
|
||||
<button
|
||||
|
||||
@@ -5,10 +5,7 @@ import { createPortal } from 'react-dom';
|
||||
import { HelpCircle, X } from 'lucide-react';
|
||||
import { analytics } from '@/lib/analytics';
|
||||
|
||||
/**
|
||||
* Help modal with keyboard shortcuts and getting started guide.
|
||||
* Opens with "?" key - because that's what you'd naturally press.
|
||||
*/
|
||||
// Help modal.
|
||||
export function HowItWorks() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
@@ -24,7 +21,6 @@ export function HowItWorks() {
|
||||
const handleClose = () => {
|
||||
setIsClosing(true);
|
||||
analytics.helpClosed();
|
||||
// Wait for exit animation to finish
|
||||
setTimeout(() => {
|
||||
setIsOpen(false);
|
||||
setIsClosing(false);
|
||||
@@ -36,7 +32,6 @@ export function HowItWorks() {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Lock body scroll when modal is open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
@@ -48,13 +43,10 @@ export function HowItWorks() {
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// Global keyboard shortcut: ? to toggle modal
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Ignore if typing in input
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||||
|
||||
// Skip if Ctrl/Alt/Meta are pressed (Shift is allowed for ?)
|
||||
if (e.ctrlKey || e.altKey || e.metaKey) return;
|
||||
|
||||
if (e.key === '?' || (e.shiftKey && e.key === '/')) {
|
||||
@@ -66,7 +58,6 @@ export function HowItWorks() {
|
||||
}
|
||||
}
|
||||
|
||||
// Close on Escape
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
handleClose();
|
||||
}
|
||||
@@ -78,7 +69,6 @@ export function HowItWorks() {
|
||||
|
||||
const modal = (
|
||||
<>
|
||||
{/* Backdrop with blur */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/30 backdrop-blur-sm z-[99998]"
|
||||
onClick={handleClose}
|
||||
@@ -89,18 +79,15 @@ export function HowItWorks() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Modal - AccessGuide style: rectangular with left border accent */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="how-it-works-title"
|
||||
className="fixed bg-[var(--bg-primary)] border-l-4 z-[99999]"
|
||||
className="fixed bg-[var(--bg-primary)] border border-[var(--border-primary)]/30 rounded-xl z-[99999]"
|
||||
style={{
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
borderRadius: '0 4px 4px 0',
|
||||
borderLeftColor: 'var(--accent)',
|
||||
width: '620px',
|
||||
maxWidth: 'calc(100vw - 32px)',
|
||||
maxHeight: 'min(85vh, 720px)',
|
||||
@@ -110,11 +97,10 @@ export function HowItWorks() {
|
||||
? 'modalSlideOut 0.2s ease-out forwards'
|
||||
: 'modalSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 16px 48px -8px rgba(0, 0, 0, 0.25)',
|
||||
boxShadow: '0 16px 48px -8px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border-secondary)]">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border-primary)]/15">
|
||||
<h3 id="how-it-works-title" className="text-base font-semibold text-[var(--text-primary)]">
|
||||
Help
|
||||
</h3>
|
||||
@@ -126,12 +112,10 @@ export function HowItWorks() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6" style={{ scrollbarGutter: 'stable' }}>
|
||||
|
||||
{/* Shortcuts - AccessGuide style */}
|
||||
<section>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-4 pl-3 border-l-2 border-[var(--accent)]">Keyboard Shortcuts</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-4 pl-3 border-l-2 border-[var(--text-muted)]/30">Keyboard Shortcuts</h4>
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-2">
|
||||
{[
|
||||
['↑↓←→', 'Navigate through apps'],
|
||||
@@ -148,7 +132,7 @@ export function HowItWorks() {
|
||||
['1 / 2', 'Switch AUR helper (yay/paru)'],
|
||||
].map(([key, desc]) => (
|
||||
<div key={key} className="flex items-center gap-3 text-sm">
|
||||
<kbd className="inline-flex items-center justify-center min-w-[52px] px-2 py-1 bg-[var(--bg-secondary)] border-l-2 border-[var(--accent)] text-xs font-mono text-[var(--text-secondary)]">
|
||||
<kbd className="inline-flex items-center justify-center min-w-[52px] px-2 py-1 bg-[var(--bg-secondary)] border border-[var(--border-primary)]/20 rounded text-xs font-mono text-[var(--text-secondary)]">
|
||||
{key}
|
||||
</kbd>
|
||||
<span className="text-[var(--text-muted)]">{desc}</span>
|
||||
@@ -157,9 +141,8 @@ export function HowItWorks() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Getting Started - AccessGuide style */}
|
||||
<section>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 pl-3 border-l-2 border-[var(--accent)]">Getting Started</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 pl-3 border-l-2 border-[var(--text-muted)]/30">Getting Started</h4>
|
||||
<ol className="space-y-2 text-sm text-[var(--text-muted)] leading-relaxed">
|
||||
<li>
|
||||
<strong className="text-[var(--text-secondary)]">1. Pick your distro</strong> — Select your Linux distribution from the dropdown at the top. This determines which package manager commands TuxMate generates for you.
|
||||
@@ -176,9 +159,8 @@ export function HowItWorks() {
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* Notes - AccessGuide style */}
|
||||
<section>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 pl-3 border-l-2 border-[var(--accent)]">Good to Know</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 pl-3 border-l-2 border-[var(--text-muted)]/30">Good to Know</h4>
|
||||
<ul className="space-y-2 text-sm text-[var(--text-muted)] leading-relaxed">
|
||||
<li>
|
||||
<strong className="text-[var(--text-secondary)]">Greyed out apps</strong> aren't available in your distro's official repositories. Try switching to Flatpak or Snap in the dropdown, or hover the info icon next to the app for alternative installation methods.
|
||||
@@ -196,7 +178,7 @@ export function HowItWorks() {
|
||||
<strong className="text-[var(--text-secondary)]">NixOS</strong> — Generates `environment.systemPackages`. If you pick unfree apps, the download includes comments showing exactly what to whitelist with `allowUnfree`.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-[var(--text-secondary)]">Script Safety</strong> — Downloaded scripts are robust and idempotent. They include error handling, network retries, and system checks. Run them with <code className="px-1 py-0.5 bg-[var(--bg-secondary)] border-l-2 border-[var(--accent)] text-xs font-mono">bash tuxmate-*.sh</code> to safely install your selection.
|
||||
<strong className="text-[var(--text-secondary)]">Script Safety</strong> — Downloaded scripts are robust and idempotent. They include error handling, network retries, and system checks. Run them with <code className="px-1 py-0.5 bg-[var(--bg-secondary)] border border-[var(--border-primary)]/20 rounded text-xs font-mono">bash tuxmate-*.sh</code> to safely install your selection.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
450
src/components/sidebar/Sidebar.tsx
Normal file
450
src/components/sidebar/Sidebar.tsx
Normal file
@@ -0,0 +1,450 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Check, Copy, Download, X, Search, ChevronDown, Github, Heart, Eye, Terminal, Trash2 } from 'lucide-react';
|
||||
import { distros, type DistroId } from '@/lib/data';
|
||||
import { generateInstallScript } from '@/lib/generateInstallScript';
|
||||
import { analytics } from '@/lib/analytics';
|
||||
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||
import { DistroIcon } from '@/components/distro/DistroIcon';
|
||||
import { HowItWorks } from '@/components/header/HowItWorks';
|
||||
|
||||
interface SidebarProps {
|
||||
selectedDistro: DistroId;
|
||||
onDistroSelect: (id: DistroId) => void;
|
||||
selectedApps: Set<string>;
|
||||
selectedCount: number;
|
||||
clearAll: () => void;
|
||||
command: string;
|
||||
searchQuery: string;
|
||||
onSearchChange: (query: string) => void;
|
||||
searchInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
hasAurPackages: boolean;
|
||||
aurAppNames: string[];
|
||||
selectedHelper: 'yay' | 'paru';
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
hasUnfreePackages?: boolean;
|
||||
unfreeAppNames?: string[];
|
||||
onOpenDrawer: () => void;
|
||||
activeShortcut?: string | null;
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
selectedDistro,
|
||||
onDistroSelect,
|
||||
selectedApps,
|
||||
selectedCount,
|
||||
clearAll,
|
||||
command,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
searchInputRef,
|
||||
hasAurPackages,
|
||||
aurAppNames,
|
||||
selectedHelper,
|
||||
setSelectedHelper,
|
||||
hasUnfreePackages,
|
||||
unfreeAppNames,
|
||||
onOpenDrawer,
|
||||
activeShortcut,
|
||||
}: SidebarProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [distroOpen, setDistroOpen] = useState(false);
|
||||
|
||||
const showAur = selectedDistro === 'arch' && hasAurPackages;
|
||||
const currentDistro = distros.find(d => d.id === selectedDistro);
|
||||
const distroColor = currentDistro?.color || 'var(--accent)';
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
if (selectedCount === 0) return;
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
const distroName = distros.find(d => d.id === selectedDistro)?.name || selectedDistro;
|
||||
analytics.commandCopied(distroName, selectedCount);
|
||||
setTimeout(() => setCopied(false), 3000);
|
||||
}, [command, selectedCount, selectedDistro]);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
if (selectedCount === 0) return;
|
||||
const script = generateInstallScript({
|
||||
distroId: selectedDistro,
|
||||
selectedAppIds: selectedApps,
|
||||
helper: selectedHelper,
|
||||
});
|
||||
const isNix = selectedDistro === 'nix';
|
||||
const mimeType = isNix ? 'text/plain' : 'text/x-shellscript';
|
||||
const blob = new Blob([script], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = isNix ? 'configuration.nix' : `tuxmate-${selectedDistro}.sh`;
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
const distroName = distros.find(d => d.id === selectedDistro)?.name || selectedDistro;
|
||||
analytics.scriptDownloaded(distroName, selectedCount);
|
||||
}, [selectedCount, selectedDistro, selectedApps, selectedHelper]);
|
||||
|
||||
const handleSearchKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Escape' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="sidebar fixed left-0 top-0 bottom-0 hidden lg:flex flex-col overflow-hidden"
|
||||
style={{
|
||||
width: '380px',
|
||||
zIndex: 20,
|
||||
}}
|
||||
>
|
||||
<div className="px-6 pt-7 pb-5">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="/tuxmate.png"
|
||||
alt="TuxMate Logo"
|
||||
className="w-16 h-16 object-contain shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col items-start">
|
||||
<h1 className="text-[28px] font-extrabold tracking-tight text-[var(--text-primary)] leading-none"
|
||||
style={{ fontFamily: 'var(--font-heading)', transition: 'color 0.5s' }}>
|
||||
TuxMate
|
||||
</h1>
|
||||
<p className="text-[11px] text-[var(--text-muted)] tracking-[0.14em] uppercase mt-2 font-medium leading-none"
|
||||
style={{ transition: 'color 0.5s' }}>
|
||||
Linux Bulk App Installer
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden sidebar-scroll">
|
||||
<div className="px-5 pb-5">
|
||||
<div className="sidebar-search flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg border border-[var(--border-primary)]/20 bg-transparent">
|
||||
<Search className="w-3.5 h-3.5 text-[var(--text-muted)] shrink-0 opacity-40" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
placeholder="Search apps..."
|
||||
className="flex-1 bg-transparent text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)]/40 outline-none"
|
||||
/>
|
||||
{searchQuery ? (
|
||||
<button
|
||||
onClick={() => onSearchChange('')}
|
||||
className="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
) : (
|
||||
<kbd className="text-[10px] text-[var(--text-muted)]/40 border border-[var(--border-primary)]/30 rounded px-1.5 py-0.5 font-mono">/</kbd>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-5 pb-5 relative">
|
||||
<p className="text-[10px] uppercase tracking-[0.15em] text-[var(--text-muted)] mb-2 px-1 font-semibold">Distribution</p>
|
||||
<button
|
||||
onClick={() => setDistroOpen(prev => !prev)}
|
||||
className="sidebar-distro-btn w-full flex items-center gap-3.5 px-4 py-3 rounded-xl border border-[var(--border-primary)]/20 bg-[var(--bg-secondary)]/50"
|
||||
>
|
||||
<div className="w-7 h-7 flex items-center justify-center shrink-0 rounded-lg"
|
||||
style={{ backgroundColor: `color-mix(in srgb, ${distroColor}, transparent 80%)` }}>
|
||||
<DistroIcon url={currentDistro?.iconUrl || ''} name={currentDistro?.name || ''} size={20} />
|
||||
</div>
|
||||
<span className="flex-1 text-left text-sm font-semibold text-[var(--text-primary)]">
|
||||
{currentDistro?.name}
|
||||
</span>
|
||||
<ChevronDown className={`w-4 h-4 text-[var(--text-muted)] transition-transform duration-200 ${distroOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{distroOpen && (
|
||||
<div className="absolute left-5 right-5 mt-2 rounded-xl bg-[var(--bg-secondary)] border border-[var(--border-primary)]/30 overflow-hidden shadow-xl z-50"
|
||||
style={{ animation: 'dropIn 0.2s ease-out' }}>
|
||||
{distros.map((distro) => (
|
||||
<button
|
||||
key={distro.id}
|
||||
onClick={() => {
|
||||
onDistroSelect(distro.id);
|
||||
setDistroOpen(false);
|
||||
analytics.distroSelected(distro.name);
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 py-2.5 px-4 text-left transition-all duration-100 ${selectedDistro === distro.id
|
||||
? ''
|
||||
: 'hover:bg-[var(--bg-hover)]'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: selectedDistro === distro.id
|
||||
? `color-mix(in srgb, ${distro.color}, transparent 85%)`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<div className="w-5 h-5 flex items-center justify-center">
|
||||
<DistroIcon url={distro.iconUrl} name={distro.name} size={18} />
|
||||
</div>
|
||||
<span className={`flex-1 text-sm ${selectedDistro === distro.id
|
||||
? 'text-[var(--text-primary)] font-semibold'
|
||||
: 'text-[var(--text-secondary)]'
|
||||
}`}>{distro.name}</span>
|
||||
{selectedDistro === distro.id && (
|
||||
<Check className="w-4 h-4" style={{ color: distro.color }} strokeWidth={2.5} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mx-5 mb-4 border-t border-[var(--border-primary)]/15" />
|
||||
<div className="px-5 pb-4">
|
||||
<div className="flex items-center justify-between mb-2.5 px-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
||||
<p className="text-[10px] uppercase tracking-[0.15em] text-[var(--text-muted)] font-semibold">Command</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedCount > 0 && (
|
||||
<span className="text-[12px] font-bold px-2 py-0.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: `color-mix(in srgb, ${distroColor}, transparent 82%)`,
|
||||
color: distroColor,
|
||||
}}>
|
||||
{selectedCount} app{selectedCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="sidebar-command-preview rounded-xl bg-[var(--bg-secondary)]/60 border border-[var(--border-primary)]/15 overflow-hidden cursor-pointer group"
|
||||
onClick={() => selectedCount > 0 && onOpenDrawer()}
|
||||
>
|
||||
<div className="px-4 py-3.5">
|
||||
<div className="flex items-start gap-2">
|
||||
{selectedDistro !== 'nix' && selectedCount > 0 && (
|
||||
<span className="text-xs font-bold shrink-0 mt-0.5 select-none opacity-50" style={{ color: distroColor }}>$</span>
|
||||
)}
|
||||
<code className={`text-[13px] font-mono leading-[1.6] break-all ${selectedCount > 0 ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)] opacity-50'
|
||||
}`}
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{selectedCount > 0 ? command : 'Select apps to generate command...'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
{selectedCount > 0 && (
|
||||
<div className="px-4 py-2 bg-[var(--bg-tertiary)]/50 border-t border-[var(--border-primary)]/15 flex items-center justify-center gap-1.5 text-[10px] text-[var(--text-muted)] group-hover:text-[var(--text-secondary)] transition-colors">
|
||||
<Eye className="w-3 h-3" />
|
||||
<span>Click for full preview</span>
|
||||
<kbd className="ml-1 text-[9px] border border-[var(--border-primary)]/30 rounded px-1 py-px font-mono opacity-50">Tab</kbd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-5 pb-3">
|
||||
<div className="grid grid-cols-2 gap-2.5 mb-2.5">
|
||||
<button
|
||||
data-action="copy"
|
||||
onClick={handleCopy}
|
||||
disabled={selectedCount === 0}
|
||||
className={`sidebar-action-btn flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-semibold transition-all ${selectedCount === 0
|
||||
? 'opacity-30 cursor-not-allowed bg-[var(--bg-tertiary)] text-[var(--text-muted)]'
|
||||
: copied
|
||||
? 'text-white shadow-lg'
|
||||
: activeShortcut === 'y'
|
||||
? 'bg-[var(--bg-hover)] opacity-80 shadow-inner scale-[0.98]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: copied ? distroColor : undefined,
|
||||
}}
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4 opacity-60" />}
|
||||
<span>{copied ? 'Copied!' : 'Copy'}</span>
|
||||
</button>
|
||||
<button
|
||||
data-action="download"
|
||||
onClick={handleDownload}
|
||||
disabled={selectedCount === 0}
|
||||
className={`sidebar-action-btn flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-semibold transition-all ${selectedCount === 0
|
||||
? 'opacity-30 cursor-not-allowed bg-[var(--bg-tertiary)] text-[var(--text-muted)]'
|
||||
: activeShortcut === 'd'
|
||||
? 'bg-[var(--bg-hover)] opacity-80 shadow-inner scale-[0.98]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]'
|
||||
}`}
|
||||
>
|
||||
<Download className="w-4 h-4 opacity-60" />
|
||||
<span>Download</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2.5">
|
||||
<button
|
||||
onClick={() => selectedCount > 0 && onOpenDrawer()}
|
||||
disabled={selectedCount === 0}
|
||||
className={`sidebar-action-btn flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-semibold transition-all ${selectedCount === 0
|
||||
? 'opacity-30 cursor-not-allowed bg-[var(--bg-tertiary)] text-[var(--text-muted)]'
|
||||
: 'text-white shadow-md'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: selectedCount > 0 ? distroColor : undefined,
|
||||
}}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
<span>Preview</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={clearAll}
|
||||
disabled={selectedCount === 0}
|
||||
className={`sidebar-action-btn flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-semibold transition-all ${selectedCount === 0
|
||||
? 'opacity-30 cursor-not-allowed bg-[var(--bg-tertiary)] text-[var(--text-muted)]'
|
||||
: activeShortcut === 'c'
|
||||
? 'bg-red-500/20 text-red-400 scale-[0.98]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:text-red-400 hover:bg-red-500/10'
|
||||
}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 opacity-60" />
|
||||
<span>Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAur && (
|
||||
<>
|
||||
<div className="mx-5 my-3 border-t border-[var(--border-primary)]/25" />
|
||||
<div className="px-5 pb-2">
|
||||
<div className="flex items-center gap-2 mb-2.5 px-1">
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="#1793d1">
|
||||
<path d="M12 0c-.39 0-.77.126-1.11.365a2.22 2.22 0 0 0-.82 1.056L0 24h4.15l2.067-5.58h11.666L19.95 24h4.05L13.91 1.42A2.24 2.24 0 0 0 12 0zm0 4.542l5.77 15.548H6.23l5.77-15.548z" />
|
||||
</svg>
|
||||
<p className="text-[11px] uppercase tracking-[0.15em] text-[var(--text-muted)] font-semibold">AUR Helper</p>
|
||||
<span className="text-[10px] text-[var(--text-muted)] opacity-50">·</span>
|
||||
<span className="text-[10px] text-[var(--text-muted)]">{aurAppNames.length} pkg{aurAppNames.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{(['yay', 'paru'] as const).map((helper) => (
|
||||
<button
|
||||
key={helper}
|
||||
onClick={() => setSelectedHelper(helper)}
|
||||
className={`py-3 px-4 rounded-xl text-sm font-semibold transition-all ${selectedHelper === helper
|
||||
? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]'
|
||||
}`}
|
||||
>
|
||||
{helper}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedDistro === 'nix' && hasUnfreePackages && unfreeAppNames && unfreeAppNames.length > 0 && (
|
||||
<>
|
||||
<div className="mx-5 my-3 border-t border-[var(--border-primary)]/25" />
|
||||
<div className="px-5 pb-2">
|
||||
<div className="p-3.5 rounded-xl bg-amber-500/8 border border-amber-500/20">
|
||||
<p className="text-[11px] font-semibold text-amber-400 mb-1 flex items-center gap-1.5">
|
||||
<span className="text-amber-400">⚠</span> Unfree Packages
|
||||
</p>
|
||||
<p className="text-[11px] text-[var(--text-muted)] leading-relaxed">
|
||||
{unfreeAppNames.join(', ')} require{' '}
|
||||
<code className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[10px] font-mono rounded-md">allowUnfree = true</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="min-h-4" />
|
||||
<div className="px-5 pb-3">
|
||||
<div className="px-4 py-4 rounded-xl bg-[var(--bg-secondary)]/40 border border-[var(--border-primary)]/15">
|
||||
<p className="text-[12px] uppercase tracking-[0.15em] text-[var(--text-secondary)] font-bold mb-3">Keyboard</p>
|
||||
<div className="grid grid-cols-2 gap-y-2.5 gap-x-6 text-[13px]">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Search</span>
|
||||
<kbd className="text-[11px] text-[var(--text-primary)] bg-[var(--bg-secondary)] border border-[var(--border-primary)]/50 rounded-md px-2 py-0.5 font-mono font-medium shadow-sm">/</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Navigate</span>
|
||||
<kbd className="text-[11px] text-[var(--text-primary)] bg-[var(--bg-secondary)] border border-[var(--border-primary)]/50 rounded-md px-2 py-0.5 font-mono font-medium shadow-sm">←→↑↓</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Select</span>
|
||||
<kbd className="text-[11px] text-[var(--text-primary)] bg-[var(--bg-secondary)] border border-[var(--border-primary)]/50 rounded-md px-2 py-0.5 font-mono font-medium shadow-sm">Space</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Copy</span>
|
||||
<kbd className="text-[11px] text-[var(--text-primary)] bg-[var(--bg-secondary)] border border-[var(--border-primary)]/50 rounded-md px-2 py-0.5 font-mono font-medium shadow-sm">y</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Download</span>
|
||||
<kbd className="text-[11px] text-[var(--text-primary)] bg-[var(--bg-secondary)] border border-[var(--border-primary)]/50 rounded-md px-2 py-0.5 font-mono font-medium shadow-sm">d</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Preview</span>
|
||||
<kbd className="text-[11px] text-[var(--text-primary)] bg-[var(--bg-secondary)] border border-[var(--border-primary)]/50 rounded-md px-2 py-0.5 font-mono font-medium shadow-sm">Tab</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Theme</span>
|
||||
<kbd className="text-[11px] text-[var(--text-primary)] bg-[var(--bg-secondary)] border border-[var(--border-primary)]/50 rounded-md px-2 py-0.5 font-mono font-medium shadow-sm">t</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Clear All</span>
|
||||
<kbd className="text-[11px] text-[var(--text-primary)] bg-[var(--bg-secondary)] border border-[var(--border-primary)]/50 rounded-md px-2 py-0.5 font-mono font-medium shadow-sm">c</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Help</span>
|
||||
<kbd className="text-[11px] text-[var(--text-primary)] bg-[var(--bg-secondary)] border border-[var(--border-primary)]/50 rounded-md px-2 py-0.5 font-mono font-medium shadow-sm">?</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[var(--text-secondary)]">Close</span>
|
||||
<kbd className="text-[11px] text-[var(--text-primary)] bg-[var(--bg-secondary)] border border-[var(--border-primary)]/50 rounded-md px-2 py-0.5 font-mono font-medium shadow-sm">Esc</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="px-5 py-4 border-t border-[var(--border-primary)]/15">
|
||||
<div className="flex items-center justify-between">
|
||||
<ThemeToggle />
|
||||
|
||||
{/* Links */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
<a
|
||||
href="https://github.com/abusoww/tuxmate"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 px-2.5 py-2 rounded-xl hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all text-[12px]"
|
||||
title="View on GitHub"
|
||||
onClick={() => analytics.githubClicked()}
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/abusoww/tuxmate/blob/main/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 px-2.5 py-2 rounded-xl hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-rose-400 transition-all text-[12px]"
|
||||
title="Contribute"
|
||||
onClick={() => analytics.contributeClicked()}
|
||||
>
|
||||
<Heart className="w-4 h-4" />
|
||||
</a>
|
||||
<HowItWorks />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
3
src/components/sidebar/index.ts
Normal file
3
src/components/sidebar/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Sidebar components
|
||||
|
||||
export { Sidebar } from './Sidebar';
|
||||
@@ -14,11 +14,6 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
const { theme, toggle } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
/**
|
||||
* Classic hydration mismatch avoidance. Server has no idea what
|
||||
* localStorage says, so we render a placeholder first.
|
||||
* Yes, there's probably a better way. No, I don't want to hear about it.
|
||||
*/
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true)
|
||||
@@ -26,12 +21,11 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
|
||||
const isDark = theme === "dark"
|
||||
|
||||
// Render placeholder with same dimensions during SSR
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-20 h-10 p-1 rounded-md",
|
||||
"flex w-20 h-10 p-1 rounded-full",
|
||||
"bg-[var(--bg-secondary)] border border-[var(--border-primary)]",
|
||||
className
|
||||
)}
|
||||
@@ -42,7 +36,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-20 h-10 p-1 rounded-md cursor-pointer transition-[background-color,box-shadow] duration-300",
|
||||
"flex w-20 h-10 p-1 rounded-full cursor-pointer transition-[background-color,box-shadow] duration-300",
|
||||
"bg-[var(--bg-secondary)] border border-[var(--border-primary)]",
|
||||
className
|
||||
)}
|
||||
@@ -56,7 +50,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-center items-center w-8 h-8 rounded-sm transition-transform duration-300",
|
||||
"flex justify-center items-center w-8 h-8 rounded-full transition-transform duration-300",
|
||||
isDark ? "transform translate-x-0" : "transform translate-x-10",
|
||||
"bg-[var(--bg-tertiary)]"
|
||||
)}
|
||||
@@ -75,7 +69,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-center items-center w-8 h-8 rounded-sm transition-transform duration-300",
|
||||
"flex justify-center items-center w-8 h-8 rounded-full transition-transform duration-300",
|
||||
isDark
|
||||
? "bg-transparent"
|
||||
: "transform -translate-x-10"
|
||||
|
||||
@@ -3,21 +3,19 @@
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { type Category } from '@/lib/data';
|
||||
|
||||
// What we're navigating to
|
||||
export interface NavItem {
|
||||
type: 'category' | 'app';
|
||||
id: string;
|
||||
category: Category;
|
||||
}
|
||||
|
||||
// Where we are in the grid
|
||||
export interface FocusPosition {
|
||||
col: number;
|
||||
row: number;
|
||||
}
|
||||
|
||||
|
||||
// Vim-style keyboard navigation. Because real devs don't use mice.
|
||||
// Keyboard navigation hook.
|
||||
export function useKeyboardNavigation(
|
||||
navItems: NavItem[][],
|
||||
onToggleCategory: (id: string) => void,
|
||||
@@ -25,29 +23,24 @@ export function useKeyboardNavigation(
|
||||
) {
|
||||
const [focusPos, setFocusPos] = useState<FocusPosition | null>(null);
|
||||
|
||||
// Track if focus was set via keyboard (to enable scroll) vs mouse (no scroll)
|
||||
const fromKeyboard = useRef(false);
|
||||
|
||||
// Track if focus mode is keyboard (for UI highlighting)
|
||||
const [isKeyboardNavigating, setIsKeyboardNavigating] = useState(false);
|
||||
|
||||
/** Clear focus (e.g., when clicking outside) */
|
||||
const clearFocus = useCallback(() => setFocusPos(null), []);
|
||||
|
||||
/** Get the currently focused item */
|
||||
const focusedItem = useMemo(() => {
|
||||
if (!focusPos) return null;
|
||||
return navItems[focusPos.col]?.[focusPos.row] || null;
|
||||
}, [navItems, focusPos]);
|
||||
|
||||
/** Set focus position by item type and id (from mouse - no scroll) */
|
||||
const setFocusByItem = useCallback((type: 'category' | 'app', id: string) => {
|
||||
for (let col = 0; col < navItems.length; col++) {
|
||||
const colItems = navItems[col];
|
||||
for (let row = 0; row < colItems.length; row++) {
|
||||
if (colItems[row].type === type && colItems[row].id === id) {
|
||||
fromKeyboard.current = false; // Mouse selection - don't scroll
|
||||
setIsKeyboardNavigating(false); // Disable focus ring
|
||||
fromKeyboard.current = false;
|
||||
setIsKeyboardNavigating(false);
|
||||
setFocusPos({ col, row });
|
||||
return;
|
||||
}
|
||||
@@ -55,18 +48,14 @@ export function useKeyboardNavigation(
|
||||
}
|
||||
}, [navItems]);
|
||||
|
||||
/** Keyboard event handler */
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Skip if typing in input
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||||
|
||||
// Skip if modifier keys are pressed (prevents conflicts with browser shortcuts like Ctrl+D)
|
||||
if (e.ctrlKey || e.altKey || e.metaKey) return;
|
||||
|
||||
const key = e.key;
|
||||
|
||||
// Space to toggle
|
||||
if (key === ' ') {
|
||||
e.preventDefault();
|
||||
if (focusPos) {
|
||||
@@ -77,21 +66,17 @@ export function useKeyboardNavigation(
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigation keys (arrow keys + vim keys)
|
||||
if (!['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'j', 'k', 'h', 'l', 'Escape'].includes(key)) return;
|
||||
e.preventDefault();
|
||||
|
||||
// Escape clears focus
|
||||
if (key === 'Escape') {
|
||||
setFocusPos(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as keyboard navigation - will trigger scroll and focus ring
|
||||
fromKeyboard.current = true;
|
||||
setIsKeyboardNavigating(true);
|
||||
|
||||
// Navigate
|
||||
setFocusPos(prev => {
|
||||
if (!prev) return { col: 0, row: 0 };
|
||||
|
||||
@@ -129,14 +114,12 @@ export function useKeyboardNavigation(
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [navItems, focusPos, onToggleCategory, onToggleApp]);
|
||||
|
||||
/* Scroll focused item into view - only when navigating via keyboard */
|
||||
useEffect(() => {
|
||||
if (!focusPos || !fromKeyboard.current) return;
|
||||
|
||||
const item = navItems[focusPos.col]?.[focusPos.row];
|
||||
if (!item) return;
|
||||
|
||||
// Find visible element among duplicates (mobile/desktop layouts both render same data-nav-id)
|
||||
const elements = document.querySelectorAll<HTMLElement>(
|
||||
`[data-nav-id="${item.type}:${item.id}"]`
|
||||
);
|
||||
|
||||
@@ -5,11 +5,7 @@ import { distros, apps, type DistroId } from '@/lib/data';
|
||||
import { isAurPackage } from '@/lib/aur';
|
||||
import { isUnfreePackage } from '@/lib/nixUnfree';
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export { isAurPackage, AUR_PATTERNS, KNOWN_AUR_PACKAGES } from '@/lib/aur';
|
||||
|
||||
// Everything the app needs to work
|
||||
|
||||
export interface UseLinuxInitReturn {
|
||||
selectedDistro: DistroId;
|
||||
selectedApps: Set<string>;
|
||||
@@ -22,7 +18,6 @@ export interface UseLinuxInitReturn {
|
||||
generatedCommand: string;
|
||||
selectedCount: number;
|
||||
availableCount: number;
|
||||
// Arch/AUR specific
|
||||
hasYayInstalled: boolean;
|
||||
setHasYayInstalled: (value: boolean) => void;
|
||||
selectedHelper: 'yay' | 'paru';
|
||||
@@ -30,10 +25,8 @@ export interface UseLinuxInitReturn {
|
||||
hasAurPackages: boolean;
|
||||
aurPackageNames: string[];
|
||||
aurAppNames: string[];
|
||||
// Nix unfree specific
|
||||
hasUnfreePackages: boolean;
|
||||
unfreeAppNames: string[];
|
||||
// Hydration state
|
||||
isHydrated: boolean;
|
||||
}
|
||||
|
||||
@@ -49,7 +42,6 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
const [selectedHelper, setSelectedHelper] = useState<'yay' | 'paru'>('yay');
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
|
||||
// Load saved preferences from localStorage
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedDistro = localStorage.getItem(STORAGE_KEY_DISTRO) as DistroId | null;
|
||||
@@ -64,7 +56,6 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
|
||||
if (savedApps) {
|
||||
const appIds = JSON.parse(savedApps) as string[];
|
||||
// Filter to only valid app IDs that are available on the distro
|
||||
const validApps = appIds.filter(id => {
|
||||
const app = apps.find(a => a.id === id);
|
||||
if (!app) return false;
|
||||
@@ -82,12 +73,10 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
setSelectedHelper('paru');
|
||||
}
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
// Save to localStorage whenever state changes (but not on first render)
|
||||
useEffect(() => {
|
||||
if (!hydrated) return;
|
||||
try {
|
||||
@@ -96,11 +85,9 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
localStorage.setItem(STORAGE_KEY_YAY, hasYayInstalled.toString());
|
||||
localStorage.setItem(STORAGE_KEY_HELPER, selectedHelper);
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
}, [selectedDistro, selectedApps, hasYayInstalled, selectedHelper, hydrated]);
|
||||
|
||||
// Compute AUR package info for Arch
|
||||
const aurPackageInfo = useMemo(() => {
|
||||
if (selectedDistro !== 'arch') {
|
||||
return { hasAur: false, packages: [] as string[], appNames: [] as string[] };
|
||||
@@ -122,7 +109,6 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
return { hasAur: aurPkgs.length > 0, packages: aurPkgs, appNames: aurAppNames };
|
||||
}, [selectedDistro, selectedApps]);
|
||||
|
||||
// Compute unfree package info for Nix
|
||||
const unfreePackageInfo = useMemo(() => {
|
||||
if (selectedDistro !== 'nix') {
|
||||
return { hasUnfree: false, appNames: [] as string[] };
|
||||
@@ -173,7 +159,6 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
}, []);
|
||||
|
||||
const toggleApp = useCallback((appId: string) => {
|
||||
// Check availability inline to avoid stale closure
|
||||
const app = apps.find(a => a.id === appId);
|
||||
if (!app) return;
|
||||
const pkg = app.targets[selectedDistro];
|
||||
@@ -230,7 +215,6 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
|
||||
if (packageNames.length === 0) return '# No packages selected';
|
||||
|
||||
// Nix: show declarative config (no unfree warning in preview - that's in download)
|
||||
if (selectedDistro === 'nix') {
|
||||
const sortedPkgs = packageNames.filter(p => p.trim()).sort();
|
||||
const pkgList = sortedPkgs.map(p => ` ${p}`).join('\n');
|
||||
@@ -238,36 +222,26 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
}
|
||||
|
||||
if (selectedDistro === 'snap') {
|
||||
// Snap needs separate commands for --classic packages
|
||||
if (packageNames.length === 1) {
|
||||
return `${distro.installPrefix} ${packageNames[0]}`;
|
||||
}
|
||||
// For multiple snap packages, we chain them with &&
|
||||
// Note: snap doesn't support installing multiple packages in one command like apt
|
||||
return packageNames.map(p => `sudo snap install ${p}`).join(' && ');
|
||||
}
|
||||
|
||||
// Arch with AUR packages - this is where it gets fun
|
||||
if (selectedDistro === 'arch' && aurPackageInfo.hasAur) {
|
||||
if (!hasYayInstalled) {
|
||||
// User doesn't have current helper installed - prepend installation
|
||||
const helperName = selectedHelper; // yay or paru
|
||||
|
||||
// Common setup: sudo pacman -S --needed git base-devel
|
||||
// Then clone, make, install
|
||||
const installHelperCmd = `sudo pacman -S --needed git base-devel && git clone https://aur.archlinux.org/${helperName}.git /tmp/${helperName} && cd /tmp/${helperName} && makepkg -si --noconfirm && cd - && rm -rf /tmp/${helperName}`;
|
||||
|
||||
// Install packages using the helper
|
||||
const installCmd = `${helperName} -S --needed --noconfirm ${packageNames.join(' ')}`;
|
||||
|
||||
return `${installHelperCmd} && ${installCmd}`;
|
||||
} else {
|
||||
// User has helper installed - use it for ALL packages
|
||||
return `${selectedHelper} -S --needed --noconfirm ${packageNames.join(' ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Homebrew: separate formulae and casks into separate commands
|
||||
if (selectedDistro === 'homebrew') {
|
||||
const formulae = packageNames.filter(p => !p.startsWith('--cask '));
|
||||
const casks = packageNames.filter(p => p.startsWith('--cask ')).map(p => p.replace('--cask ', ''));
|
||||
@@ -296,7 +270,6 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
generatedCommand,
|
||||
selectedCount: selectedApps.size,
|
||||
availableCount,
|
||||
// Arch/AUR specific
|
||||
hasYayInstalled,
|
||||
setHasYayInstalled,
|
||||
selectedHelper,
|
||||
@@ -304,10 +277,8 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
hasAurPackages: aurPackageInfo.hasAur,
|
||||
aurPackageNames: aurPackageInfo.packages,
|
||||
aurAppNames: aurPackageInfo.appNames,
|
||||
// Nix unfree specific
|
||||
hasUnfreePackages: unfreePackageInfo.hasUnfree,
|
||||
unfreeAppNames: unfreePackageInfo.appNames,
|
||||
// Hydration state
|
||||
isHydrated: hydrated,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,17 +11,13 @@ interface ThemeContextType {
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* Theme provider that syncs with localStorage and system preferences.
|
||||
* Also handles the initial hydration dance to avoid theme flash.
|
||||
*/
|
||||
// Theme provider.
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
// Initial state reads from DOM to match what the inline script set
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return document.documentElement.classList.contains('light') ? 'light' : 'dark'
|
||||
}
|
||||
return 'light' // SSR default
|
||||
return 'light'
|
||||
})
|
||||
const [hydrated, setHydrated] = useState(false)
|
||||
|
||||
|
||||
@@ -8,12 +8,7 @@ export interface TooltipState {
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tooltip that stays open while hovering trigger or tooltip.
|
||||
* - 450ms delay before showing
|
||||
* - Stays open once shown (until mouse leaves both trigger and tooltip)
|
||||
* - Dismiss on click/scroll/escape
|
||||
*/
|
||||
// Tooltip hook.
|
||||
export function useTooltip() {
|
||||
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
||||
const showTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -22,7 +17,6 @@ export function useTooltip() {
|
||||
const isOverTooltip = useRef(false);
|
||||
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// Allow setting the tooltip element ref from the Tooltip component
|
||||
const setTooltipRef = useCallback((el: HTMLDivElement | null) => {
|
||||
tooltipRef.current = el;
|
||||
}, []);
|
||||
@@ -40,7 +34,6 @@ export function useTooltip() {
|
||||
|
||||
const tryHide = useCallback(() => {
|
||||
cancel();
|
||||
// Only hide if mouse is not over trigger or tooltip
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
if (!isOverTrigger.current && !isOverTooltip.current) {
|
||||
setTooltip(null);
|
||||
@@ -54,13 +47,15 @@ export function useTooltip() {
|
||||
cancel();
|
||||
|
||||
const rect = target.getBoundingClientRect();
|
||||
const clientX = e.clientX; // Capture mouse X position
|
||||
const clientX = e.clientX;
|
||||
|
||||
const zoom = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
|
||||
showTimeout.current = setTimeout(() => {
|
||||
setTooltip({
|
||||
content,
|
||||
x: clientX, // Use mouse X instead of element center
|
||||
y: rect.top,
|
||||
x: clientX / zoom,
|
||||
y: rect.top / zoom,
|
||||
});
|
||||
}, 450);
|
||||
}, [cancel]);
|
||||
@@ -82,7 +77,6 @@ export function useTooltip() {
|
||||
|
||||
useEffect(() => {
|
||||
const dismiss = (e: MouseEvent) => {
|
||||
// Don't dismiss if clicking inside the tooltip
|
||||
if (tooltipRef.current && tooltipRef.current.contains(e.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,16 +8,13 @@ import {
|
||||
} from '@/lib/verification';
|
||||
|
||||
export interface UseVerificationResult {
|
||||
// Kept for compatibility, always false now
|
||||
isLoading: boolean;
|
||||
hasError: boolean;
|
||||
isVerified: (distro: DistroId, packageName: string) => boolean;
|
||||
getVerificationSource: (distro: DistroId, packageName: string) => 'flathub' | 'snap' | null;
|
||||
}
|
||||
|
||||
// Now purely synchronous using build-time generated data
|
||||
export function useVerification(): UseVerificationResult {
|
||||
// Check if package is verified for the distro
|
||||
const isVerified = useCallback((distro: DistroId, packageName: string): boolean => {
|
||||
if (distro === 'flatpak') {
|
||||
return isFlathubVerified(packageName);
|
||||
@@ -28,7 +25,6 @@ export function useVerification(): UseVerificationResult {
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
// Get verification source for badge styling
|
||||
const getVerificationSource = useCallback((distro: DistroId, packageName: string): 'flathub' | 'snap' | null => {
|
||||
if (distro === 'flatpak' && isFlathubVerified(packageName)) {
|
||||
return 'flathub';
|
||||
|
||||
@@ -1,66 +1,137 @@
|
||||
{
|
||||
"fetchedAt": "2026-01-25T17:01:05.599Z",
|
||||
"count": 303,
|
||||
"meta": {
|
||||
"fetchedAt": "2026-02-22T00:43:13.504Z"
|
||||
},
|
||||
"count": 697,
|
||||
"apps": [
|
||||
"ai.jan.Jan",
|
||||
"app.bluebubbles.BlueBubbles",
|
||||
"app.comaps.comaps",
|
||||
"app.devsuite.Ptyxis",
|
||||
"app.drey.Dialect",
|
||||
"app.drey.EarTag",
|
||||
"app.drey.Warp",
|
||||
"app.eduroam.geteduroam",
|
||||
"app.fotema.Fotema",
|
||||
"app.grayjay.Grayjay",
|
||||
"app.opencomic.OpenComic",
|
||||
"app.organicmaps.desktop",
|
||||
"app.polychromatic.controller",
|
||||
"app.tintero.Tintero",
|
||||
"app.twintaillauncher.ttl",
|
||||
"app.xemu.xemu",
|
||||
"app.zen_browser.zen",
|
||||
"at.vintagestory.VintageStory",
|
||||
"be.alexandervanhee.gradia",
|
||||
"best.ellie.StartupConfiguration",
|
||||
"br.com.wiselabs.simplexity",
|
||||
"br.eng.silas.qpdftools",
|
||||
"ca.desrt.dconf-editor",
|
||||
"ca.edestcroix.Recordbox",
|
||||
"ca.parallel_launcher.ParallelLauncher",
|
||||
"cafe.avery.Delfin",
|
||||
"ch.tlaun.TL",
|
||||
"chat.delta.desktop",
|
||||
"chat.simplex.simplex",
|
||||
"cn.xfangfang.wiliwili",
|
||||
"com.abisource.AbiWord",
|
||||
"com.actualbudget.actual",
|
||||
"com.adamcake.Bolt",
|
||||
"com.adilhanney.saber",
|
||||
"com.atlauncher.ATLauncher",
|
||||
"com.bambulab.BambuStudio",
|
||||
"com.beavernotes.beavernotes",
|
||||
"com.belmoussaoui.Authenticator",
|
||||
"com.belmoussaoui.Decoder",
|
||||
"com.belmoussaoui.Obfuscate",
|
||||
"com.bilingify.readest",
|
||||
"com.bitwarden.desktop",
|
||||
"com.bitwig.BitwigStudio",
|
||||
"com.blitterstudio.amiberry",
|
||||
"com.borgbase.Vorta",
|
||||
"com.boxy_svg.BoxySVG",
|
||||
"com.brave.Browser",
|
||||
"com.cassidyjames.butler",
|
||||
"com.chatterino.chatterino",
|
||||
"com.collaboraoffice.Office",
|
||||
"com.core447.StreamController",
|
||||
"com.daniel15.wcc",
|
||||
"com.dec05eba.gpu_screen_recorder",
|
||||
"com.devolutions.remotedesktopmanager",
|
||||
"com.discordapp.Discord",
|
||||
"com.endlessm.photos",
|
||||
"com.expidusos.file_manager",
|
||||
"com.fastmail.Fastmail",
|
||||
"com.feaneron.Boatswain",
|
||||
"com.freerdp.FreeRDP",
|
||||
"com.geeks3d.furmark",
|
||||
"com.github.ADBeveridge.Raider",
|
||||
"com.github.Darazaki.Spedread",
|
||||
"com.github.IsmaelMartinez.teams_for_linux",
|
||||
"com.github.KRTirtho.Spotube",
|
||||
"com.github.Matoking.protontricks",
|
||||
"com.github.Murmele.Gittyup",
|
||||
"com.github.PintaProject.Pinta",
|
||||
"com.github.Rosalie241.RMG",
|
||||
"com.github.d4nj1.tlpui",
|
||||
"com.github.dail8859.NotepadNext",
|
||||
"com.github.dynobo.normcap",
|
||||
"com.github.finefindus.eyedropper",
|
||||
"com.github.flxzt.rnote",
|
||||
"com.github.gabutakut.gabutdm",
|
||||
"com.github.geigi.cozy",
|
||||
"com.github.hluk.copyq",
|
||||
"com.github.hugolabe.Wike",
|
||||
"com.github.huluti.Curtail",
|
||||
"com.github.iwalton3.jellyfin-media-player",
|
||||
"com.github.iwalton3.jellyfin-mpv-shim",
|
||||
"com.github.jeromerobert.pdfarranger",
|
||||
"com.github.jkotra.eovpn",
|
||||
"com.github.johnfactotum.Foliate",
|
||||
"com.github.joseexposito.touche",
|
||||
"com.github.louis77.tuner",
|
||||
"com.github.maoschanz.drawing",
|
||||
"com.github.marhkb.Pods",
|
||||
"com.github.mtkennerly.ludusavi",
|
||||
"com.github.neithern.g4music",
|
||||
"com.github.phase1geo.minder",
|
||||
"com.github.polymeilex.neothesia",
|
||||
"com.github.qarmin.czkawka",
|
||||
"com.github.rafostar.Clapper",
|
||||
"com.github.ryonakano.pinit",
|
||||
"com.github.ryonakano.reco",
|
||||
"com.github.sdv43.whaler",
|
||||
"com.github.skylot.jadx",
|
||||
"com.github.taiko2k.tauonmb",
|
||||
"com.github.tchx84.Flatseal",
|
||||
"com.github.tenderowl.frog",
|
||||
"com.github.unrud.VideoDownloader",
|
||||
"com.github.unrud.djpdf",
|
||||
"com.github.vikdevelop.photopea_app",
|
||||
"com.github.vikdevelop.timer",
|
||||
"com.github.vkohaupt.vokoscreenNG",
|
||||
"com.github.vladimiry.ElectronMail",
|
||||
"com.github.wwmm.easyeffects",
|
||||
"com.github.wwmm.pulseeffects",
|
||||
"com.github.xournalpp.xournalpp",
|
||||
"com.github.zocker_160.SyncThingy",
|
||||
"com.github.ztefn.haguichi",
|
||||
"com.gitlab.bitseater.meteo",
|
||||
"com.gopeed.Gopeed",
|
||||
"com.heroicgameslauncher.hgl",
|
||||
"com.icons8.Lunacy",
|
||||
"com.indomitusgroup.indipdf",
|
||||
"com.infinipaint.infinipaint",
|
||||
"com.interversehq.qView",
|
||||
"com.jeffser.Alpaca",
|
||||
"com.jwestall.Forecast",
|
||||
"com.kgurgul.cpuinfo",
|
||||
"com.ktechpit.colorwall",
|
||||
"com.ktechpit.orion",
|
||||
"com.ktechpit.torrhunt",
|
||||
"com.ktechpit.ultimate-media-downloader",
|
||||
"com.ktechpit.whatsie",
|
||||
"com.ktechpit.wonderwall",
|
||||
"com.logseq.Logseq",
|
||||
"com.lunarclient.LunarClient",
|
||||
"com.markopejic.downloader",
|
||||
@@ -69,142 +140,355 @@
|
||||
"com.ml4w.dotfilesinstaller",
|
||||
"com.modrinth.ModrinthApp",
|
||||
"com.moonlight_stream.Moonlight",
|
||||
"com.neatdecisions.Detwinner",
|
||||
"com.notesnook.Notesnook",
|
||||
"com.ntrack.n-track",
|
||||
"com.obsproject.Studio",
|
||||
"com.odnoyko.valot",
|
||||
"com.pikatorrent.PikaTorrent",
|
||||
"com.play0ad.zeroad",
|
||||
"com.plexamp.Plexamp",
|
||||
"com.pojtinger.felicitas.Sessions",
|
||||
"com.pokemmo.PokeMMO",
|
||||
"com.prusa3d.PrusaSlicer",
|
||||
"com.rafaelmardojai.Blanket",
|
||||
"com.raggesilver.BlackBox",
|
||||
"com.ranfdev.DistroShelf",
|
||||
"com.rcloneui.RcloneUI",
|
||||
"com.realm667.Wolfenstein_Blade_of_Agony",
|
||||
"com.redis.RedisInsight",
|
||||
"com.rtosta.zapzap",
|
||||
"com.rustdesk.RustDesk",
|
||||
"com.saivert.pwvucontrol",
|
||||
"com.sidevesh.Luminance",
|
||||
"com.steamgriddb.SGDBoop",
|
||||
"com.steamgriddb.steam-rom-manager",
|
||||
"com.stremio.Service",
|
||||
"com.stremio.Stremio",
|
||||
"com.super_productivity.SuperProductivity",
|
||||
"com.surfshark.Surfshark",
|
||||
"com.thincast.client",
|
||||
"com.tomjwatson.Emote",
|
||||
"com.toolstack.Folio",
|
||||
"com.ulaa.Ulaa",
|
||||
"com.unicornsonlsd.finamp",
|
||||
"com.usebottles.bottles",
|
||||
"com.usebruno.Bruno",
|
||||
"com.valvesoftware.SteamLink",
|
||||
"com.vixalien.sticky",
|
||||
"com.voxdsp.TuxFishing",
|
||||
"com.vscodium.codium",
|
||||
"com.vscodium.codium-insiders",
|
||||
"com.vysp3r.ProtonPlus",
|
||||
"com.warlordsoftwares.formatlab",
|
||||
"com.warlordsoftwares.media-downloader",
|
||||
"com.warlordsoftwares.tube2go",
|
||||
"com.warlordsoftwares.youtube-downloader-4ktube",
|
||||
"com.zettlr.Zettlr",
|
||||
"de.capypara.FieldMonitor",
|
||||
"de.haeckerfelix.AudioSharing",
|
||||
"de.haeckerfelix.Fragments",
|
||||
"de.haeckerfelix.Shortwave",
|
||||
"de.k_bo.Televido",
|
||||
"de.leopoldluley.Clapgrep",
|
||||
"de.mediathekview.MediathekView",
|
||||
"de.schmidhuberj.DieBahn",
|
||||
"de.schmidhuberj.tubefeeder",
|
||||
"de.z_ray.OptimusUI",
|
||||
"dev.bragefuglseth.Fretboard",
|
||||
"dev.bragefuglseth.Keypunch",
|
||||
"dev.deedles.Trayscale",
|
||||
"dev.dergs.Tonearm",
|
||||
"dev.diegovsky.Riff",
|
||||
"dev.edfloreshz.Calculator",
|
||||
"dev.edfloreshz.CosmicTweaks",
|
||||
"dev.edfloreshz.Tasks",
|
||||
"dev.fredol.open-tv",
|
||||
"dev.ftb.ftb-app",
|
||||
"dev.geopjr.Collision",
|
||||
"dev.geopjr.Tuba",
|
||||
"dev.goats.xivlauncher",
|
||||
"dev.heppen.webapps",
|
||||
"dev.ibrahimcetin.reins",
|
||||
"dev.lapce.lapce",
|
||||
"dev.lasheen.qr",
|
||||
"dev.lizardbyte.app.Sunshine",
|
||||
"dev.mufeed.Wordbook",
|
||||
"dev.qwery.AddWater",
|
||||
"dev.ters.LocalTranslate",
|
||||
"dev.vencord.Vesktop",
|
||||
"dk.gqrx.gqrx",
|
||||
"eu.betterbird.Betterbird",
|
||||
"eu.ithz.umftpd",
|
||||
"eu.jumplink.Learn6502",
|
||||
"eu.nokun.MirrorHall",
|
||||
"fr.handbrake.ghb",
|
||||
"garden.jamie.Morphosis",
|
||||
"gg.minion.Minion",
|
||||
"gg.norisk.NoRiskClientLauncherV3",
|
||||
"hu.irl.cameractrls",
|
||||
"im.bernard.Nostalgia",
|
||||
"im.dino.Dino",
|
||||
"im.fluffychat.Fluffychat",
|
||||
"im.nheko.Nheko",
|
||||
"in.cinny.Cinny",
|
||||
"info.beyondallreason.bar",
|
||||
"info.bibletime.BibleTime",
|
||||
"info.cemu.Cemu",
|
||||
"info.febvre.Komikku",
|
||||
"info.mumble.Mumble",
|
||||
"info.portfolio_performance.PortfolioPerformance",
|
||||
"info.smplayer.SMPlayer",
|
||||
"ink.whis.Whis",
|
||||
"io.anytype.anytype",
|
||||
"io.appflowy.AppFlowy",
|
||||
"io.bassi.Amberol",
|
||||
"io.emeric.toolblex",
|
||||
"io.ente.auth",
|
||||
"io.ente.photos",
|
||||
"io.frama.tractor.carburetor",
|
||||
"io.freetubeapp.FreeTube",
|
||||
"io.gdevelop.ide",
|
||||
"io.github.BrisklyDev.Brisk",
|
||||
"io.github.CyberTimon.RapidRAW",
|
||||
"io.github.DenysMb.Kontainer",
|
||||
"io.github.Ethanscharlie.albumripper",
|
||||
"io.github.Faugus.faugus-launcher",
|
||||
"io.github.Foldex.AdwSteamGtk",
|
||||
"io.github.Geocld.XStreamingDesktop",
|
||||
"io.github.IshuSinghSE.aurynk",
|
||||
"io.github.JakubMelka.Pdf4qt",
|
||||
"io.github.N3kosempai.hetairos-ai",
|
||||
"io.github.N3kosempai.klia-store",
|
||||
"io.github.Predidit.Kazumi",
|
||||
"io.github.Qalculate",
|
||||
"io.github.Qalculate.qalculate-qt",
|
||||
"io.github.Soundux",
|
||||
"io.github.TheWisker.Cavasik",
|
||||
"io.github.aandrew_me.ytdn",
|
||||
"io.github.alainm23.planify",
|
||||
"io.github.alescdb.mailviewer",
|
||||
"io.github.alfianlosari.GTKChatGPT",
|
||||
"io.github.amit9838.mousam",
|
||||
"io.github.antimicrox.antimicrox",
|
||||
"io.github.arunsivaramanneo.GPUViewer",
|
||||
"io.github.astralvixen.geforce-infinity",
|
||||
"io.github.benjamimgois.goverlay",
|
||||
"io.github.brunofin.Cohesion",
|
||||
"io.github.bytezz.IPLookup",
|
||||
"io.github.cboxdoerfer.FSearch",
|
||||
"io.github.celluloid_player.Celluloid",
|
||||
"io.github.cosmic_utils.Examine",
|
||||
"io.github.cosmic_utils.camera",
|
||||
"io.github.davidoc26.wallpaper_selector",
|
||||
"io.github.debasish_patra_1987.linuxthemestore",
|
||||
"io.github.diegopvlk.Cine",
|
||||
"io.github.diegopvlk.Tomatillo",
|
||||
"io.github.dimtpap.coppwr",
|
||||
"io.github.dosbox-staging",
|
||||
"io.github.dubstar_04.design",
|
||||
"io.github.dvlv.boxbuddyrs",
|
||||
"io.github.dyegoaurelio.simple-wireplumber-gui",
|
||||
"io.github.ebonjaeger.bluejay",
|
||||
"io.github.ecotubehq.player",
|
||||
"io.github.efogdev.mpris-timer",
|
||||
"io.github.endless_sky.endless_sky",
|
||||
"io.github.fabrialberio.pinapp",
|
||||
"io.github.fastrizwaan.WineCharm",
|
||||
"io.github.fastrizwaan.WineZGUI",
|
||||
"io.github.ferraridamiano.ConverterNOW",
|
||||
"io.github.flattool.Ignition",
|
||||
"io.github.flattool.Warehouse",
|
||||
"io.github.gaheldev.Millisecond",
|
||||
"io.github.gamingdoom.Datcord",
|
||||
"io.github.getnf.embellish",
|
||||
"io.github.giantpinkrobots.bootqt",
|
||||
"io.github.giantpinkrobots.flatsweep",
|
||||
"io.github.giantpinkrobots.varia",
|
||||
"io.github.gopher64.gopher64",
|
||||
"io.github.hedge_dev.hedgemodmanager",
|
||||
"io.github.hkdb.Aerion",
|
||||
"io.github.htkhiem.Euphonica",
|
||||
"io.github.ilya_zlobintsev.LACT",
|
||||
"io.github.jeffshee.Hidamari",
|
||||
"io.github.jliljebl.Flowblade",
|
||||
"io.github.jonmagon.kdiskmark",
|
||||
"io.github.jorchube.monitorets",
|
||||
"io.github.josephmawa.Gauge",
|
||||
"io.github.kolunmi.Bazaar",
|
||||
"io.github.kukuruzka165.materialgram",
|
||||
"io.github.ladaapp.lada",
|
||||
"io.github.lainsce.Notejot",
|
||||
"io.github.lawstorant.boxflat",
|
||||
"io.github.libvibrant.vibrantLinux",
|
||||
"io.github.limo_app.limo",
|
||||
"io.github.linx_systems.ClamUI",
|
||||
"io.github.mandruis7.xbox-cloud-gaming-electron",
|
||||
"io.github.marco_calautti.DeltaPatcher",
|
||||
"io.github.martchus.syncthingtray",
|
||||
"io.github.martinrotter.rssguard",
|
||||
"io.github.mfat.sshpilot",
|
||||
"io.github.mfat.tvhplayer",
|
||||
"io.github.mhogomchungu.media-downloader",
|
||||
"io.github.milkshiift.GoofCord",
|
||||
"io.github.mpc_qt.mpc-qt",
|
||||
"io.github.mpobaschnig.Vaults",
|
||||
"io.github.mrvladus.List",
|
||||
"io.github.nacho.mecalin",
|
||||
"io.github.nokse22.Exhibit",
|
||||
"io.github.nokse22.asciidraw",
|
||||
"io.github.nokse22.high-tide",
|
||||
"io.github.nokse22.inspector",
|
||||
"io.github.nozwock.Packet",
|
||||
"io.github.nroduit.Weasis",
|
||||
"io.github.nuttyartist.notes",
|
||||
"io.github.olaproeis.Ferrite",
|
||||
"io.github.onionware_github.onionmedia",
|
||||
"io.github.pantheon_tweaks.pantheon-tweaks",
|
||||
"io.github.peazip.PeaZip",
|
||||
"io.github.pieterdd.RcloneShuttle",
|
||||
"io.github.plrigaux.sysd-manager",
|
||||
"io.github.pol_rivero.github-desktop-plus",
|
||||
"io.github.prateekmedia.appimagepool",
|
||||
"io.github.prateekmedia.pstube",
|
||||
"io.github.quodlibet.QuodLibet",
|
||||
"io.github.qwersyk.Newelle",
|
||||
"io.github.radiolamp.mangojuice",
|
||||
"io.github.realmazharhussain.GdmSettings",
|
||||
"io.github.revisto.drum-machine",
|
||||
"io.github.rfrench3.scopebuddy-gui",
|
||||
"io.github.schwarzen.colormydesktop",
|
||||
"io.github.seadve.Kooha",
|
||||
"io.github.seadve.Mousai",
|
||||
"io.github.sepehr_rs.Sudoku",
|
||||
"io.github.sharkwouter.Minigalaxy",
|
||||
"io.github.shiiion.primehack",
|
||||
"io.github.shonebinu.Brief",
|
||||
"io.github.shonubot.Spruce",
|
||||
"io.github.sigmasd.stimulator",
|
||||
"io.github.sitraorg.sitra",
|
||||
"io.github.streetpea.Chiaki4deck",
|
||||
"io.github.swordpuffin.rewaita",
|
||||
"io.github.swordpuffin.wardrobe",
|
||||
"io.github.teacond.Morse",
|
||||
"io.github.thetumultuousunicornofdarkness.cpu-x",
|
||||
"io.github.tntwise.REAL-Video-Enhancer",
|
||||
"io.github.tobagin.karere",
|
||||
"io.github.tobagin.scramble",
|
||||
"io.github.totoshko88.RustConn",
|
||||
"io.github.troyeguo.koodo-reader",
|
||||
"io.github.ungoogled_software.ungoogled_chromium",
|
||||
"io.github.unknownskl.greenlight",
|
||||
"io.github.v81d.Wattage",
|
||||
"io.github.vikdevelop.SaveDesktop",
|
||||
"io.github.vmkspv.lenspect",
|
||||
"io.github.wartybix.Constrict",
|
||||
"io.github.wiiznokes.fan-control",
|
||||
"io.github.wivrn.wivrn",
|
||||
"io.github.xyproto.zsnes",
|
||||
"io.github.yairm210.unciv",
|
||||
"io.github.zaedus.spider",
|
||||
"io.github.zarestia_dev.rclone-manager",
|
||||
"io.github.zingytomato.netpeek",
|
||||
"io.gitlab.Goodvibes",
|
||||
"io.gitlab.adhami3310.Converter",
|
||||
"io.gitlab.adhami3310.Footage",
|
||||
"io.gitlab.adhami3310.Impression",
|
||||
"io.gitlab.librewolf-community",
|
||||
"io.gitlab.news_flash.NewsFlash",
|
||||
"io.gitlab.theevilskeleton.Upscaler",
|
||||
"io.kapsa.drive",
|
||||
"io.kinvolk.Headlamp",
|
||||
"io.m51.Gelly",
|
||||
"io.missioncenter.MissionCenter",
|
||||
"io.openrct2.OpenRCT2",
|
||||
"io.podman_desktop.PodmanDesktop",
|
||||
"io.qt.QtCreator",
|
||||
"io.sourceforge.pysolfc.PySolFC",
|
||||
"it.dottorblaster.cauldron",
|
||||
"it.fabiodistasio.AntaresSQL",
|
||||
"it.mijorus.gearlever",
|
||||
"it.mijorus.smile",
|
||||
"it.mijorus.whisper",
|
||||
"it.mq1.TinyWiiBackupManager",
|
||||
"jp.nonbili.noutube",
|
||||
"md.obsidian.Obsidian",
|
||||
"me.ahola.aphototoollibre",
|
||||
"me.amankhanna.opendeck",
|
||||
"me.iepure.devtoolbox",
|
||||
"me.timschneeberger.GalaxyBudsClient",
|
||||
"me.timschneeberger.jdsp4linux",
|
||||
"moe.launcher.an-anime-game-launcher",
|
||||
"moe.launcher.sleepy-launcher",
|
||||
"moe.launcher.the-honkers-railway-launcher",
|
||||
"net.codelogistics.webapps",
|
||||
"net.davidotek.pupgui2",
|
||||
"net.fasterland.converseen",
|
||||
"net.fhannenheim.musicfetch",
|
||||
"net.giuspen.cherrytree",
|
||||
"net.jami.Jami",
|
||||
"net.kuribo64.melonDS",
|
||||
"net.lutris.Lutris",
|
||||
"net.mkiol.Jupii",
|
||||
"net.mkiol.SpeechNote",
|
||||
"net.nokyan.Resources",
|
||||
"net.nymtech.NymVPN",
|
||||
"net.openra.OpenRA",
|
||||
"net.pcsx2.PCSX2",
|
||||
"net.retrodeck.retrodeck",
|
||||
"net.runelite.RuneLite",
|
||||
"net.sapples.LiveCaptions",
|
||||
"net.shadps4.shadPS4",
|
||||
"net.sourceforge.VMPK",
|
||||
"net.sourceforge.m64py.M64Py",
|
||||
"net.supertuxkart.SuperTuxKart",
|
||||
"net.trowell.typesetter",
|
||||
"net.veloren.airshipper",
|
||||
"net.waterfox.waterfox",
|
||||
"net.werwolv.ImHex",
|
||||
"net.wz2100.wz2100",
|
||||
"net.xmind.XMind",
|
||||
"net.zdechov.app.x2048",
|
||||
"network.loki.Session",
|
||||
"nl.openoffice.bluefish",
|
||||
"one.ablaze.floorp",
|
||||
"org.alienarena.alienarena",
|
||||
"org.altlinux.Tuner",
|
||||
"org.azahar_emu.Azahar",
|
||||
"org.bunkus.mkvtoolnix-gui",
|
||||
"org.cloudcompare.CloudCompare",
|
||||
"org.cockpit_project.CockpitClient",
|
||||
"org.contourterminal.Contour",
|
||||
"org.cryptomator.Cryptomator",
|
||||
"org.deskflow.deskflow",
|
||||
"org.diasurgical.DevilutionX",
|
||||
"org.dune3d.dune3d",
|
||||
"org.dupot.easyflatpak",
|
||||
"org.equicord.equibop",
|
||||
"org.fcitx.Fcitx5",
|
||||
"org.fedoraproject.MediaWriter",
|
||||
"org.feichtmeier.Musicpod",
|
||||
"org.ferdium.Ferdium",
|
||||
"org.fkoehler.KTailctl",
|
||||
"org.flameshot.Flameshot",
|
||||
"org.flightgear.FlightGear",
|
||||
"org.fooyin.fooyin",
|
||||
"org.freac.freac",
|
||||
"org.freecad.FreeCAD",
|
||||
"org.freedownloadmanager.Manager",
|
||||
"org.gabmus.gfeeds",
|
||||
"org.gabmus.hydrapaper",
|
||||
"org.gabmus.whatip",
|
||||
"org.gajim.Gajim",
|
||||
"org.gaphor.Gaphor",
|
||||
"org.garudalinux.firedragon",
|
||||
"org.gimp.GIMP",
|
||||
"org.gnome.Boxes",
|
||||
"org.gnome.Builder",
|
||||
"org.gnome.Calculator",
|
||||
"org.gnome.Calendar",
|
||||
"org.gnome.Characters",
|
||||
"org.gnome.Chess",
|
||||
"org.gnome.Connections",
|
||||
"org.gnome.Contacts",
|
||||
"org.gnome.Decibels",
|
||||
@@ -214,95 +498,207 @@
|
||||
"org.gnome.Evolution",
|
||||
"org.gnome.Extensions",
|
||||
"org.gnome.Firmware",
|
||||
"org.gnome.Fractal",
|
||||
"org.gnome.Logs",
|
||||
"org.gnome.Loupe",
|
||||
"org.gnome.Mahjongg",
|
||||
"org.gnome.Maps",
|
||||
"org.gnome.Mines",
|
||||
"org.gnome.Music",
|
||||
"org.gnome.Papers",
|
||||
"org.gnome.Quadrapassel",
|
||||
"org.gnome.Shotwell",
|
||||
"org.gnome.Showtime",
|
||||
"org.gnome.SimpleScan",
|
||||
"org.gnome.Snapshot",
|
||||
"org.gnome.Sudoku",
|
||||
"org.gnome.TextEditor",
|
||||
"org.gnome.Totem",
|
||||
"org.gnome.Weather",
|
||||
"org.gnome.World.Iotas",
|
||||
"org.gnome.World.PikaBackup",
|
||||
"org.gnome.World.Secrets",
|
||||
"org.gnome.baobab",
|
||||
"org.gnome.clocks",
|
||||
"org.gnome.design.IconLibrary",
|
||||
"org.gnome.font-viewer",
|
||||
"org.gnome.gedit",
|
||||
"org.gnome.gitlab.YaLTeR.Identity",
|
||||
"org.gnome.gitlab.YaLTeR.VideoTrimmer",
|
||||
"org.gnome.gitlab.somas.Apostrophe",
|
||||
"org.gnucash.GnuCash",
|
||||
"org.gottcode.FocusWriter",
|
||||
"org.gramps_project.Gramps",
|
||||
"org.gtkhash.gtkhash",
|
||||
"org.inkscape.Inkscape",
|
||||
"org.jamovi.jamovi",
|
||||
"org.jaspstats.JASP",
|
||||
"org.jellyfin.JellyfinDesktop",
|
||||
"org.jellyfin.JellyfinServer",
|
||||
"org.jousse.vincent.Pomodorolm",
|
||||
"org.js.nuclear.Nuclear",
|
||||
"org.kartkrew.RingRacers",
|
||||
"org.kde.CrowTranslate",
|
||||
"org.kde.amarok",
|
||||
"org.kde.arianna",
|
||||
"org.kde.ark",
|
||||
"org.kde.audiotube",
|
||||
"org.kde.calligra",
|
||||
"org.kde.digikam",
|
||||
"org.kde.dolphin",
|
||||
"org.kde.dragonplayer",
|
||||
"org.kde.elisa",
|
||||
"org.kde.falkon",
|
||||
"org.kde.filelight",
|
||||
"org.kde.gcompris",
|
||||
"org.kde.ghostwriter",
|
||||
"org.kde.gwenview",
|
||||
"org.kde.haruna",
|
||||
"org.kde.isoimagewriter",
|
||||
"org.kde.kaffeine",
|
||||
"org.kde.kalk",
|
||||
"org.kde.kalzium",
|
||||
"org.kde.kamoso",
|
||||
"org.kde.kasts",
|
||||
"org.kde.kate",
|
||||
"org.kde.kcalc",
|
||||
"org.kde.kclock",
|
||||
"org.kde.kdenlive",
|
||||
"org.kde.kdf",
|
||||
"org.kde.kget",
|
||||
"org.kde.kid3",
|
||||
"org.kde.kleopatra",
|
||||
"org.kde.kmahjongg",
|
||||
"org.kde.kmines",
|
||||
"org.kde.kmymoney",
|
||||
"org.kde.koko",
|
||||
"org.kde.kolourpaint",
|
||||
"org.kde.konsole",
|
||||
"org.kde.kontact",
|
||||
"org.kde.kpat",
|
||||
"org.kde.krdc",
|
||||
"org.kde.krename",
|
||||
"org.kde.krita",
|
||||
"org.kde.kshisen",
|
||||
"org.kde.kstars",
|
||||
"org.kde.ksudoku",
|
||||
"org.kde.ktorrent",
|
||||
"org.kde.ktouch",
|
||||
"org.kde.kwalletmanager5",
|
||||
"org.kde.kweather",
|
||||
"org.kde.kwrite",
|
||||
"org.kde.labplot",
|
||||
"org.kde.marble",
|
||||
"org.kde.merkuro",
|
||||
"org.kde.minuet",
|
||||
"org.kde.neochat",
|
||||
"org.kde.okular",
|
||||
"org.kde.plasmatube",
|
||||
"org.kde.qrca",
|
||||
"org.kde.skanlite",
|
||||
"org.kde.skanpage",
|
||||
"org.kde.subtitlecomposer",
|
||||
"org.kde.yakuake",
|
||||
"org.keepassxc.KeePassXC",
|
||||
"org.kicad.KiCad",
|
||||
"org.kiwix.desktop",
|
||||
"org.learningequality.Kolibri",
|
||||
"org.leocad.LeoCAD",
|
||||
"org.librecad.librecad",
|
||||
"org.libreoffice.LibreOffice",
|
||||
"org.librepcb.LibrePCB",
|
||||
"org.libretro.RetroArch",
|
||||
"org.linux_hardware.hw-probe",
|
||||
"org.localsend.localsend_app",
|
||||
"org.luanti.luanti",
|
||||
"org.meshtastic.meshtasticd",
|
||||
"org.mixxx.Mixxx",
|
||||
"org.moneymanagerex.MMEX",
|
||||
"org.mozilla.Thunderbird",
|
||||
"org.mozilla.firefox",
|
||||
"org.mozilla.vpn",
|
||||
"org.nickvision.cavalier",
|
||||
"org.nickvision.money",
|
||||
"org.nickvision.tagger",
|
||||
"org.nickvision.tubeconverter",
|
||||
"org.nicotine_plus.Nicotine",
|
||||
"org.nomacs.ImageLounge",
|
||||
"org.onionshare.OnionShare",
|
||||
"org.onlyoffice.desktopeditors",
|
||||
"org.openandroidinstaller.OpenAndroidInstaller",
|
||||
"org.openmw.OpenMW",
|
||||
"org.openrgb.OpenRGB",
|
||||
"org.openscad.OpenSCAD",
|
||||
"org.opensurge2d.OpenSurge",
|
||||
"org.pencil2d.Pencil2D",
|
||||
"org.photoqt.PhotoQt",
|
||||
"org.pitivi.Pitivi",
|
||||
"org.ppsspp.PPSSPP",
|
||||
"org.prismlauncher.PrismLauncher",
|
||||
"org.pulseaudio.pavucontrol",
|
||||
"org.pvermeer.WebAppHub",
|
||||
"org.qbittorrent.qBittorrent",
|
||||
"org.qownnotes.QOwnNotes",
|
||||
"org.remmina.Remmina",
|
||||
"org.rncbc.qpwgraph",
|
||||
"org.sabnzbd.sabnzbd",
|
||||
"org.scummvm.ScummVM",
|
||||
"org.shotcut.Shotcut",
|
||||
"org.siril.Siril",
|
||||
"org.sqlitebrowser.sqlitebrowser",
|
||||
"org.squidowl.halloy",
|
||||
"org.srb2.SRB2",
|
||||
"org.stellarium.Stellarium",
|
||||
"org.strawberrymusicplayer.strawberry",
|
||||
"org.telegram.desktop",
|
||||
"org.tenacityaudio.Tenacity",
|
||||
"org.texstudio.TeXstudio",
|
||||
"org.thonny.Thonny",
|
||||
"org.torproject.torbrowser-launcher",
|
||||
"org.turbowarp.TurboWarp",
|
||||
"org.tuxpaint.Tuxpaint",
|
||||
"org.upscayl.Upscayl",
|
||||
"org.vinegarhq.Sober",
|
||||
"org.vinegarhq.Vinegar",
|
||||
"org.virt_manager.virt-manager",
|
||||
"org.wesnoth.Wesnoth",
|
||||
"org.wezfurlong.wezterm",
|
||||
"org.x.Warpinator",
|
||||
"org.xonotic.Xonotic",
|
||||
"org.zaproxy.ZAP",
|
||||
"org.zrythm.Zrythm",
|
||||
"page.codeberg.JakobDev.jdMinecraftLauncher",
|
||||
"page.codeberg.censor.Censor",
|
||||
"page.codeberg.impromptux.ytdl-gui",
|
||||
"page.codeberg.libre_menu_editor.LibreMenuEditor",
|
||||
"page.codeberg.lo_vely.Nucleus",
|
||||
"page.codeberg.tahoso.azul-box",
|
||||
"page.kramo.Cartridges",
|
||||
"page.tesk.Refine",
|
||||
"re.fossplant.songrec",
|
||||
"re.sonny.Eloquent",
|
||||
"re.sonny.Junction",
|
||||
"re.sonny.Tangram",
|
||||
"re.sonny.Workbench",
|
||||
"rocks.koreader.KOReader",
|
||||
"rocks.shy.VacuumTube",
|
||||
"rs.ruffle.Ruffle",
|
||||
"ru.linux_gaming.PortProton",
|
||||
"ru.yandex.Browser",
|
||||
"sa.sy.bluerecorder",
|
||||
"se.sjoerd.Graphs",
|
||||
"so.libdb.dissent",
|
||||
"space.gaiasky.GaiaSky",
|
||||
"space.rirusha.Cassette",
|
||||
"studio.planetpeanut.Bobby",
|
||||
"tv.kodi.Kodi",
|
||||
"tv.plex.PlexDesktop",
|
||||
"tv.plex.PlexHTPC",
|
||||
"us.materialio.Materialious",
|
||||
"vn.hoabinh.quan.CoBang",
|
||||
"website.i2pd.i2pd",
|
||||
"work.openpaper.Paperwork",
|
||||
"xyz.hyperplay.HyperPlay",
|
||||
"xyz.ketok.Speedtest",
|
||||
"xyz.z3ntu.razergenie"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user