Replace motion with animejs and new layout redesign (#672)

Co-authored-by: taroj1205 <taroj1205@gmail.com>
Co-authored-by: Shintaro Jokagi <61367823+taroj1205@users.noreply.github.com>
Co-authored-by: mr. m <91018726+mauro-balades@users.noreply.github.com>
Co-authored-by: Canoa <gitgood@thatcanoa.org>
Co-authored-by: Bryan Galdámez <josuegalre@gmail.com>
This commit is contained in:
mr. m 2025-06-21 17:12:31 +02:00 committed by GitHub
parent 9db42c090f
commit da96f4844a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 679 additions and 690 deletions

View file

@ -64,15 +64,6 @@ jobs:
if: steps.check-node-modules-cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Save node_modules cache
if: steps.check-node-modules-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
node_modules
*/node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/pnpm-lock.yaml') }}
quality_checks:
name: ${{ matrix.name }}
needs: [check_changes, setup]
@ -102,6 +93,7 @@ jobs:
restore-keys: ${{ runner.os }}-turbo-${{ matrix.check }}-
- name: Restore node_modules cache
id: cache
uses: actions/cache/restore@v4
with:
path: |
@ -121,6 +113,7 @@ jobs:
run_install: false
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Run ${{ matrix.name }}
@ -128,7 +121,7 @@ jobs:
playwright:
name: Playwright Tests
needs: [check_changes, setup, quality_checks]
needs: [check_changes, setup]
if: ${{ needs.check_changes.outputs.exists == 'true' }}
runs-on: ubuntu-latest
steps:
@ -144,6 +137,7 @@ jobs:
restore-keys: ${{ runner.os }}-turbo-
- name: Restore node_modules cache
id: cache
uses: actions/cache/restore@v4
with:
path: |
@ -163,6 +157,7 @@ jobs:
run_install: false
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Cache Playwright Browsers
@ -177,6 +172,7 @@ jobs:
- name: Run Playwright Tests
run: pnpm exec turbo run test:playwright
timeout-minutes: 10
verify:
name: Verify

View file

@ -5,21 +5,22 @@
"words": [
"adam",
"AMOLED",
"animejs",
"Astronav",
"Briel",
"brhm",
"Brhm",
"Briel",
"bryan",
"Canoa",
"canoa",
"Canoa",
"cfasync",
"createdAsc",
"createdDefault",
"createdDesc",
"daniel",
"FMPEG",
"ferrocyante",
"flatpaks",
"FMPEG",
"Galdámez",
"García",
"Garro",
@ -33,14 +34,14 @@
"Jokagi",
"junicode",
"Junicode",
"kristijanribaric",
"Kristijan",
"kristijanribaric",
"laggy",
"larvey",
"Larvey",
"linaarchsum",
"linuxarmsum",
"linuxsum",
"linaarchsum",
"mfsa",
"mozilla",
"Nehalem",
@ -51,12 +52,13 @@
"patreon",
"Pdzly",
"Ribaric",
"securtiy",
"taroj",
"Turborepo",
"testid",
"theming",
"tuta",
"tsconfigs",
"Turborepo",
"tuta",
"unfloatable",
"unfocusing",
"unrs",
@ -66,8 +68,8 @@
"VAAPI",
"wmfcdm",
"workerd",
"XPCOM",
"xmark",
"XPCOM",
"yumemi",
"zsync"
],

View file

@ -37,23 +37,25 @@
"@fortawesome/free-solid-svg-icons": "6.7.1",
"@types/react": "19.1.6",
"@types/react-dom": "19.1.5",
"animejs": "4.0.2",
"astro": "5.7.10",
"astro-navbar": "2.3.7",
"turbo": "2.5.4",
"autoprefixer": "10.4.14",
"clsx": "2.1.1",
"date-fns": "4.1.0",
"free-astro-components": "1.2.0",
"jiti": "2.4.2",
"js-confetti": "0.12.0",
"lefthook": "1.11.13",
"motion": "12.15.0",
"postcss": "8.5.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"sharp": "0.33.5",
"tailwind-merge": "3.3.0",
"tailwindcss": "3.4.15",
"typescript": "5.6.3"
"turbo": "2.5.4",
"typescript": "5.6.3",
"ua-parser-js": "2.0.3"
},
"devDependencies": {
"@commitlint/cli": "19.8.1",
@ -72,6 +74,7 @@
"@playwright/test": "1.52.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/user-event": "14.6.1",
"@types/animejs": "3.1.13",
"@types/eslint-plugin-jsx-a11y": "6.10.0",
"@types/jsdom": "21.1.7",
"@types/node": "22.15.18",

169
pnpm-lock.yaml generated
View file

@ -44,6 +44,9 @@ importers:
'@types/react-dom':
specifier: 19.1.5
version: 19.1.5(@types/react@19.1.6)
animejs:
specifier: 4.0.2
version: 4.0.2
astro:
specifier: 5.7.10
version: 5.7.10(@types/node@22.15.18)(jiti@2.4.2)(rollup@4.41.1)(typescript@5.6.3)(yaml@2.8.0)
@ -65,12 +68,12 @@ importers:
jiti:
specifier: 2.4.2
version: 2.4.2
js-confetti:
specifier: 0.12.0
version: 0.12.0
lefthook:
specifier: 1.11.13
version: 1.11.13
motion:
specifier: 12.15.0
version: 12.15.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
postcss:
specifier: 8.5.1
version: 8.5.1
@ -95,6 +98,9 @@ importers:
typescript:
specifier: 5.6.3
version: 5.6.3
ua-parser-js:
specifier: 2.0.3
version: 2.0.3
devDependencies:
'@commitlint/cli':
specifier: 19.8.1
@ -144,6 +150,9 @@ importers:
'@testing-library/user-event':
specifier: 14.6.1
version: 14.6.1(@testing-library/dom@10.4.0)
'@types/animejs':
specifier: 3.1.13
version: 3.1.13
'@types/eslint-plugin-jsx-a11y':
specifier: 6.10.0
version: 6.10.0
@ -1505,6 +1514,9 @@ packages:
'@tybys/wasm-util@0.9.0':
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
'@types/animejs@3.1.13':
resolution: {integrity: sha512-yWg9l1z7CAv/TKpty4/vupEh24jDGUZXv4r26StRkpUPQm04ztJaftgpto8vwdFs8SiTq6XfaPKCSI+wjzNMvQ==}
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
@ -1556,6 +1568,9 @@ packages:
'@types/nlcst@2.0.3':
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
'@types/node-fetch@2.6.12':
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
'@types/node@17.0.45':
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
@ -1826,6 +1841,9 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
animejs@4.0.2:
resolution: {integrity: sha512-f0L/kSya2RF23iMSF/VO01pMmLwlAFoiQeNAvBXhEyLzIPd2/QTBRatwGUqkVCC6seaAJYzAkGir55N4SL+h3A==}
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
@ -1942,6 +1960,9 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
autoprefixer@10.4.14:
resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
engines: {node: ^10 || ^12 || >=14}
@ -2125,6 +2146,10 @@ packages:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@ -2334,6 +2359,10 @@ packages:
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@ -2341,6 +2370,9 @@ packages:
destr@2.0.5:
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
detect-europe-js@0.1.2:
resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==}
detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
@ -2744,23 +2776,13 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
form-data@4.0.3:
resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==}
engines: {node: '>= 6'}
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
framer-motion@12.15.0:
resolution: {integrity: sha512-XKg/LnKExdLGugZrDILV7jZjI599785lDIJZLxMiiIFidCsy0a4R2ZEf+Izm67zyOuJgQYTHOmodi7igQsw3vg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
free-astro-components@1.2.0:
resolution: {integrity: sha512-bsT9dWsNlRGDNjqcoIlz6w8NcSCgOpx6oxiwZgYwq9RVbi3JqUImPc6c4Kico2wRJSIXc7HHyr71QgmgXv7nfg==}
@ -3111,6 +3133,9 @@ packages:
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
engines: {node: '>= 0.4'}
is-standalone-pwa@0.1.1:
resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==}
is-string@1.1.1:
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
engines: {node: '>= 0.4'}
@ -3184,6 +3209,9 @@ packages:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
js-confetti@0.12.0:
resolution: {integrity: sha512-1R0Akxn3Zn82pMqW65N1V2NwKkZJ75bvBN/VAb36Ya0YHwbaSiAJZVRr/19HBxH/O8x2x01UFAbYI18VqlDN6g==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -3547,6 +3575,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime@3.0.0:
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
engines: {node: '>=10.0.0'}
@ -3575,26 +3611,6 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
motion-dom@12.15.0:
resolution: {integrity: sha512-D2ldJgor+2vdcrDtKJw48k3OddXiZN1dDLLWrS8kiHzQdYVruh0IoTwbJBslrnTXIPgFED7PBN2Zbwl7rNqnhA==}
motion-utils@12.12.1:
resolution: {integrity: sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==}
motion@12.15.0:
resolution: {integrity: sha512-HLouXyIb1uQFiZgJTYGrtEzbatPc6vK+HP+Qt6afLQjaudiGiLLVsoy71CwzD/Stlh06FUd5OpyiXqn6XvqjqQ==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@ -4550,6 +4566,13 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
ua-is-frozen@0.1.2:
resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==}
ua-parser-js@2.0.3:
resolution: {integrity: sha512-LZyXZdNttONW8LjzEH3Z8+6TE7RfrEiJqDKyh0R11p/kxvrV2o9DrT2FGZO+KVNs3k+drcIQ6C3En6wLnzJGpw==}
hasBin: true
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
@ -6316,6 +6339,8 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/animejs@3.1.13': {}
'@types/aria-query@5.0.4': {}
'@types/babel__core@7.20.5':
@ -6382,6 +6407,11 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
'@types/node-fetch@2.6.12':
dependencies:
'@types/node': 22.15.18
form-data: 4.0.3
'@types/node@17.0.45': {}
'@types/node@22.15.18':
@ -6704,6 +6734,8 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
animejs@4.0.2: {}
ansi-align@3.0.1:
dependencies:
string-width: 4.2.3
@ -6940,6 +6972,8 @@ snapshots:
async-function@1.0.0: {}
asynckit@0.4.0: {}
autoprefixer@10.4.14(postcss@8.5.1):
dependencies:
browserslist: 4.25.0
@ -7129,6 +7163,10 @@ snapshots:
color-convert: 2.0.1
color-string: 1.9.1
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
comma-separated-tokens@2.0.3: {}
commander@14.0.0: {}
@ -7375,10 +7413,14 @@ snapshots:
defu@6.1.4: {}
delayed-stream@1.0.0: {}
dequal@2.0.3: {}
destr@2.0.5: {}
detect-europe-js@0.1.2: {}
detect-libc@2.0.4: {}
deterministic-object-hash@2.0.2:
@ -7948,16 +7990,15 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
fraction.js@4.3.7: {}
framer-motion@12.15.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
form-data@4.0.3:
dependencies:
motion-dom: 12.15.0
motion-utils: 12.12.1
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fraction.js@4.3.7: {}
free-astro-components@1.2.0(@types/node@22.15.18)(jiti@2.4.2)(prettier-plugin-astro@0.14.1)(prettier@3.5.3)(rollup@4.41.1)(yaml@2.8.0):
dependencies:
@ -8393,6 +8434,8 @@ snapshots:
dependencies:
call-bound: 1.0.4
is-standalone-pwa@0.1.1: {}
is-string@1.1.1:
dependencies:
call-bound: 1.0.4
@ -8481,6 +8524,8 @@ snapshots:
jiti@2.4.2: {}
js-confetti@0.12.0: {}
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@ -9004,6 +9049,12 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mime@3.0.0: {}
min-indent@1.0.1: {}
@ -9038,20 +9089,6 @@ snapshots:
minipass@7.1.2: {}
motion-dom@12.15.0:
dependencies:
motion-utils: 12.12.1
motion-utils@12.12.1: {}
motion@12.15.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
framer-motion: 12.15.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
mrmime@2.0.1: {}
ms@2.1.3: {}
@ -10078,6 +10115,18 @@ snapshots:
typescript@5.6.3: {}
ua-is-frozen@0.1.2: {}
ua-parser-js@2.0.3:
dependencies:
'@types/node-fetch': 2.6.12
detect-europe-js: 0.1.2
is-standalone-pwa: 0.1.1
node-fetch: 2.7.0
ua-is-frozen: 0.1.2
transitivePeerDependencies:
- encoding
ufo@1.6.1: {}
ultrahtml@1.6.0: {}

View file

@ -0,0 +1,13 @@
<svg width="408" height="96" viewBox="0 0 408 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M214.062 57.7319H220.318V64.7607H213.28V58.5127H189.038V70.2275H214.062V76.4756H220.318V89.752H214.062V96H188.256V89.752H181.999V82.7231H189.038V88.9712H213.28V77.2563H188.256V71.0083H181.999V57.7319H188.256V51.4839H214.062V57.7319Z" fill="#2e2e2e"/>
<path d="M246.906 57.7319H252.38V51.4839H265.675V57.7319H271.93V96H264.892V58.5127H253.162V96H246.124V58.5127H234.394V96H227.356V57.7319H233.612V51.4839H246.906V57.7319Z" fill="#2e2e2e"/>
<path d="M292.263 57.7319H297.737V51.4839H311.031V58.5127H298.519V88.9712H311.031V96H297.737V89.752H292.263V96H278.969V88.9712H291.481V58.5127H278.969V51.4839H292.263V57.7319Z" fill="#2e2e2e"/>
<path d="M337.619 57.7319H343.093V51.4839H362.644V58.5127H343.876V96H336.837V58.5127H318.069V51.4839H337.619V57.7319Z" fill="#2e2e2e"/>
<path d="M376.719 70.2275H400.962V51.4839H408V96H400.962V77.2563H376.719V96H369.682V51.4839H376.719V70.2275Z" fill="#2e2e2e"/>
<path d="M144 95.937H48.0007L47.9841 94.6992C47.327 68.7964 26.0955 48 0 48V0.0629883H144V95.937Z" fill="#2e2e2e"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M214.062 6.24805H220.318V19.5244H214.062V24.9917H220.318V38.2681H214.062V44.5161H181.999V24.9917H188.256V19.5244H181.999V0H214.062V6.24805ZM189.038 37.4873H213.28V25.7725H189.038V37.4873ZM189.038 18.7437H213.28V7.02881H189.038V18.7437Z" fill="#2e2e2e"/>
<path d="M237.516 37.4873H262.54V44.5161H230.478V0H237.516V37.4873Z" fill="#2e2e2e"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M304.763 6.24805H311.019V44.5161H303.981V25.7725H279.739V44.5161H272.701V6.24805H278.957V0H304.763V6.24805ZM279.739 18.7437H303.981V7.02881H279.739V18.7437Z" fill="#2e2e2e"/>
<path d="M353.241 6.24805H359.498V13.2769H352.46V7.02881H328.217V37.4873H352.46V31.2393H359.498V38.2681H353.241V44.5161H327.435V38.2681H321.179V6.24805H327.435V0H353.241V6.24805Z" fill="#2e2e2e"/>
<path d="M376.696 18.7437H394.682V12.4956H400.938V0H407.976V13.2769H401.72V19.5244H395.464V24.9917H401.72V31.2393H407.976V44.5161H400.938V32.0205H394.682V25.7725H376.696V44.5161H369.658V0H376.696V18.7437Z" fill="#2e2e2e"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,13 @@
<svg width="1407" height="500" viewBox="0 0 1407 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_239_62)">
<path d="M1200.9 290.615C1200.9 246.523 1234.56 194.179 1291.91 194.179H1334.26L1319.06 242.181C1301.03 299.309 1273.23 335.14 1239.13 335.14C1214.37 335.14 1200.9 318.196 1200.9 290.615ZM986.088 266.512C965.018 334.06 997.167 380.54 1070.15 380.54C1080.8 380.54 1094.69 379.665 1099.04 378.585C1101.21 377.937 1102.29 376.847 1102.94 374.892L1115.54 332.754C1116.18 330.367 1115.54 328.412 1111.84 328.628C1100.76 329.719 1090.77 330.583 1081.65 330.583C1044.94 330.583 1029.95 311.037 1040.82 276.069L1066.44 193.963H1140.95C1142.9 193.963 1144.64 192.872 1145.5 190.486L1158.75 147.916C1159.4 145.529 1158.11 143.79 1155.28 143.79H1082.08L1091.64 112.732C1092.29 110.561 1091.85 109.255 1090.12 107.743L1051.45 72.7753C1049.28 70.8206 1046.89 71.4686 1046.02 74.298L986.077 266.523L986.088 266.512ZM729.565 268.024C708.064 336.663 742.599 381.62 796.898 381.62C828.172 381.62 852.718 366.631 870.321 342.084L869.889 373.574C869.889 376.62 871.412 377.484 874.014 377.484H908.766C911.379 377.484 912.459 376.393 913.334 374.006L983.269 148.337C984.133 145.518 983.053 143.78 980.008 143.78H930.483C928.085 143.78 926.789 144.87 925.915 147.257L896.152 242.397C877.686 301.263 848.366 334.924 817.74 334.924C787.113 334.924 774.079 310.81 785.591 273.888L824.91 148.348C825.774 145.529 824.694 143.79 821.649 143.79H772.124C769.738 143.79 768.431 144.881 767.556 147.268L729.543 268.034L729.565 268.024ZM560.148 266.512C539.079 334.06 571.228 380.54 644.209 380.54C654.846 380.54 668.755 379.665 673.096 378.585C675.267 377.937 676.358 376.847 677.005 374.892L689.597 332.754C690.245 330.367 689.381 328.412 685.904 328.628C674.824 329.719 664.835 330.583 655.71 330.583C619.003 330.583 604.014 311.037 614.878 276.069L640.504 193.963H715.008C716.962 193.963 718.701 192.872 719.565 190.486L732.815 147.916C733.463 145.529 732.167 143.79 729.338 143.79H656.142L665.699 112.732C666.347 110.561 665.915 109.255 664.176 107.743L625.515 72.7753C623.345 70.8206 620.947 71.4686 620.083 74.298L560.137 266.523L560.148 266.512ZM1220.88 381.631C1249.99 381.631 1274.09 368.596 1292.34 342.095V373.585C1292.34 376.188 1293.86 377.494 1296.25 377.494H1331C1333.39 377.494 1334.69 376.404 1335.56 374.017L1406.15 148.132C1406.8 145.745 1405.5 143.79 1403.11 143.79H1293.42C1186.34 143.79 1144.64 236.317 1144.64 296.263C1144.42 348.607 1177.22 381.62 1220.88 381.62" fill="#2e2e2e"/>
<path d="M40.6017 5L126.617 91.4471C128.571 93.4017 130.526 94.0497 133.571 94.0497H500.213C502.826 94.0497 504.338 91.0043 501.736 88.6177L416.79 3.04536C414.835 1.09071 412.88 0 408.755 0H42.5564C39.0791 0 38.6471 3.04536 40.6017 5Z" fill="#2e2e2e"/>
<path d="M27.999 496.091C27.5671 498.045 28.431 500 31.0444 500H388.561C392.038 500 393.561 498.477 394.641 495.443L502.815 148.348C503.906 144.87 502.373 143.791 499.338 143.791H140.731C137.686 143.791 136.822 144.881 135.731 147.268L27.999 496.091Z" fill="#2e2e2e"/>
<path d="M0.850098 396.177C0.850098 400.086 5.8501 400.086 6.93001 396.177L88.8199 130.54C89.9106 127.494 89.9106 125.54 87.2972 122.937L5.8501 42.5701C3.89545 40.3994 0.850098 41.4794 0.850098 44.0927V396.177Z" fill="#2e2e2e"/>
</g>
<defs>
<clipPath id="clip0_239_62">
<rect width="1405.48" height="500" fill="white" transform="translate(0.850098)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -1,10 +1,7 @@
---
import Image from 'astro/components/Image.astro'
import { motion } from 'motion/react'
import { getTitleAnimation } from '~/animations'
import ComImage from '~/assets/ComImage.png'
import Button from '~/components/Button.astro'
import Description from '~/components/Description.astro'
import CheckIcon from '~/icons/CheckIcon.astro'
import GitHubIcon from '~/icons/GitHubIcon.astro'
import { getLocale, getUI } from '~/utils/i18n'
@ -19,50 +16,85 @@ const {
---
<section
id="Community"
id="community"
class="relative flex w-full flex-col items-center gap-6 py-12 text-start md:text-center lg:py-36"
>
<Description class="mb-2 text-4xl font-bold sm:text-6xl">
<h2 class="mb-2 text-4xl font-bold sm:text-6xl">
{
community.title.map((title, index) =>
community.title.map(title =>
title !== '\n' ? (
<motion.span client:load {...getTitleAnimation(0.2 + index * 0.2)}>
<span style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)">
{title}
</motion.span>
</span>
) : (
<br class="hidden md:block" />
)
)
}
</Description>
<motion.p client:load {...getTitleAnimation(0.6)} className="lg:w-1/2 lg:px-0">
{community.description}
</motion.p>
</h2>
<p
class="text-base lg:w-1/2 lg:px-0"
style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)"
>
<span class="opacity-80">{community.description}</span>
</p>
<div class="flex w-full flex-wrap gap-3 sm:gap-10 md:justify-center">
<motion.span client:load {...getTitleAnimation(0.8)}>
<div
class="community__button"
style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)"
>
<Button class:list={['px-4']} href="https://github.com/zen-browser">
<GitHubIcon class="size-4" />
<span>{community.lists.freeAndOpenSource.title}</span>
</Button>
</motion.span>
<motion.div client:load {...getTitleAnimation(1)} className="flex items-center gap-4">
</div>
<div
class="community__button flex items-center gap-4"
style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)"
>
<CheckIcon class="size-4" />
<span>{community.lists.simpleYetPowerful.title}</span>
</motion.div>
<motion.div client:load {...getTitleAnimation(1.2)} className="flex items-center gap-4">
</div>
<div
class="community__button flex items-center gap-4"
style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)"
>
<CheckIcon class="size-4" />
<span>{community.lists.privateAndAlwaysUpToDate.title}</span>
</motion.div>
</div>
<motion.span
className="flex max-w-full lg:max-w-none lg:flex-none"
client:load
{...getTitleAnimation(1.4)}
>
</div>
<span class="flex max-w-full lg:max-w-none lg:flex-none">
<Image
src={ComImage}
alt={community.images.community.alt}
class="rounded-3xl shadow-md lg:mx-auto dark:opacity-80"
style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)"
/>
</motion.span>
</span>
</section>
<script>
import { animate, onScroll, stagger } from 'animejs'
function initAnimations() {
const debug = false
const elements = document.querySelectorAll(
'#community h2 span, #community p, #community .community__button, #community img'
)
animate(elements, {
opacity: { from: 0.001, to: 1 },
translateY: { from: 20, to: 0 },
filter: { from: 'blur(4px)', to: 'blur(0px)' },
duration: 300,
delay: stagger(150),
ease: 'cubicBezier(0.25, 0.1, 0.25, 1)',
autoplay: onScroll({
target: '#community',
debug,
}),
})
}
initAnimations()
</script>

View file

@ -1,9 +1,6 @@
---
import { motion } from 'motion/react'
import { getTitleAnimation } from '~/animations'
import { getLocale, getUI } from '~/utils/i18n'
import Description from './Description.astro'
import Video from './Video.astro'
const locale = getLocale(Astro)
@ -19,96 +16,61 @@ interface Props {
}
const { titles } = Astro.props
const descriptions = Object.values(features.featureTabs).map(tab => tab.description)
---
<section id="Features" class="relative flex w-full flex-col py-12 text-start lg:py-36">
<section id="features" class="relative flex w-full flex-col py-12 text-start lg:py-36">
<Description class="mb-2 text-4xl font-bold sm:text-6xl">
{
(titles || features.titles).map((title, index) =>
title !== '\n' ? (
<motion.span client:load {...getTitleAnimation(0.2 + index * 0.2)}>
<span class="title-line opacity-0" data-index={index}>
{title}
</motion.span>
</span>
) : (
<br class="hidden md:block" />
)
)
}
</Description>
<motion.p client:load {...getTitleAnimation(0.6)} className="lg:w-1/2">
<p class="feature-description-text opacity-0 lg:w-1/2">
{features.description}
</motion.p>
</p>
<div class="mt-6 flex flex-col gap-6 lg:flex-row lg:justify-between lg:gap-2">
<div class="flex w-full flex-col lg:w-1/3">
<!-- Mobile tabs -->
<!-- Mobile Tabs -->
<div class="flex gap-2 overflow-x-auto overflow-y-clip lg:hidden">
<motion.button
client:load
{...getTitleAnimation()}
className="feature-tab whitespace-nowrap"
data-active="true"
{
Object.entries(features.featureTabs).map(([key, tab], i) => (
<button
class="feature-tab whitespace-nowrap opacity-0"
data-index={i}
data-feature-key={key}
data-active={i === 0 ? 'true' : undefined}
>
{features.featureTabs.workspaces.title}
</motion.button>
<motion.button
client:load
{...getTitleAnimation(0.2)}
className="feature-tab whitespace-nowrap"
>
{features.featureTabs.compactMode.title}
</motion.button>
<motion.button
client:load
{...getTitleAnimation(0.4)}
className="feature-tab whitespace-nowrap"
>
{features.featureTabs.glance.title}
</motion.button>
<motion.button
client:load
{...getTitleAnimation(0.6)}
className="feature-tab whitespace-nowrap"
>
{features.featureTabs.splitView.title}
</motion.button>
{tab.title}
</button>
))
}
</div>
<!-- Desktop features list -->
<div id="features-list" class="hidden lg:flex lg:flex-col lg:gap-3">
<motion.div client:load {...getTitleAnimation(0.8)} className="feature" data-active="true">
<Description class="text-2xl font-bold">
{features.featureTabs.workspaces.title}
</Description>
<Description>
{features.featureTabs.workspaces.description}
</Description>
</motion.div>
<motion.div client:load {...getTitleAnimation(1)} className="feature">
<Description class="text-2xl font-bold">
{features.featureTabs.compactMode.title}
</Description>
<Description>
{features.featureTabs.compactMode.description}
</Description>
</motion.div>
<motion.div client:load {...getTitleAnimation(1.2)} className="feature">
<Description class="text-2xl font-bold">
{features.featureTabs.glance.title}
</Description>
<Description>
{features.featureTabs.glance.description}
</Description>
</motion.div>
<motion.div client:load {...getTitleAnimation(1.4)} className="feature">
<Description class="text-2xl font-bold">
{features.featureTabs.splitView.title}
</Description>
<Description>
{features.featureTabs.splitView.description}
</Description>
</motion.div>
{
Object.entries(features.featureTabs).map(([key, tab], i) => (
<div
class="feature opacity-0"
data-index={i}
data-feature-key={key}
data-active={i === 0 ? 'true' : undefined}
>
<Description class="text-2xl font-bold">{tab.title}</Description>
<Description>{tab.description}</Description>
</div>
))
}
</div>
<!-- Mobile description -->
@ -116,6 +78,7 @@ const descriptions = Object.values(features.featureTabs).map(tab => tab.descript
</div>
</div>
<!-- Video Section -->
<div class="sticky top-6 h-fit w-full lg:w-3/5">
<div class="relative w-full">
<div class="video-stack relative h-full w-full">
@ -162,54 +125,58 @@ const descriptions = Object.values(features.featureTabs).map(tab => tab.descript
</section>
<script>
import { animate, stagger } from 'animejs'
// Animate Titles
animate('.title-line', {
opacity: [0, 1],
delay: stagger(200, { start: 200 }),
duration: 600,
easing: 'easeOutQuad',
})
animate('.feature-description-text', {
opacity: [0, 1],
delay: 600,
duration: 600,
easing: 'easeOutQuad',
})
animate('.feature-tab, .feature', {
opacity: [0, 1],
delay: stagger(200, { start: 800 }),
duration: 500,
easing: 'easeOutQuad',
})
const features = document.querySelectorAll('.feature, .feature-tab') as NodeListOf<HTMLElement>
// Set initial description
const descriptionEl = document.querySelector('.feature-description') as HTMLDivElement
const descriptionEl = document.querySelector('.feature-description') as HTMLDivElement | null
const descriptions = descriptionEl?.dataset.descriptions?.split('|||')
if (descriptionEl && descriptions) {
descriptionEl.textContent = descriptions[0]
}
const videos = document.querySelectorAll('.feature-video') as NodeListOf<HTMLVideoElement>
function changeToFeature({ target }: MouseEvent | { target: HTMLElement }) {
let targetEl: HTMLElement | null = target as HTMLElement
function changeToFeature({ target }: { target: HTMLElement }) {
const targetEl = target?.closest('.feature, .feature-tab') as HTMLElement | null
if (!targetEl) return
if (target instanceof HTMLElement) {
targetEl = target.closest('.feature, .feature-tab')
}
if (!targetEl) {
return
}
const index = Array.from(features).indexOf(targetEl) % 4
if (index === -1) {
return
}
// Update both mobile and desktop elements
for (let i = 0; i < features.length; i += 1) {
const f = features[i]
const index = parseInt(targetEl.dataset.index as string, 10)
if (isNaN(index)) return
// Toggle active states
features.forEach((el, i) => {
if (i % 4 === index) {
f.setAttribute('data-active', 'true')
el.setAttribute('data-active', 'true')
} else {
f.removeAttribute('data-active')
}
el.removeAttribute('data-active')
}
})
// Update mobile description
const descriptionEl = document.querySelector('.feature-description')
if (descriptionEl && descriptions) {
descriptionEl.textContent = descriptions[index]
}
const videos = document.querySelectorAll<HTMLVideoElement>('.feature-video')
for (let i = 0; i < videos.length; i += 1) {
const vid = videos[i]
// Animate videos
videos.forEach((vid, i) => {
const yOffset = (i - index) * 20
const zOffset = i === index ? 0 : -100 - Math.abs(i - index) * 50
const scale = i === index ? 1 : 0.95
@ -224,22 +191,17 @@ const descriptions = Object.values(features.featureTabs).map(tab => tab.descript
vid.play()
} else {
vid.removeAttribute('data-active')
vid.style.transform = `translate3d(-50%, ${yOffset}px, ${zOffset}px)
rotate3d(1, 0, 0, ${rotation}deg)
scale(${scale})`
vid.style.transform = `translate3d(-50%, ${yOffset}px, ${zOffset}px) rotate3d(1, 0, 0, ${rotation}deg) scale(${scale})`
vid.style.zIndex = String(1 - Math.abs(i - index))
vid.pause()
}
}
}
for (const feature of features) {
feature.addEventListener('click', changeToFeature)
})
}
// Set up event listeners
features.forEach(f => f.addEventListener('click', changeToFeature as unknown as EventListener))
changeToFeature({ target: features[0] })
</script>
<style>
.feature {
@apply w-full cursor-pointer select-none rounded-lg p-4 opacity-0 hover:bg-subtle;

View file

@ -24,9 +24,11 @@ const {
class="flex w-full flex-col gap-4 text-center lg:w-1/2 lg:text-left"
aria-labelledby="footer-title"
>
<a href="/">
<Description id="footer-title" class="text-6xl font-bold !text-paper"
>{footer.title}</Description
>
</a>
<Description class="mx-auto max-w-xl lg:mx-0">
{footer.description}
</Description>
@ -115,6 +117,11 @@ const {
>{footer.reportAnIssue}</a
>
</li>
<li>
<a href="https://github.com/zen-browser/desktop/security/policy" class="font-normal"
>{footer.securtiy}</a
>
</li>
</ul>
</nav>
</div>

View file

@ -1,24 +1,11 @@
---
import { motion } from 'motion/react'
import { getTitleAnimation } from '~/animations'
import Button from '~/components/Button.astro'
import Description from '~/components/Description.astro'
import Title from '~/components/Title.astro'
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
import { getLocale, getPath, getUI } from '~/utils/i18n'
import SocialMediaStrip from './SocialMediaStrip.astro'
import Video from './Video.astro'
let titleAnimationCounter = 0
function getNewAnimationDelay() {
titleAnimationCounter++
return titleAnimationCounter * 0.15
}
function getHeroTitleAnimation() {
return getTitleAnimation(getNewAnimationDelay())
}
const locale = getLocale(Astro)
const getLocalePath = getPath(locale)
@ -32,58 +19,54 @@ const {
<header
id="header"
class="flex w-full flex-col items-center gap-[20%] py-32 text-center lg:gap-[25%]"
class="flex w-full flex-col items-center gap-6 py-16 text-center lg:gap-12 lg:py-32"
>
<div class="flex h-full flex-col items-center justify-center">
<Title class="relative px-12 text-center font-normal md:text-7xl lg:px-0 lg:text-9xl">
<div class="flex h-full flex-col items-center justify-center gap-6 md:gap-8">
<div class="flex flex-col items-center justify-center gap-4 md:gap-8">
<div>
<Title class="relative px-12 text-center text-5xl md:text-7xl lg:px-0 lg:text-9xl">
{
hero.title.map(title =>
title.text !== '\n' ? (
<motion.span
client:load
{...getHeroTitleAnimation()}
className={title.highlight ? 'italic text-coral' : ''}
<b
class:list={['font-normal', title.highlight && 'italic text-coral']}
style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)"
>
{title.text}
</motion.span>
</b>
) : (
<br class="hidden md:block" />
)
)
}
</Title>
<motion.span client:load {...getHeroTitleAnimation()}>
<Description class="px-12 text-center lg:px-0">
<p
class="px-12 text-center lg:px-0"
style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)"
>
{hero.description[0]}
<br class="hidden sm:inline" />
{hero.description[1]}</Description
{hero.description[1]}
</p>
</div>
<div
class="flex w-2/3 flex-col items-center justify-center gap-3 sm:gap-6 md:w-fit md:flex-row"
>
</motion.span>
<div class="mt-6 flex w-2/3 flex-col gap-3 sm:gap-6 md:w-fit md:flex-row">
<motion.span client:load {...getHeroTitleAnimation()}>
<Button class="w-full" href={getLocalePath('/download')} isPrimary>
<div style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)">
<Button class="w-fit" href={getLocalePath('/download')} isPrimary>
{hero.buttons.beta}
<ArrowRightIcon class="size-4" />
</Button>
</motion.span>
<motion.span client:load {...getHeroTitleAnimation()}>
<Button href={getLocalePath('/donate')}>{hero.buttons.support}</Button>
</motion.span>
</div>
<motion.span
client:load
{...getHeroTitleAnimation()}
className="mx-auto translate-y-16 !transform"
>
<SocialMediaStrip />
</motion.span>
<div style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)">
<Button class="w-fit" href={getLocalePath('/donate')}>
{hero.buttons.support}
</Button>
</div>
</div>
</div>
<SocialMediaStrip style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)" />
</div>
</header>
<motion.span
className="flex max-w-full lg:max-w-none lg:flex-none"
client:load
{...getHeroTitleAnimation()}
>
<Video
name="hero-video"
autoplay
@ -91,6 +74,39 @@ const {
muted
playsinline
preload="none"
class="mb-24 rounded-3xl shadow-md dark:opacity-80"
class="rounded-xl"
style="transform: translateY(20px); opacity: 0.001; filter: blur(4px); transform-origin: top;"
/>
</motion.span>
</header>
<script>
import { animate, onScroll, stagger } from 'animejs'
function initAnimations() {
const debug = false
const elements = document.querySelectorAll(
'#header h1 b, #header p, #header div:has(a), #header video, #header ul'
)
animate(elements, {
// @ts-expect-error - element is HTMLElement
opacity: element => {
if (element.tagName === 'UL') {
return { from: 0.001, to: 0.8 }
}
return { from: 0.001, to: 1 }
},
translateY: { from: 20, to: 0 },
filter: { from: 'blur(4px)', to: 'blur(0px)' },
duration: 300,
delay: stagger(150),
ease: 'cubicBezier(0.25, 0.1, 0.25, 1)',
autoplay: onScroll({
target: '#header',
debug,
}),
})
}
initAnimations()
</script>

View file

@ -1,5 +1,5 @@
---
const { gap = 4 } = Astro.props
const { class: className, ...props } = Astro.props
import { icon, library } from '@fortawesome/fontawesome-svg-core'
import {
@ -9,6 +9,7 @@ import {
faReddit,
faXTwitter,
} from '@fortawesome/free-brands-svg-icons'
import { cn } from '~/utils/merge'
library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit)
const Mastodon = icon({ prefix: 'fab', iconName: 'mastodon' })
@ -18,7 +19,7 @@ const XTwitter = icon({ prefix: 'fab', iconName: 'x-twitter' })
const Reddit = icon({ prefix: 'fab', iconName: 'reddit' })
---
<ul class={`flex items-center opacity-80 gap-${gap}`}>
<ul class={cn('flex items-center gap-4 opacity-80', className)} {...props}>
<li>
<a
href="https://github.com/zen-browser"

View file

@ -1,12 +1,11 @@
---
import { motion } from 'motion/react'
import { getTitleAnimation } from '~/animations'
import Description from '~/components/Description.astro'
import h3 from '~/components/Description.astro'
import { getLocale, getUI } from '~/utils/i18n'
const locale = getLocale(Astro)
import tutaLogo from '~/assets/sponsors/tutaLogo_monochrome.svg'
import tutaLogo from '~/assets/sponsors/tutaLogo-dark.svg'
import blacksmithLogo from '~/assets/sponsors/blacksmith-logo-dark.svg'
import Image from 'astro/components/Image.astro'
const { showSponsors = true } = Astro.props
@ -19,23 +18,64 @@ const {
---
<section id="sponsors" class:list={['py-12', !showSponsors && 'hidden']}>
<div class="mx-auto flex flex-col text-center">
<motion.span client:load {...getTitleAnimation(0.2)}>
<Description class="mb-2 text-4xl font-bold sm:text-6xl">{sponsors.title}</Description>
</motion.span>
<motion.span client:load {...getTitleAnimation(0.4)}>
<Description set:html={sponsors.description} />
</motion.span>
<div class="relative mt-8 flex items-center justify-center">
<motion.span client:load {...getTitleAnimation(0.6)}>
<a href={sponsors.sponsors['tuta'].url} target="_blank" class="w-fit">
<div class="flex flex-col items-center gap-6 text-center">
<h3
class="text-4xl font-bold sm:text-6xl"
style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)"
>
{sponsors.title}
</h3>
<p class="text-base" style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)">
<span class="opacity-80" set:html={sponsors.description} />
</p>
<div
class="sponsors__sponsor relative mt-4 flex flex-col items-center justify-center gap-8 md:flex-row md:gap-12"
>
<a href={sponsors.sponsors['blacksmith'].url} target="_blank" class="w-fit">
<Image
src={blacksmithLogo}
alt={sponsors.sponsors['blacksmith'].name}
class="h-16 w-fit object-contain dark:invert"
/>
</a>
<a
href={sponsors.sponsors['tuta'].url}
target="_blank"
class="w-fit"
style="transform: translateY(20px); opacity: 0.001; filter: blur(4px)"
>
<Image
src={tutaLogo}
alt={sponsors.sponsors['tuta'].name}
class="h-16 w-fit object-contain"
class="h-16 w-fit object-contain dark:invert"
/>
</a>
</motion.span>
</div>
</div>
</section>
<script>
import { animate, onScroll, stagger } from 'animejs'
function initAnimations() {
const debug = false
const elements = document.querySelectorAll(
'#sponsors h3, #sponsors p, #sponsors .sponsors__sponsor a'
)
animate(elements, {
opacity: { from: 0.001, to: 1 },
translateY: { from: 20, to: 0 },
filter: { from: 'blur(4px)', to: 'blur(0px)' },
duration: 300,
delay: stagger(150),
ease: 'cubicBezier(0.25, 0.1, 0.25, 1)',
autoplay: onScroll({
target: '#sponsors',
debug,
}),
})
}
initAnimations()
</script>

View file

@ -1,43 +1,63 @@
<script>
import JSConfetti from 'js-confetti'
import { UAParser } from 'ua-parser-js'
// Handle platform selection
const platformButtons = document.querySelectorAll('.platform-selector')
const platformSections = document.querySelectorAll('.platform-section')
// Function to detect OS and select appropriate platform
function detectOS() {
const userAgent = window.navigator.userAgent
const { cpu, os } = UAParser(userAgent)
let detectedOS = 'mac' // Default to macOS
if (userAgent.indexOf('Windows') !== -1) {
if (os.name === 'Windows') {
detectedOS = 'windows'
} else if (userAgent.indexOf('Linux') !== -1) {
} else if (os.name === 'Linux' || os.name === 'Ubuntu') {
detectedOS = 'linux'
}
return detectedOS
let arch = cpu.architecture || 'x86_64' // Default to x86_64
if (arch === 'amd64' || arch === 'x86_64') {
arch = 'x86_64'
} else if (arch === 'arm64' || arch === 'aarch64') {
arch = 'aarch64'
} else if (arch === 'arm') {
arch = 'arm64' // Treat arm as arm64 for simplicity
}
return { os: detectedOS, cpu: arch }
}
function getRandomNumber(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
// Initialize platform based on user's OS
async function initializePlatform() {
const detectedOS = detectOS()
selectPlatform(detectedOS)
selectPlatform(detectedOS.os, detectedOS.cpu)
for (const button of document.querySelectorAll('.download-button')) {
button.addEventListener('click', () => {
const jsConfetti = new JSConfetti()
jsConfetti.addConfetti({
confettiNumber: 100,
// If we get a 1 to 10k chance, show only burger confetti
...(getRandomNumber(1, 10000) === 1 ? { emojis: ['🍔'] } : {}),
})
})
}
}
// Function to select a platform
async function selectPlatform(platform: string) {
// Update button styling
for (const button of platformButtons) {
const buttonPlatform = button.getAttribute('data-platform')
if (buttonPlatform === platform) {
button.setAttribute('data-active', 'true')
} else {
button.setAttribute('data-active', 'false')
}
}
async function selectPlatform(platform: string, cpu = 'x86_64') {
// Show/hide platform sections
for (const section of platformSections) {
if (section.id === `${platform}-downloads`) {
if (
section.getAttribute('data-platform') === platform &&
section.getAttribute('data-cpu') === cpu
) {
section.setAttribute('data-active', 'true')
} else {
section.setAttribute('data-active', 'false')
@ -45,14 +65,6 @@
}
}
// Handle platform button clicks
for (const button of platformButtons) {
button.addEventListener('click', () => {
const platform = button.getAttribute('data-platform') ?? ''
selectPlatform(platform)
})
}
// Check for twilight mode
async function checkTwilightMode() {
const urlParams = new URLSearchParams(window.location.search)

View file

@ -1,4 +1,7 @@
---
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
import Button from '../Button.astro'
interface ReleaseInfo {
label?: string
link: string
@ -24,130 +27,74 @@ interface PlatformReleases {
interface Props {
platform: 'mac' | 'windows' | 'linux'
icon: string[]
title: string
description: string
releases: PlatformReleases
cpu: 'x86_64' | 'aarch64' | 'arm64'
}
const { platform, icon, title, description, releases } = Astro.props
import { Image } from 'astro:assets'
import AppIconDark from '../../assets/app-icon-dark.png'
import AppIconLight from '../../assets/app-icon-light.png'
import DownloadCard from './ButtonCard.astro'
const { platform, icon, releases, cpu } = Astro.props
function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
return !!obj && typeof obj === 'object' && 'link' in obj
}
function getReleaseLink(releases: PlatformReleases, { os, cpu }: { os: string; cpu?: string }) {
if (os === 'mac') {
return releases.universal?.link || releases.all?.link || ''
} else {
if (cpu === 'x86_64') {
if (isFlatReleaseInfo(releases.x86_64)) {
return releases.flathub?.all.link || releases.x86_64.link || ''
}
return releases.x86_64?.tarball?.link ? releases.x86_64?.tarball?.link : ''
} else if (cpu === 'aarch64') {
return releases.aarch64?.tarball?.link || ''
} else if (cpu === 'arm64') {
return releases.arm64?.link || ''
}
}
}
---
<div
id={`${platform}-downloads`}
id={`${platform}-${cpu}-downloads`}
data-active={platform === 'mac'}
class="platform-section data-[active='false']:hidden"
data-cpu={cpu}
data-platform={platform}
>
<div class="items-center gap-8 md:flex">
<div class="mb-8 md:mb-0 md:w-2/3">
<div class="mb-4 flex items-center gap-3">
<div class="download-card__icon">
<div class="mx-auto flex w-full justify-center gap-4">
<Button
class="download-button w-fit"
href={getReleaseLink(releases, { os: platform, cpu })}
isPrimary
>
<Fragment set:html={icon} />
</div>
<h3 class="text-2xl font-medium">{title}</h3>
</div>
<p class="text-muted-foreground mb-6" set:html={description} />
<div class="space-y-6">
{
platform === 'linux' ? (
<>
{releases.flathub && releases.flathub.all.label && (
<article class="flathub-download data-[twilight='true']:hidden">
<h4 class="mb-3 text-lg font-medium">Package Managers</h4>
<div class="space-y-3">
<DownloadCard
label={releases.flathub.all.label}
href={releases.flathub.all.link}
variant="flathub"
/>
</div>
</article>
)}
{releases.x86_64 &&
typeof releases.x86_64 === 'object' &&
'tarball' in releases.x86_64 &&
releases.x86_64.tarball && (
<article>
<h4 class="mb-3 text-lg font-medium">x86_64</h4>
<div class="">
{releases.x86_64.tarball && (
<DownloadCard
label={releases.x86_64.tarball.label ? releases.x86_64.tarball.label : ''}
href={releases.x86_64.tarball.link ? releases.x86_64.tarball.link : ''}
variant="x86_64"
checksum={releases.x86_64.tarball.checksum}
/>
)}
</div>
</article>
)}
{releases.aarch64 &&
typeof releases.aarch64 === 'object' &&
'tarball' in releases.aarch64 &&
releases.aarch64.tarball && (
<article>
<h4 class="mb-3 text-lg font-medium">ARM64</h4>
<div class="gap-3">
{releases.aarch64.tarball && (
<DownloadCard
label={
releases.aarch64.tarball.label ? releases.aarch64.tarball.label : ''
Download for {
platform === 'mac' ? 'MacOS' : platform.charAt(0).toUpperCase() + platform.slice(1)
}
href={releases.aarch64.tarball.link ? releases.aarch64.tarball.link : ''}
variant="aarch64"
checksum={releases.aarch64.tarball.checksum}
/>
)}
</div>
</article>
)}
</>
) : (
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-3">
{releases.universal && releases.universal.label && (
<DownloadCard
label={releases.universal.label}
href={releases.universal.link}
checksum={releases.universal.checksum}
/>
)}
{releases.x86_64 && isFlatReleaseInfo(releases.x86_64) && releases.x86_64.label && (
<DownloadCard
label={releases.x86_64.label}
href={releases.x86_64.link}
checksum={releases.x86_64.checksum}
/>
)}
{releases.arm64 && releases.arm64.label && (
<DownloadCard
label={releases.arm64.label}
href={releases.arm64.link}
checksum={releases.arm64.checksum}
/>
)}
</div>
</div>
</Button>
{
platform === 'linux' && (
<Button
class="download-button w-fit"
href={releases.flathub?.all.link || ''}
aria-label="Download from Flathub"
target="_blank"
rel="noopener noreferrer"
>
Flathub
<ArrowRightIcon class="size-4" />
</Button>
)
}
</div>
</div>
<div
class="download-browser-logo flex justify-center text-coral transition-colors data-[twilight='true']:text-zen-blue md:w-1/3"
>
<Image src={AppIconDark} alt="Zen Browser" class="w-32 translate-y-6 transform dark:hidden" />
<Image
src={AppIconLight}
alt="Zen Browser"
class="hidden w-32 translate-y-6 transform dark:block"
/>
</div>
</div>
</div>
<style is:global>
.download-button .svg-inline--fa {
@apply size-5;
}
</style>

View file

@ -49,12 +49,16 @@
"tuta": {
"name": "Tuta",
"url": "https://tuta.com/"
},
"blacksmith": {
"name": "BlackSmith",
"url": "https://www.blacksmith.sh/"
}
}
},
"community": {
"title": ["Our ", "Core ", "Values"],
"description": "We make it not only a priority, but a necessity to ensure that Zen always strikes the right balance between beauty, performance, and privacy. We are committed to making Zen the most beautiful, productive, and privacy-respecting browser out there — without compromising on your experience.",
"description": "We make it not only a priority, but a necessity to ensure that Zen always strikes the right balance between beauty, performance, and privacy.",
"lists": {
"freeAndOpenSource": {
"title": "Free and open-source",
@ -262,11 +266,13 @@
}
},
"download": {
"title": "Download Zen",
"description": "Download Zen for your platform and experience a more mindful internet browsing experience. All downloads include SHA256 checksums for verification.",
"title": "Start your journey",
"description": "Download Zen for your platform and experience a more mindful internet browsing experience.<br/>Zen is available for macOS, Windows, and Linux.",
"twilightInfo": "You're currently in Twilight mode, this means you're downloading the latest experimental features and updates.",
"beta": "Zen is currently in ",
"otherDownload": "If your device has been wrongly detected, <a href='https://github.com/zen-browser/desktop/releases/latest/' class='zen-link'>see other downloads</a>.<br />Please report any issues you encounter on <a href='https://github.com/zen-browser/desktop/issues/new/choose' class='zen-link ml-[1px]'>GitHub</a>.",
"alertInfo": {
"description": "<strong class='font-medium text-zen-blue'>Twilight Mode:</strong> You're currently in Twilight mode, this means you're downloading the latest experimental features and updates."
"description": "You're currently in Twilight mode, this means you're downloading the latest experimental features and updates."
},
"platformSelector": {
"title": "Platform Selector",
@ -290,10 +296,7 @@
"platformNames": {
"mac": "macOS",
"windows": "Windows",
"linux": "Linux",
"macDownload": "MacOS Download",
"windowsDownload": "Windows Download",
"linuxDownload": "Linux Download"
"linux": "Linux"
},
"platformDescriptions": {
"mac": "Works on both new Apple (M-Series) and older Intel Macs.<br />Requires macOS 11.0 or later.",
@ -448,7 +451,7 @@
},
"download": {
"title": "Download - Zen",
"description": "Download Zen for your platform and experience a more mindful internet browsing experience. All downloads include SHA256 checksums for verification."
"description": "Download Zen for your platform and experience a more mindful internet browsing experience."
},
"privacyPolicy": {
"title": "Privacy Policy - Zen",
@ -476,6 +479,7 @@
"zenMods": "Zen Mods",
"releaseNotes": "Release Notes",
"getHelp": "Get Help",
"securtiy": "Security",
"discord": "Discord",
"uptimeStatus": "Uptime Status",
"reportAnIssue": "Report an Issue",

View file

@ -49,6 +49,10 @@
"tuta": {
"name": "Tuta",
"url": "https://tuta.com/"
},
"blacksmith": {
"name": "BlackSmith",
"url": "https://www.blacksmith.sh/"
}
}
},
@ -262,11 +266,13 @@
}
},
"download": {
"title": "Zenをダウンロードする",
"title": "Ready to experience Zen?",
"description": "お使いのプラットフォーム向けにZenをダウンロード。すべてのダウンロードにはSHA256チェックサムが含まれています",
"twilightInfo": "現在Twilightモードです。最新の実験的機能とアップデートをダウンロードしています。",
"beta": "Zen is currently in ",
"otherDownload": "If your device got wrongly detected, <a href='https://github.com/zen-browser/desktop/releases/latest/' class='zen-link ml-[1px]'>see other downloads</a>.<br />Please report any issues you encounter on <a href='https://github.com/zen-browser/desktop/issues/new/choose' class='zen-link ml-[1px]'>GitHub</a>.",
"alertInfo": {
"description": "<strong class='font-medium text-zen-blue'>Twilightモード:</strong> 現在Twilightモードで、最新の実験的機能とアップデートをダウンロードしています。"
"description": "You're currently in Twilight mode, this means you're downloading the latest experimental features and updates."
},
"platformSelector": {
"title": "プラットフォーム選択",
@ -290,10 +296,7 @@
"platformNames": {
"mac": "macOS",
"windows": "Windows",
"linux": "Linux",
"macDownload": "MacOSダウンロード",
"windowsDownload": "Windowsダウンロード",
"linuxDownload": "Linuxダウンロード"
"linux": "Linux"
},
"platformDescriptions": {
"mac": "AppleMシリーズ・Intel両対応。<br />macOS 11.0以降が必要です。",
@ -481,6 +484,7 @@
"zenMods": "Zen Mods",
"releaseNotes": "リリースノート",
"getHelp": "ヘルプ",
"securtiy": "Security",
"discord": "Discord",
"uptimeStatus": "稼働状況",
"reportAnIssue": "問題を報告",

View file

@ -9,8 +9,10 @@ import { getLocale, getUI } from '~/utils/i18n'
import { icon, library } from '@fortawesome/fontawesome-svg-core'
import { faApple, faGithub, faLinux, faWindows } from '@fortawesome/free-brands-svg-icons'
import ExternalLinkIcon from '~/icons/ExternalLink.astro'
import LockIcon from '~/icons/LockIcon.astro'
import Image from 'astro/components/Image.astro'
import AppIconDark from '~/assets/app-icon-dark.png'
import AppIconLight from '~/assets/app-icon-light.png'
export { getStaticPaths } from '~/utils/i18n'
@ -24,28 +26,40 @@ library.add(faWindows, faLinux, faApple, faGithub)
const windowsIcon = icon({ prefix: 'fab', iconName: 'windows' })
const linuxIcon = icon({ prefix: 'fab', iconName: 'linux' })
const appleIcon = icon({ prefix: 'fab', iconName: 'apple' })
const githubIcon = icon({ prefix: 'fab', iconName: 'github' })
const checksums = await getChecksums()
const releases = getReleasesWithChecksums(locale)(checksums)
const platformNames = download.platformNames
const platformDescriptions = download.platformDescriptions
---
<DownloadScript />
<Layout title={layout.download.title} description={layout.download.description}>
<main class="flex min-h-screen flex-col px-6 data-[os='windows']:bg-zen-blue">
<div class="container relative mx-auto py-12">
<div class="mb-6 mt-12 flex flex-col gap-4">
<Description id="download-title" class="text-6xl font-bold">{download.title}</Description>
<Description class="max-w-xl text-pretty">
{download.description}
<main class="mt-48 flex flex-col px-6 data-[os='windows']:bg-zen-blue">
<div class="container relative mx-auto pb-48">
<div class="mb-6 flex flex-col gap-4">
<div class="download-browser-logo mx-auto">
<Image
src={AppIconDark}
alt="Zen Browser"
class="w-32 translate-y-3 transform dark:hidden"
/>
<Image
src={AppIconLight}
alt="Zen Browser"
class="hidden w-32 translate-y-3 transform dark:block"
/>
</div>
<Description id="download-title" class="mx-auto max-w-3xl text-center text-6xl font-bold"
>{download.title}</Description
>
<Description class="mx-auto text-pretty text-center">
<Fragment set:html={download.description} />
</Description>
<div
id="twilight-info"
class="hidden max-w-xl items-center justify-center gap-3 text-pretty rounded-xl border border-zen-blue/20 bg-zen-blue/5 p-4 text-left data-[twilight='true']:flex"
class="mx-auto hidden max-w-xl items-center justify-center gap-3 text-pretty rounded-xl border border-zen-blue/20 bg-zen-blue/5 p-4 text-left text-center data-[twilight='true']:flex"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -65,140 +79,72 @@ const platformDescriptions = download.platformDescriptions
</div>
</div>
<!-- Platform selector -->
<div class="mb-6 flex">
<div
class="inline-flex rounded-3xl bg-[rgba(255,255,255,0.4)] p-2 shadow-md dark:bg-[rgba(0,0,0,0.3)] dark:shadow-sm"
>
<button
class="platform-selector rounded-2xl px-5 py-2.5 transition-all duration-200 data-[active='true']:bg-subtle"
data-platform="mac"
>
<span class="flex items-center gap-2">
<Fragment set:html={appleIcon.html} />
<span>{platformNames.mac}</span>
</span>
</button>
<button
class="platform-selector rounded-2xl px-5 py-2.5 transition-all duration-200 data-[active='true']:bg-subtle"
data-platform="windows"
>
<span class="flex items-center gap-2">
<Fragment set:html={windowsIcon.html} />
<span>{platformNames.windows}</span>
</span>
</button>
<button
class="platform-selector rounded-2xl px-5 py-2.5 transition-all duration-200 data-[active='true']:bg-subtle"
data-platform="linux"
>
<span class="flex items-center gap-2">
<Fragment set:html={linuxIcon.html} />
<span>{platformNames.linux}</span>
</span>
</button>
</div>
</div>
<!-- Download Cards -->
<div
class="relative mb-16 overflow-hidden rounded-3xl bg-[rgba(255,255,255,0.4)] p-8 shadow-xl md:p-10 dark:bg-[rgba(0,0,0,0.3)]"
>
<div class="relative overflow-hidden">
<!-- MacOS Download -->
<PlatformDownload
platform="mac"
icon={appleIcon.html}
title={platformNames.macDownload}
description={platformDescriptions.mac}
releases={releases.macos}
cpu="x86_64"
/>
<PlatformDownload
platform="mac"
icon={appleIcon.html}
description={platformDescriptions.mac}
releases={releases.macos}
cpu="arm64"
/>
<!-- Windows Download -->
<PlatformDownload
platform="windows"
icon={windowsIcon.html}
title={platformNames.windowsDownload}
description={platformDescriptions.windows}
releases={releases.windows}
cpu="x86_64"
/>
<PlatformDownload
platform="windows"
icon={windowsIcon.html}
description={platformDescriptions.windows}
releases={releases.windows}
cpu="arm64"
/>
<!-- Linux Download -->
<PlatformDownload
platform="linux"
icon={linuxIcon.html}
title={platformNames.linuxDownload}
description={platformDescriptions.linux}
releases={releases.linux}
cpu="x86_64"
/>
<PlatformDownload
platform="linux"
icon={linuxIcon.html}
description={platformDescriptions.linux}
releases={releases.linux}
cpu="aarch64"
/>
</div>
<!-- Additional resources -->
<section class="mb-16">
<h2 class="mb-4 text-3xl font-semibold">
{download.additionalResources.title}
</h2>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<a
href="https://github.com/zen-browser"
class="group rounded-2xl bg-[rgba(255,255,255,0.4)] p-6 transition-all duration-300 hover:shadow-lg dark:bg-[rgba(0,0,0,0.3)]"
<div class="mt-20 flex flex-col items-center justify-center gap-2 text-center">
<div class="flex cursor-default items-center justify-center gap-3 text-sm font-bold">
{download.beta}
<span
class="leading-20 caption inline-block flex h-6 items-center rounded-md border border-dark px-2 text-xs"
>BETA</span
>
<div class="flex items-start justify-between">
<div>
<h3 class="mb-2 text-xl font-medium">
{download.additionalResources.sourceCode.title}
</h3>
<p class="text-muted-foreground">
{download.additionalResources.sourceCode.description}
</p>
</div>
<div
class="grid aspect-square h-12 w-12 place-items-center rounded-xl border border-subtle transition-all duration-100 hover:bg-coral hover:bg-opacity-10 group-hover:border-coral group-hover:border-opacity-10"
class="max-w-[55ch] cursor-default text-balance text-sm font-normal text-black/50 dark:text-white/50"
>
<div
class="text-muted-foreground transition-all duration-200 group-hover:text-coral"
>
<Fragment set:html={githubIcon.html} />
</div>
</div>
</div>
</a>
<a
href="https://docs.zen-browser.app"
class="group rounded-2xl bg-[rgba(255,255,255,0.4)] p-6 transition-all duration-200 hover:shadow-lg dark:bg-[rgba(0,0,0,0.3)]"
>
<div class="flex items-start justify-between">
<div>
<h3 class="mb-2 text-xl font-medium">
{download.additionalResources.documentation.title}
</h3>
<p class="text-muted-foreground">
{download.additionalResources.documentation.description}
</p>
</div>
<div
class="rounded-xl border border-subtle p-3 transition-all duration-100 hover:bg-coral hover:bg-opacity-10 group-hover:border-coral group-hover:border-opacity-20"
>
<ExternalLinkIcon
class="h-5 w-5 transition-all duration-200 group-hover:text-coral"
/>
</div>
</div>
</a>
</div>
</section>
<!-- Security Notice -->
<div class="grid grid-cols-[auto,1fr] gap-4 rounded-2xl bg-subtle bg-opacity-10 p-6">
<div class="h-fit rounded-xl bg-subtle p-3">
<LockIcon class="h-5 w-5" />
</div>
<div>
<h3 class="mb-2 text-lg font-medium">
{download.securityNotice.title}
</h3>
<p class="text-muted-foreground" set:html={download.securityNotice.description} />
<Fragment set:html={download.otherDownload} />
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
{
"version": "xxx",
"extra": "",
"version": "1.12.11t",
"extra": "This update includes some attempts at performance improvements, better workspace management and some bug fixes!",
"fixes": [],
"features": []
}

View file

@ -4,12 +4,6 @@ import { beforeEach, describe, expect, it } from 'vitest'
import PlatformDownload from '~/components/download/PlatformDownload.astro'
const mockIcon = ['<svg></svg>']
const mockReleases = {
universal: { label: 'Universal', link: '/universal', checksum: 'abc123' },
x86_64: { label: 'x86_64', link: '/x86_64', checksum: 'def456' },
arm64: { label: 'ARM64', link: '/arm64', checksum: 'ghi789' },
flathub: { all: { label: 'Flathub', link: '/flathub' } },
}
describe('<PlatformDownload />', () => {
let container: Awaited<ReturnType<typeof AstroContainer.create>>
@ -17,37 +11,6 @@ describe('<PlatformDownload />', () => {
container = await AstroContainer.create()
})
it('renders mac platform', async () => {
const result = await container.renderToString(PlatformDownload, {
props: {
platform: 'mac',
icon: mockIcon,
title: 'Mac Title',
description: 'Mac Desc',
releases: mockReleases,
},
})
expect(result).toContain('Mac Title')
expect(result).toContain('Mac Desc')
expect(result).toContain('Universal')
})
it('renders windows platform', async () => {
const result = await container.renderToString(PlatformDownload, {
props: {
platform: 'windows',
icon: mockIcon,
title: 'Win Title',
description: 'Win Desc',
releases: mockReleases,
},
})
expect(result).toContain('Win Title')
expect(result).toContain('Win Desc')
expect(result).toContain('x86_64')
expect(result).toContain('ARM64')
})
it('renders linux platform with flathub and tarball', async () => {
const linuxReleases = {
flathub: { all: { label: 'Flathub', link: '/flathub' } },
@ -63,16 +26,10 @@ describe('<PlatformDownload />', () => {
props: {
platform: 'linux',
icon: mockIcon,
title: 'Linux Title',
description: 'Linux Desc',
releases: linuxReleases,
},
})
expect(result).toContain('Linux Title')
expect(result).toContain('Linux Desc')
expect(result).toContain('Flathub')
expect(result).toContain('Tarball')
expect(result).toContain('x86_64')
})
it('renders linux platform with all branches', async () => {
@ -97,30 +54,11 @@ describe('<PlatformDownload />', () => {
props: {
platform: 'linux',
icon: mockIcon,
title: 'Linux Title',
description: 'Linux Desc',
releases: linuxReleases,
},
})
// Test basic content
expect(result).toContain('Linux Title')
expect(result).toContain('Linux Desc')
// Test Flathub section
expect(result).toContain('Flathub')
expect(result).toContain('/flathub')
// Test x86_64 section
expect(result).toContain('x86_64')
expect(result).toContain('Tarball x86_64')
expect(result).toContain('/tarball-x86_64')
expect(result).toContain('sha256')
// Test ARM64 section
expect(result).toContain('ARM64')
expect(result).toContain('Tarball ARM64')
expect(result).toContain('/tarball-arm64')
expect(result).toContain('sha256-arm64')
})
})

View file

@ -3,38 +3,44 @@ import { expect, test, type BrowserContextOptions, type Page } from '@playwright
import { getReleasesWithChecksums } from '~/components/download/release-data'
import { CONSTANT } from '~/constants'
// Helper to get the platform section by id
const getPlatformSection = (page: Page, platform: string) =>
page.locator(`#${platform}-downloads.platform-section[data-active='true']`)
const getPlatformSection = (page: Page, platform: string, cpu: string) => {
return page.locator(`#${platform}-${cpu}-downloads.platform-section`)
}
// Helper to get the platform tab button
const getPlatformButton = (page: Page, platform: string) =>
page.locator(`button.platform-selector[data-platform='${platform}']`)
const platformConfigs: { name: string; userAgent: string; platform: string }[] = [
const platformConfigs: {
name: string
userAgent: string
platform: string
expectedCpu: string
}[] = [
{
name: 'windows',
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
platform: 'Win32',
expectedCpu: 'x86_64',
},
{
name: 'mac',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15',
platform: 'MacIntel',
expectedCpu: 'x86_64',
},
{
name: 'linux',
userAgent:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
platform: 'Linux x86_64',
expectedCpu: 'x86_64',
},
]
test.describe('Download page default tab per platform', () => {
for (const { name, userAgent, platform } of platformConfigs) {
test(`shows correct default tab for ${name} platform`, async ({ browser }) => {
test.describe('Download page shows correct platform section per platform', () => {
for (const { name, userAgent, platform, expectedCpu } of platformConfigs) {
test(`shows correct platform section for ${name} ${expectedCpu} platform`, async ({
browser,
}) => {
const context = await browser.newContext({
userAgent,
locale: 'en-US',
@ -42,66 +48,64 @@ test.describe('Download page default tab per platform', () => {
} as BrowserContextOptions)
const page = await context.newPage()
await page.goto('/download')
await expect(getPlatformSection(page, name)).toBeVisible()
await expect(getPlatformButton(page, name)).toHaveAttribute('data-active', 'true')
// Other platforms should not be active
for (const other of platformConfigs.filter(p => p.name !== name)) {
await expect(getPlatformSection(page, other.name)).toBeHidden()
await expect(getPlatformButton(page, other.name)).not.toHaveAttribute('data-active', 'true')
await page.waitForLoadState('domcontentloaded')
await expect(getPlatformSection(page, name, expectedCpu)).toBeVisible()
for (const other of platformConfigs.filter(
p => !(p.name === name && p.expectedCpu === expectedCpu)
)) {
const otherSection = page.locator(
`#${other.name}-${other.expectedCpu}-downloads.platform-section`
)
await expect(otherSection).toBeHidden()
}
await context.close()
})
}
})
test.describe('Download page platform detection and tab switching', () => {
test('shows correct platform section and tab when switching platforms', async ({ page }) => {
await page.goto('/download')
const platforms = ['windows', 'mac', 'linux']
for (const platform of platforms) {
await getPlatformButton(page, platform).click()
await expect(getPlatformSection(page, platform)).toBeVisible()
await expect(getPlatformButton(page, platform)).toHaveAttribute('data-active', 'true')
// other platform sections should be hidden
for (const otherPlatform of platforms.filter(p => p !== platform)) {
await expect(getPlatformSection(page, otherPlatform)).toBeHidden()
await expect(getPlatformButton(page, otherPlatform)).not.toHaveAttribute(
'data-active',
'true'
)
}
}
})
})
test.describe('Download page download links', () => {
const releases = getReleasesWithChecksums('en')(CONSTANT.CHECKSUMS)
type Releases = ReturnType<ReturnType<typeof getReleasesWithChecksums>>
function getPlatformLinks(releases: Releases) {
return {
mac: [releases.macos.universal],
windows: [releases.windows.x86_64, releases.windows.arm64],
linux: [
releases.linux.x86_64.tarball,
releases.linux.aarch64.tarball,
releases.linux.flathub.all,
],
'mac-x86_64': [releases.macos.universal],
'mac-arm64': [releases.macos.universal],
'windows-x86_64': [releases.windows.x86_64],
'windows-arm64': [releases.windows.arm64],
'linux-x86_64': [releases.linux.x86_64.tarball, releases.linux.flathub.all],
'linux-aarch64': [releases.linux.aarch64.tarball, releases.linux.flathub.all],
}
}
test('all platform download links are correct', async ({ page }) => {
const platforms = ['windows', 'mac', 'linux']
const platformLinkSelectors = getPlatformLinks(releases)
await page.goto('/download')
await page.waitForLoadState('domcontentloaded')
for (const platform of platforms) {
await getPlatformButton(page, platform).click()
for (const { label, link } of platformLinkSelectors[
platform as keyof typeof platformLinkSelectors
]) {
const downloadLink = page.locator(`#${platform}-downloads .download-link[href="${link}"]`)
await expect(downloadLink).toContainText(label)
for (const [platformCpu, links] of Object.entries(platformLinkSelectors)) {
const platform = platformCpu.split('-')[0] as 'mac' | 'windows' | 'linux'
for (const { link } of links) {
const downloadLink = page.locator(
`#${platformCpu}-downloads .download-button[href="${link}"]`
)
const isFlathubLink = link.includes('flathub.org')
if (isFlathubLink) {
await expect(downloadLink).toContainText('Flathub')
} else {
await expect(downloadLink).toContainText(
`Download for ${
platform === 'mac' ? 'MacOS' : platform.charAt(0).toUpperCase() + platform.slice(1)
}`
)
}
await expect(downloadLink).toHaveAttribute('href', link)
}
}