feat(prettier): add prettier formatting

This commit is contained in:
Shintaro Jokagi 2025-05-28 13:43:44 +12:00
parent 01f4dac75d
commit 7fafa6bc69
No known key found for this signature in database
GPG key ID: 0DDF8FA44C9A0DA8
85 changed files with 5670 additions and 2788 deletions

View file

@ -1,28 +1,28 @@
import type { Linter } from "eslint";
// @ts-expect-error - no types available
import jsxA11y from "eslint-plugin-jsx-a11y";
import { astroFiles } from "./shared";
import { type Linter } from 'eslint'
import jsxA11y from 'eslint-plugin-jsx-a11y'
import { astroFiles } from './shared'
export const astroConfig: Linter.Config = {
name: "eslint/astro",
name: 'eslint/astro',
files: astroFiles,
plugins: {
"jsx-a11y": jsxA11y,
'jsx-a11y': jsxA11y,
},
rules: {
// Astro specific adjustments
"@typescript-eslint/no-unused-vars": "off", // Astro components can have unused props
"import/no-unresolved": "off",
"no-undef": "off", // Astro has global variables like Astro
'@typescript-eslint/no-unused-vars': 'off', // Astro components can have unused props
'import/no-unresolved': 'off',
'no-undef': 'off', // Astro has global variables like Astro
// A11y rules for Astro
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-has-content": "error",
"jsx-a11y/anchor-is-valid": "error",
"jsx-a11y/click-events-have-key-events": "error",
"jsx-a11y/interactive-supports-focus": "error",
"jsx-a11y/no-redundant-roles": "error",
"jsx-a11y/img-redundant-alt": "error",
"jsx-a11y/no-access-key": "error",
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/click-events-have-key-events': 'error',
'jsx-a11y/interactive-supports-focus': 'error',
'jsx-a11y/no-redundant-roles': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/no-access-key': 'error',
},
};
}

View file

@ -1,72 +1,73 @@
import type { Linter } from "eslint";
import { sharedFiles } from "./shared";
import { type Linter } from 'eslint'
import { sharedFiles } from './shared'
export const baseConfig: Linter.Config = {
name: "eslint/base",
name: 'eslint/base',
files: sharedFiles,
rules: {
"constructor-super": "error",
"for-direction": "error",
"getter-return": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-this-before-super": "error",
"no-undef": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-var": "error",
"no-with": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",
'constructor-super': 'error',
'for-direction': 'error',
'getter-return': 'error',
'no-async-promise-executor': 'error',
'no-case-declarations': 'error',
'no-class-assign': 'error',
'no-compare-neg-zero': 'error',
'no-cond-assign': 'error',
'no-const-assign': 'error',
'no-constant-binary-expression': 'error',
'no-constant-condition': 'error',
'no-control-regex': 'error',
'no-debugger': 'error',
'no-delete-var': 'error',
'no-dupe-args': 'error',
'no-dupe-class-members': 'error',
'no-dupe-else-if': 'error',
'no-dupe-keys': 'error',
'no-duplicate-case': 'error',
'no-empty-character-class': 'error',
'no-empty-pattern': 'error',
'no-empty-static-block': 'error',
'no-ex-assign': 'error',
'no-fallthrough': 'error',
'no-func-assign': 'error',
'no-global-assign': 'error',
'no-import-assign': 'error',
'no-invalid-regexp': 'error',
'no-loss-of-precision': 'error',
'no-misleading-character-class': 'error',
'no-new-native-nonconstructor': 'error',
'no-nonoctal-decimal-escape': 'error',
'no-obj-calls': 'error',
'no-octal': 'error',
'no-redeclare': 'error',
'no-regex-spaces': 'error',
'no-self-assign': 'error',
'no-setter-return': 'error',
'no-this-before-super': 'error',
'no-undef': 'error',
'no-unexpected-multiline': 'error',
'no-unreachable': 'error',
'no-unsafe-finally': 'error',
'no-unsafe-negation': 'error',
'no-unsafe-optional-chaining': 'error',
'no-unused-labels': 'error',
'no-unused-private-class-members': 'error',
'no-useless-backreference': 'error',
'no-useless-catch': 'error',
'no-var': 'error',
'no-with': 'error',
'require-yield': 'error',
'use-isnan': 'error',
'valid-typeof': 'error',
// Additional base rules
"no-console": ["warn", { allow: ["warn", "error"] }],
"prefer-const": "error",
"object-shorthand": "error",
"prefer-template": "error",
curly: ["error", "all"],
eqeqeq: ["error", "always"],
"no-implicit-coercion": "error",
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
'object-shorthand': 'error',
'prefer-template': 'error',
curly: ['error', 'all'],
eqeqeq: ['error', 'always'],
'no-implicit-coercion': 'error',
},
};
}

View file

@ -1,4 +1,5 @@
import type { Linter } from "eslint";
import { type Linter } from "eslint";
import { configFiles } from "./shared";
export const configFilesConfig: Linter.Config = {
@ -8,6 +9,7 @@ export const configFilesConfig: Linter.Config = {
"no-console": "off",
"@typescript-eslint/no-var-requires": "off",
"import/no-default-export": "off",
"import/default": "off", // Allow missing default exports in config files
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",

View file

@ -1,11 +1,12 @@
import type { Linter } from "eslint";
import { type Linter } from 'eslint'
// @ts-expect-error - no types available
import importPlugin from "eslint-plugin-import";
import { sharedFiles } from "./shared";
import importPlugin from 'eslint-plugin-import'
import { sharedFiles } from './shared'
export const importConfigArray: Linter.Config[] = [
{
name: "eslint/import",
name: 'eslint/import',
files: sharedFiles,
plugins: {
import: importPlugin,
@ -14,50 +15,50 @@ export const importConfigArray: Linter.Config[] = [
...importPlugin.configs.recommended.rules,
...importPlugin.configs.typescript.rules,
"import/order": [
"error",
'import/order': [
'error',
{
groups: [
"builtin",
"external",
"internal",
["parent", "sibling"],
"index",
"object",
"type",
'builtin',
'external',
'internal',
['parent', 'sibling'],
'index',
'object',
'type',
],
"newlines-between": "always",
'newlines-between': 'always',
alphabetize: {
order: "asc",
order: 'asc',
caseInsensitive: true,
},
pathGroups: [
{
pattern: "@/**",
group: "internal",
position: "before",
pattern: '@/**',
group: 'internal',
position: 'before',
},
],
pathGroupsExcludedImportTypes: ["builtin"],
pathGroupsExcludedImportTypes: ['builtin'],
},
],
"import/no-unresolved": "off", // TypeScript handles this
"import/no-duplicates": ["error", { "prefer-inline": true }],
"import/consistent-type-specifier-style": ["error", "prefer-inline"],
"import/first": "error",
"import/newline-after-import": "error",
"import/no-default-export": "off", // Allow default exports
'import/no-unresolved': 'off', // TypeScript handles this
'import/no-duplicates': ['error', { 'prefer-inline': true }],
'import/consistent-type-specifier-style': ['error', 'prefer-inline'],
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-default-export': 'off', // Allow default exports
},
settings: {
"import/resolver": {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
node: true,
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
},
},
];
]

View file

@ -1,12 +1,13 @@
import type { Linter } from "eslint";
import { javascriptFiles } from "./shared";
import { type Linter } from 'eslint'
import { javascriptFiles } from './shared'
export const javascriptConfig: Linter.Config = {
name: "eslint/javascript",
name: 'eslint/javascript',
files: javascriptFiles,
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions: {
ecmaFeatures: {
jsx: true,
@ -14,22 +15,22 @@ export const javascriptConfig: Linter.Config = {
},
},
rules: {
"no-unused-vars": [
"error",
'no-unused-vars': [
'error',
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
"no-console": ["warn", { allow: ["warn", "error"] }],
"no-debugger": "error",
"prefer-const": "error",
"no-var": "error",
"object-shorthand": "error",
"prefer-template": "error",
curly: ["error", "all"],
eqeqeq: ["error", "always"],
"no-implicit-coercion": "error",
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'error',
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'error',
'prefer-template': 'error',
curly: ['error', 'all'],
eqeqeq: ['error', 'always'],
'no-implicit-coercion': 'error',
},
};
}

View file

@ -1,31 +1,31 @@
import type { Linter } from "eslint";
// @ts-expect-error - no types available
import jsxA11y from "eslint-plugin-jsx-a11y";
import { astroFiles, javascriptFiles, typescriptFiles } from "./shared";
import { type Linter } from 'eslint'
import jsxA11y from 'eslint-plugin-jsx-a11y'
import { astroFiles, javascriptFiles, typescriptFiles } from './shared'
export const jsxA11yConfig: Linter.Config = {
name: "eslint/jsx-a11y",
name: 'eslint/jsx-a11y',
files: [
...astroFiles,
...typescriptFiles.filter((f) => f.includes("tsx")),
...javascriptFiles.filter((f) => f.includes("jsx")),
...typescriptFiles.filter(f => f.includes('tsx')),
...javascriptFiles.filter(f => f.includes('jsx')),
],
plugins: {
"jsx-a11y": jsxA11y,
'jsx-a11y': jsxA11y,
},
rules: {
...jsxA11y.configs.recommended.rules,
// Additional a11y rules
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-has-content": "error",
"jsx-a11y/anchor-is-valid": "error",
"jsx-a11y/click-events-have-key-events": "error",
"jsx-a11y/interactive-supports-focus": "error",
"jsx-a11y/no-redundant-roles": "error",
"jsx-a11y/img-redundant-alt": "error",
"jsx-a11y/no-access-key": "error",
"jsx-a11y/label-has-associated-control": "error",
"jsx-a11y/no-autofocus": "warn",
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/click-events-have-key-events': 'error',
'jsx-a11y/interactive-supports-focus': 'error',
'jsx-a11y/no-redundant-roles': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/no-access-key': 'error',
'jsx-a11y/label-has-associated-control': 'error',
'jsx-a11y/no-autofocus': 'warn',
},
};
}

View file

@ -1,5 +1,7 @@
import type { Linter } from "eslint";
import { type Linter } from "eslint";
import react from "eslint-plugin-react";
import * as reactHooks from "eslint-plugin-react-hooks";
import { javascriptFiles, typescriptFiles } from "./shared";
export const reactConfig: Linter.Config = {
@ -14,7 +16,7 @@ export const reactConfig: Linter.Config = {
rules: {
...react.configs.recommended.rules,
"react/react-in-jsx-scope": "off", // Not needed in modern React/Preact
"react/react-in-jsx-scope": "off", // Not needed in React 17+
"react/prop-types": "off", // Using TypeScript
"react/jsx-uses-react": "off",
"react/jsx-uses-vars": "error",
@ -37,13 +39,11 @@ export const reactConfig: Linter.Config = {
},
settings: {
react: {
version: "detect",
version: "18.2", // React version
},
},
};
import reactHooks from "eslint-plugin-react-hooks";
export const reactHooksConfig: Linter.Config = {
name: "eslint/react-hooks",
files: [

View file

@ -1,41 +1,35 @@
export const sharedFiles = [
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.ts",
"**/*.cts",
"**/*.mts",
"**/*.tsx",
"**/*.d.ts",
];
'**/*.js',
'**/*.cjs',
'**/*.mjs',
'**/*.jsx',
'**/*.ts',
'**/*.cts',
'**/*.mts',
'**/*.tsx',
'**/*.d.ts',
]
export const sharedTestFiles = [
"**/*.test.{ts,tsx,js,jsx}",
"**/*.spec.{ts,tsx,js,jsx}",
"**/tests/**/*.{ts,tsx,js,jsx}",
"**/__tests__/**/*.{ts,tsx,js,jsx}",
];
'**/*.test.{ts,tsx,js,jsx}',
'**/*.spec.{ts,tsx,js,jsx}',
'**/tests/**/*.{ts,tsx,js,jsx}',
'**/__tests__/**/*.{ts,tsx,js,jsx}',
]
export const astroFiles = ["**/*.astro"];
export const astroFiles = ['**/*.astro']
export const typescriptFiles = [
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.cts",
"**/*.d.ts",
];
export const typescriptFiles = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '**/*.d.ts']
export const javascriptFiles = ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"];
export const javascriptFiles = ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs']
export const configFiles = [
"*.config.{ts,js,mjs}",
"**/*.config.{ts,js,mjs}",
"**/vite.config.*",
"**/vitest.config.*",
"**/playwright.config.*",
"**/astro.config.*",
"**/tailwind.config.*",
"**/eslint.config.*",
];
'*.config.{ts,js,mjs}',
'**/*.config.{ts,js,mjs}',
'**/vite.config.*',
'**/vitest.config.*',
'**/playwright.config.*',
'**/astro.config.*',
'**/tailwind.config.*',
'**/eslint.config.*',
]

View file

@ -1,19 +1,20 @@
import type { Linter } from "eslint";
import { sharedTestFiles } from "./shared";
import { type Linter } from 'eslint'
import { sharedTestFiles } from './shared'
export const testConfig: Linter.Config = {
name: "eslint/test",
name: 'eslint/test',
files: sharedTestFiles,
rules: {
"no-console": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/unbound-method": "off",
"import/no-extraneous-dependencies": "off",
'no-console': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/unbound-method': 'off',
'import/no-extraneous-dependencies': 'off',
},
};
}

View file

@ -1,14 +1,15 @@
import type { Linter } from "eslint";
import { typescriptFiles } from "./shared";
import { type Linter } from 'eslint'
import { typescriptFiles } from './shared'
export function createTypescriptConfig(tsConfigPath: string): Linter.Config {
return {
name: "eslint/typescript",
name: 'eslint/typescript',
files: typescriptFiles,
languageOptions: {
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
@ -18,27 +19,27 @@ export function createTypescriptConfig(tsConfigPath: string): Linter.Config {
},
rules: {
// Basic TypeScript rules that work without type information
"@typescript-eslint/no-unused-vars": [
"error",
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/consistent-type-imports": [
"error",
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
// Override base rules for TypeScript
"no-unused-vars": "off", // Handled by TypeScript
"no-undef": "off", // TypeScript handles this
'no-unused-vars': 'off', // Handled by TypeScript
'no-undef': 'off', // TypeScript handles this
},
};
}
}

View file

@ -23,7 +23,7 @@ jobs:
- name: Install dependencies
run: npm ci
biome:
format:
runs-on: ubuntu-latest
needs: setup
steps:
@ -40,7 +40,7 @@ jobs:
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
- name: Run Biome check
run: npx biome check ./src
run: npm run lint && npm run format:check
vitest:
runs-on: ubuntu-latest

View file

@ -9,8 +9,13 @@ Zen Browser Website
[![Status badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fuptime.zen-browser.app%2Fshield-badges%2Fstatus.json&style=for-the-badge)](https://uptime.zen-browser.app)
This repository contains the source code for the Zen Browser Website. We are thrilled to welcome you to our community. Before you start, please read this document to understand how you can contribute to this project.
This repository contains the source code for the Zen Browser Website. We are
thrilled to welcome you to our community. Before you start, please read this
document to understand how you can contribute to this project.
Zen Browser's website is built with [Astro.js](https://astro.build/), [TypeScript](https://www.typescriptlang.org/), and [Tailwind CSS](https://tailwindcss.com/).
Zen Browser's website is built with [Astro.js](https://astro.build/),
[TypeScript](https://www.typescriptlang.org/), and
[Tailwind CSS](https://tailwindcss.com/).
If you are interested in contributing to this project, please read the [Contributing Guidelines](https://docs.zen-browser.app/contribute/www).
If you are interested in contributing to this project, please read the
[Contributing Guidelines](https://docs.zen-browser.app/contribute/www).

View file

@ -1,21 +1,21 @@
import tailwind from '@astrojs/tailwind'
import tailwind from "@astrojs/tailwind";
// @ts-check
import { defineConfig } from 'astro/config'
import { defineConfig } from "astro/config";
import preact from '@astrojs/preact'
import react from "@astrojs/react";
import sitemap from '@astrojs/sitemap'
import sitemap from "@astrojs/sitemap";
// https://astro.build/config
export default defineConfig({
integrations: [tailwind(), preact({ compat: true }), sitemap()],
site: 'https://zen-browser.app',
integrations: [tailwind(), react(), sitemap()],
site: "https://zen-browser.app",
i18n: {
defaultLocale: 'en',
locales: ['en'],
defaultLocale: "en",
locales: ["en"],
routing: {
fallbackType: 'rewrite',
fallbackType: "rewrite",
prefixDefaultLocale: false,
},
},
})
});

View file

@ -1,10 +1,11 @@
import { includeIgnoreFile } from "@eslint/compat";
import type { TSESLint } from "@typescript-eslint/utils";
import prettierConfig from "eslint-config-prettier";
import astro from "eslint-plugin-astro";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import tseslint from "typescript-eslint";
import { includeIgnoreFile } from "@eslint/compat";
import { type TSESLint } from "@typescript-eslint/utils";
import prettierConfig from "eslint-config-prettier";
import astro from "eslint-plugin-astro";
import tseslint, { configs } from "typescript-eslint";
// Import modular configurations
import { astroConfig } from "./.eslint/astro";
@ -63,8 +64,8 @@ const config: TSESLint.FlatConfig.ConfigArray = tseslint.config(
ignoresConfig,
baseConfig,
// TypeScript ecosystem
...tseslint.configs.strict,
...tseslint.configs.stylistic,
...configs.strict,
...configs.stylistic,
createTypescriptConfig(tsConfigPath),
// Import management

5372
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,12 +9,10 @@
"preview": "astro preview --port 3000",
"wrangler": "wrangler",
"astro": "astro",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.astro",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx,.astro --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"biome:lint": "biome lint ./src",
"biome:format": "biome format ./src",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.astro",
"lint:fix": "eslint ./src --ext .js,.jsx,.ts,.tsx,.astro --fix",
"format": "prettier --write ./src",
"format:check": "prettier --check ./src",
"prepare": "husky",
"test": "npx vitest run",
"test:coverage": "npx vitest --coverage",
@ -23,7 +21,7 @@
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/cloudflare": "^12.5.2",
"@astrojs/preact": "^4.0.11",
"@astrojs/react": "^4.3.0",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.3.1",
"@astrojs/tailwind": "^6.0.2",
@ -31,18 +29,20 @@
"@fortawesome/fontawesome-svg-core": "^6.7.1",
"@fortawesome/free-brands-svg-icons": "^6.7.1",
"@fortawesome/free-solid-svg-icons": "^6.7.1",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"astro": "^5.7.10",
"astro-navbar": "^2.3.7",
"autoprefixer": "10.4.14",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"free-astro-components": "^1.2.0",
"jiti": "^2.4.2",
"lucide-astro": "^0.460.0",
"lucide-react": "^0.475.0",
"motion": "^11.13.5",
"postcss": "^8.5.1",
"preact": "^10.26.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"sharp": "^0.33.5",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^3.4.15",
@ -56,6 +56,7 @@
"@playwright/test": "^1.52.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.6.1",
"@types/eslint-plugin-jsx-a11y": "^6.10.0",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.15.18",
"@typescript-eslint/eslint-plugin": "^8.33.0",
@ -71,10 +72,11 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"husky": "^9.1.7",
"jiti": "^2.4.2",
"jsdom": "^26.1.0",
"lint-staged": "^15.2.7",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"prettier": "3.5.3",
"prettier-plugin-astro": "0.14.1",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript-eslint": "^8.33.0",
"vite-tsconfig-paths": "^5.1.4",

View file

@ -15,7 +15,7 @@ export default defineConfig({
testDir: './src/tests',
testIgnore: ['**.test.ts'],
fullyParallel: true,
forbidOnly: !!process.env.CI,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,

View file

@ -7,7 +7,7 @@ export default {
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
singleQuote: false,
quoteProps: "as-needed",
trailingComma: "es5",
bracketSpacing: true,

View file

@ -1,13 +1,13 @@
export function getTitleAnimation(delay = 0, duration = 0.3, once = true) {
return {
initial: { opacity: 0.001, translateY: 20, filter: 'blur(4px)' },
initial: { opacity: 0.001, translateY: 20, filter: "blur(4px)" },
whileInView: {
opacity: 1,
translateY: 0,
filter: 'blur(0px)',
filter: "blur(0px)",
transition: { duration, delay },
},
viewport: { once: once },
viewport: { once },
}
}

View file

@ -1,6 +1,6 @@
---
import ArrowLeftIcon from '~/icons/ArrowLeftIcon.astro'
import { getLocale, getUI } from '~/utils/i18n'
import ArrowLeftIcon from "~/icons/ArrowLeftIcon.astro"
import { getLocale, getUI } from "~/utils/i18n"
const locale = getLocale(Astro)
@ -11,10 +11,7 @@ const {
} = getUI(locale)
---
<button
onclick="window.history.back()"
class="mb-8 flex w-min items-center gap-2"
>
<button onclick="window.history.back()" class="mb-8 flex w-min items-center gap-2">
<ArrowLeftIcon class="size-4" />
{slug.back}
</button>

View file

@ -1,5 +1,5 @@
---
import { getLocale, getPath } from '~/utils/i18n'
import { getLocale, getPath } from "~/utils/i18n"
const locale = getLocale(Astro)
const getLocalePath = getPath(locale)
@ -13,15 +13,15 @@ const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = As
{...extra}
href={getLocalePath(href)}
class:list={[
'transition-bg flex items-center justify-center gap-2 rounded-xl px-6 py-4 transition-transform duration-150 hover:scale-[1.02] active:scale-[0.98]',
"transition-bg flex items-center justify-center gap-2 rounded-xl px-6 py-4 transition-transform duration-150 hover:scale-[1.02] active:scale-[0.98]",
className,
isPrimary
? 'border-dark bg-dark text-paper shadow-lg'
? "border-dark bg-dark text-paper shadow-lg"
: isAlert
? 'bg-red-300 text-dark'
? "bg-red-300 text-dark"
: !isBordered
? 'bg-subtle'
: '!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm',
? "bg-subtle"
: "!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm",
]}
>
<slot />
@ -31,15 +31,15 @@ const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = As
id={id}
{...extra}
class:list={[
'transition-bg flex items-center justify-center gap-2 rounded-lg px-6 py-3 transition-transform duration-150 hover:scale-[1.02]',
"transition-bg flex items-center justify-center gap-2 rounded-lg px-6 py-3 transition-transform duration-150 hover:scale-[1.02]",
className,
isPrimary
? 'border-dark bg-dark text-paper shadow-md'
? "border-dark bg-dark text-paper shadow-md"
: isAlert
? 'bg-red-300 text-dark'
? "bg-red-300 text-dark"
: !isBordered
? ''
: '!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm',
? ""
: "!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm",
]}
>
<slot />

View file

@ -4,17 +4,14 @@ const sizes = [216, 396, 576, 756]
const borderWidths = [20, 30, 40, 50]
---
<div
id="circles"
class:list={['pointer-events-none inset-0 overflow-hidden', classList]}
>
<div id="circles" class:list={["pointer-events-none inset-0 overflow-hidden", classList]}>
<div class="mx-auto opacity-10 lg:opacity-100">
{
[...Array(4)].map((_, i) => (
<div
class:list={[
'absolute -translate-x-1/2 -translate-y-1/2 rounded-full',
white ? 'border-paper' : 'border-coral',
"absolute -translate-x-1/2 -translate-y-1/2 rounded-full",
white ? "border-paper" : "border-coral",
]}
style={{
width: `${multiplier * sizes[i]}px`,

View file

@ -1,13 +1,13 @@
---
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'
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"
const locale = getLocale(Astro)
@ -33,33 +33,21 @@ const {
{community.title[2]}
</motion.span>
</Description>
<motion.p
client:load
{...getTitleAnimation(0.6)}
className="lg:w-1/2 lg:px-0"
>
<motion.p client:load {...getTitleAnimation(0.6)} className="lg:w-1/2 lg:px-0">
{community.description}
</motion.p>
<div class="flex w-full flex-wrap gap-3 sm:gap-10 md:justify-center">
<motion.span client:load {...getTitleAnimation(0.8)}>
<Button class:list={['px-4']} href="https://github.com/zen-browser">
<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"
>
<motion.div client:load {...getTitleAnimation(1)} className="flex items-center gap-4">
<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"
>
<motion.div client:load {...getTitleAnimation(1.2)} className="flex items-center gap-4">
<CheckIcon class="size-4" />
<span>{community.lists.privateAndAlwaysUpToDate.title}</span>
</motion.div>

View file

@ -1,15 +1,15 @@
---
import { motion } from 'motion/react'
import { getTitleAnimation } from '~/animations'
import Description from '~/components/Description.astro'
import { motion } from "motion/react"
import { getTitleAnimation } from "~/animations"
import Description from "~/components/Description.astro"
import CompactModeVideo from '~/assets/CompactMode.webm'
import GlanceVideo from '~/assets/Glance.webm'
import SplitViewsVideo from '~/assets/SplitViews.webm'
import WorkspacesVideo from '~/assets/Workspaces.webm'
import CompactModeVideo from "~/assets/CompactMode.webm"
import GlanceVideo from "~/assets/Glance.webm"
import SplitViewsVideo from "~/assets/SplitViews.webm"
import WorkspacesVideo from "~/assets/Workspaces.webm"
import { getLocale, getUI } from '~/utils/i18n'
import Video from './Video.astro'
import { getLocale, getUI } from "~/utils/i18n"
import Video from "./Video.astro"
const locale = getLocale(Astro)
@ -21,14 +21,11 @@ const {
const { title1 = features.title1, title2 = features.title2, title3 = features.title3 } = Astro.props
const descriptions = Object.values(features.featureTabs).map((tab) => tab.description)
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"
>
<Description class="mb-2 text-4xl sm:text-6xl font-bold">
<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">
<motion.span client:load {...getTitleAnimation(0.2)}>
{title1}
</motion.span>
@ -49,7 +46,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
<motion.button
client:load
{...getTitleAnimation()}
class="feature-tab whitespace-nowrap"
className="feature-tab whitespace-nowrap"
data-active="true"
>
{features.featureTabs.workspaces.title}
@ -57,21 +54,21 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
<motion.button
client:load
{...getTitleAnimation(0.2)}
class="feature-tab whitespace-nowrap"
className="feature-tab whitespace-nowrap"
>
{features.featureTabs.compactMode.title}
</motion.button>
<motion.button
client:load
{...getTitleAnimation(0.4)}
class="feature-tab whitespace-nowrap"
className="feature-tab whitespace-nowrap"
>
{features.featureTabs.glance.title}
</motion.button>
<motion.button
client:load
{...getTitleAnimation(0.6)}
class="feature-tab whitespace-nowrap"
className="feature-tab whitespace-nowrap"
>
{features.featureTabs.splitView.title}
</motion.button>
@ -79,12 +76,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
<!-- 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"
>
<motion.div client:load {...getTitleAnimation(0.8)} className="feature" data-active="true">
<Description class="text-2xl font-bold">
{features.featureTabs.workspaces.title}
</Description>
@ -119,14 +111,10 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
</div>
<!-- Mobile description -->
<div
class="feature-description mt-4 lg:hidden"
data-descriptions={descriptions}
>
</div>
<div class="feature-description mt-4 lg:hidden" data-descriptions={descriptions}></div>
</div>
<div class="sticky top-6 w-full lg:w-3/5 h-fit">
<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">
<Video
@ -173,25 +161,17 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
</section>
<script>
const features = document.querySelectorAll(
'.feature, .feature-tab',
) as NodeListOf<HTMLElement>
const features = document.querySelectorAll(".feature, .feature-tab") as NodeListOf<HTMLElement>
// Set initial description
const descriptionEl = document.querySelector(
'.feature-description',
) as HTMLDivElement
const descriptions = descriptionEl?.dataset.descriptions?.split(',')
const descriptionEl = document.querySelector(".feature-description") as HTMLDivElement
const descriptions = descriptionEl?.dataset.descriptions?.split(",")
if (descriptionEl && descriptions) {
descriptionEl.textContent = descriptions[0]
}
function changeToFeature({
target,
}: {
target: HTMLElement | undefined | null
}) {
target = target?.closest('.feature, .feature-tab')
function changeToFeature({ target }: { target: HTMLElement | undefined | null }) {
target = target?.closest(".feature, .feature-tab")
if (!target) return
const index = Array.from(features).indexOf(target) % 4
@ -200,21 +180,19 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
// Update both mobile and desktop elements
features.forEach((f, i) => {
if (i % 4 === index) {
f.setAttribute('data-active', 'true')
f.setAttribute("data-active", "true")
} else {
f.removeAttribute('data-active')
f.removeAttribute("data-active")
}
})
// Update mobile description
const descriptionEl = document.querySelector('.feature-description')
const descriptionEl = document.querySelector(".feature-description")
if (descriptionEl && descriptions) {
descriptionEl.textContent = descriptions[index]
}
const videos = document.querySelectorAll(
'.feature-video',
) as NodeListOf<HTMLVideoElement>
const videos = document.querySelectorAll(".feature-video") as NodeListOf<HTMLVideoElement>
videos.forEach((vid, i) => {
const yOffset = (i - index) * 20
const zOffset = i === index ? 0 : -100 - Math.abs(i - index) * 50
@ -222,14 +200,14 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
const rotation = (i - index) * 3
if (i === index) {
vid.setAttribute('data-active', 'true')
vid.style.opacity = '1'
vid.setAttribute("data-active", "true")
vid.style.opacity = "1"
vid.style.transform = `translate3d(-50%, 0, 0) scale(${scale})`
vid.style.zIndex = '10'
vid.style.zIndex = "10"
vid.currentTime = 0
vid.play()
} else {
vid.removeAttribute('data-active')
vid.removeAttribute("data-active")
vid.style.transform = `translate3d(-50%, ${yOffset}px, ${zOffset}px)
rotate3d(1, 0, 0, ${rotation}deg)
scale(${scale})`
@ -240,9 +218,11 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
}
for (const feature of features) {
feature.addEventListener('click', changeToFeature as any)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
feature.addEventListener("click", changeToFeature as any)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
changeToFeature({ target: features[0] as any })
</script>
@ -251,7 +231,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
@apply w-full cursor-pointer select-none rounded-lg p-4 opacity-0 hover:bg-subtle;
transition: background 0.2s ease-in-out;
&[data-active='true'] {
&[data-active="true"] {
@apply bg-subtle;
}
}
@ -260,7 +240,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
@apply rounded-lg px-4 py-2 text-lg font-medium opacity-0 hover:bg-subtle;
transition: background 0.2s ease-in-out;
&[data-active='true'] {
&[data-active="true"] {
@apply bg-subtle;
}
}
@ -299,7 +279,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
display: none;
object-fit: cover;
&[data-active='true'] {
&[data-active="true"] {
display: block;
opacity: 1;
}

View file

@ -1,10 +1,10 @@
---
import Button from '~/components/Button.astro'
import Circles from '~/components/Circles.astro'
import Description from '~/components/Description.astro'
import SocialMediaStrip from '~/components/SocialMediaStrip.astro'
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
import { getLocale, getPath, getUI } from '~/utils/i18n'
import Button from "~/components/Button.astro"
import Circles from "~/components/Circles.astro"
import Description from "~/components/Description.astro"
import SocialMediaStrip from "~/components/SocialMediaStrip.astro"
import ArrowRightIcon from "~/icons/ArrowRightIcon.astro"
import { getLocale, getPath, getUI } from "~/utils/i18n"
const locale = getLocale(Astro)
const getLocalePath = getPath(locale)
@ -19,13 +19,8 @@ const {
role="contentinfo"
aria-label="Site footer"
>
<div
class="container flex w-full flex-col items-start justify-between gap-12"
>
<section
class="w-full text-center lg:w-1/2 lg:text-left"
aria-labelledby="footer-title"
>
<div class="container flex w-full flex-col items-start justify-between gap-12">
<section class="w-full text-center lg:w-1/2 lg:text-left" aria-labelledby="footer-title">
<Description id="footer-title" class="text-6xl font-bold !text-paper"
>{footer.title}</Description
>
@ -48,12 +43,8 @@ const {
class="grid w-full max-w-5xl place-items-center gap-12 text-center sm:text-left"
aria-label="Footer navigation and links"
>
<div
class="grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3 lg:w-full"
>
<div
class="grid gap-8 sm:col-span-2 sm:grid-cols-2 md:col-span-1 md:grid-cols-1"
>
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3 lg:w-full">
<div class="grid gap-8 sm:col-span-2 sm:grid-cols-2 md:col-span-1 md:grid-cols-1">
<section
class="flex flex-col items-center gap-2 sm:items-start"
aria-labelledby="follow-us-heading"
@ -63,22 +54,19 @@ const {
</h2>
<SocialMediaStrip />
</section>
<section
class="flex flex-col gap-2"
aria-labelledby="about-us-heading"
>
<section class="flex flex-col gap-2" aria-labelledby="about-us-heading">
<h2 id="about-us-heading" class="text-base !font-semibold">
{footer.aboutUs}
</h2>
<nav aria-label="About navigation">
<ul class="grid gap-2 opacity-80">
<li>
<a href={getLocalePath('/about')} class="font-normal"
<a href={getLocalePath("/about")} class="font-normal"
>{footer.teamAndContributors}</a
>
</li>
<li>
<a href={getLocalePath('/privacy-policy')} class="font-normal"
<a href={getLocalePath("/privacy-policy")} class="font-normal"
>{footer.privacyPolicy}</a
>
</li>
@ -92,23 +80,17 @@ const {
</h2>
<ul class="grid gap-2 opacity-80">
<li>
<a href="https://docs.zen-browser.app/" class="font-normal"
>{footer.documentation}</a
<a href="https://docs.zen-browser.app/" class="font-normal">{footer.documentation}</a>
</li>
<li>
<a href={getLocalePath("/mods")} class="font-normal">{footer.zenMods}</a>
</li>
<li>
<a href={getLocalePath("/release-notes")} class="font-normal">{footer.releaseNotes}</a
>
</li>
<li>
<a href={getLocalePath('/mods')} class="font-normal"
>{footer.zenMods}</a
>
</li>
<li>
<a href={getLocalePath('/release-notes')} class="font-normal"
>{footer.releaseNotes}</a
>
</li>
<li>
<a href={getLocalePath('/download?twilight')} class="font-normal"
>{footer.twilight}</a
<a href={getLocalePath("/download?twilight")} class="font-normal">{footer.twilight}</a
>
</li>
</ul>
@ -119,19 +101,15 @@ const {
</h2>
<ul class="grid gap-2 opacity-80">
<li>
<a href="https://discord.gg/zen-browser" class="font-normal"
>{footer.discord}</a
<a href="https://discord.gg/zen-browser" class="font-normal">{footer.discord}</a>
</li>
<li>
<a href="https://uptime.zen-browser.app/" class="font-normal">{footer.uptimeStatus}</a
>
</li>
<li>
<a href="https://uptime.zen-browser.app/" class="font-normal"
>{footer.uptimeStatus}</a
>
</li>
<li>
<a
href="https://github.com/zen-browser/desktop/issues/new/choose"
class="font-normal">{footer.reportAnIssue}</a
<a href="https://github.com/zen-browser/desktop/issues/new/choose" class="font-normal"
>{footer.reportAnIssue}</a
>
</li>
</ul>
@ -144,15 +122,11 @@ const {
>
<p
class="flex justify-center gap-2 lg:justify-start"
set:html={footer.madeWith.replace('{link}', getLocalePath('/about'))}
set:html={footer.madeWith.replace("{link}", getLocalePath("/about"))}
/>
</section>
<section class="absolute bottom-0 right-0">
<Circles
white
multiplier={0.7}
class="mb-[-100px] ml-auto mr-[-80px] hidden lg:block"
/>
<Circles white multiplier={0.7} class="mb-[-100px] ml-auto mr-[-80px] hidden lg:block" />
</section>
</div>
</footer>

View file

@ -1,14 +1,14 @@
---
import { motion } from 'motion/react'
import { getTitleAnimation } from '~/animations'
import HomePageVideo from '~/assets/HomePageVideo.webm'
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'
import { motion } from "motion/react"
import { getTitleAnimation } from "~/animations"
import HomePageVideo from "~/assets/HomePageVideo.webm"
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() {
@ -36,9 +36,7 @@ const {
class="flex w-full flex-col items-center gap-[20%] py-32 text-center lg:gap-[25%]"
>
<div class="flex h-full flex-col items-center justify-center">
<Title
class="relative px-12 text-center font-normal leading-8 md:text-7xl lg:px-0 lg:text-9xl"
>
<Title class="relative px-12 text-center font-normal leading-8 md:text-7xl lg:px-0 lg:text-9xl">
<motion.span client:load {...getHeroTitleAnimation()}>
{hero.title[0]}
</motion.span>
@ -49,11 +47,7 @@ const {
<motion.span client:load {...getHeroTitleAnimation()}>
{hero.title[2]}
</motion.span>
<motion.span
client:load
{...getHeroTitleAnimation()}
className="italic text-coral"
>
<motion.span client:load {...getHeroTitleAnimation()} className="italic text-coral">
{hero.title[3]}
</motion.span>
<motion.span client:load {...getHeroTitleAnimation()}>
@ -81,7 +75,7 @@ const {
<motion.span
client:load
{...getHeroTitleAnimation()}
class="mx-auto translate-y-16 !transform"
className="mx-auto translate-y-16 !transform"
>
<SocialMediaStrip />
</motion.span>

View file

@ -1,5 +1,5 @@
---
import { getLocale, getPath, getUI } from '~/utils/i18n'
import { getLocale, getPath, getUI } from "~/utils/i18n"
const locale = getLocale(Astro)
const getLocalePath = getPath(locale)
@ -11,12 +11,7 @@ const {
---
<!-- Hidden checkbox for menu toggle -->
<input
type="checkbox"
id="mobile-menu-toggle"
class="peer sr-only lg:hidden"
aria-hidden="true"
/>
<input type="checkbox" id="mobile-menu-toggle" class="peer sr-only lg:hidden" aria-hidden="true" />
<!-- Mobile Slide Menu -->
<div
@ -25,17 +20,16 @@ const {
>
<div class="flex items-center justify-between border-b border-dark px-4 py-2">
<div class="text-lg font-bold">{menu.menu}</div>
<label
for="mobile-menu-toggle"
class="cursor-pointer p-2 text-dark"
aria-label="Close menu"
>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label for="mobile-menu-toggle" class="cursor-pointer p-2 text-dark">
<span class="sr-only">Close menu</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
@ -52,21 +46,18 @@ const {
<div class="mb-2 font-bold">{menu.gettingStarted}</div>
<ul class="ml-4 space-y-2">
<li>
<a
href={getLocalePath('/mods')}
class="block text-dark hover:text-coral">{menu.zenMods}</a
<a href={getLocalePath("/mods")} class="block text-dark hover:text-coral"
>{menu.zenMods}</a
>
</li>
<li>
<a
href={getLocalePath('/release-notes')}
class="block text-dark hover:text-coral">{menu.releaseNotes}</a
<a href={getLocalePath("/release-notes")} class="block text-dark hover:text-coral"
>{menu.releaseNotes}</a
>
</li>
<li>
<a
href="https://discord.gg/zen-browser"
class="block text-dark hover:text-coral">{menu.discord}</a
<a href="https://discord.gg/zen-browser" class="block text-dark hover:text-coral"
>{menu.discord}</a
>
</li>
</ul>
@ -76,21 +67,18 @@ const {
<div class="mb-2 font-bold">{menu.usefulLinks}</div>
<ul class="ml-4 space-y-2">
<li>
<a
href={getLocalePath('/donate')}
class="block text-dark hover:text-coral">{menu.donate}</a
<a href={getLocalePath("/donate")} class="block text-dark hover:text-coral"
>{menu.donate}</a
>
</li>
<li>
<a
href={getLocalePath('/about')}
class="block text-dark hover:text-coral">{menu.aboutUs}</a
<a href={getLocalePath("/about")} class="block text-dark hover:text-coral"
>{menu.aboutUs}</a
>
</li>
<li>
<a
href="https://docs.zen-browser.app"
class="block text-dark hover:text-coral">{menu.documentation}</a
<a href="https://docs.zen-browser.app" class="block text-dark hover:text-coral"
>{menu.documentation}</a
>
</li>
<li>
@ -104,15 +92,13 @@ const {
</li>
<!-- Extra Links -->
<li>
<a
href={getLocalePath('/mods')}
class="block font-bold text-dark hover:text-coral">{menu.mods}</a
<a href={getLocalePath("/mods")} class="block font-bold text-dark hover:text-coral"
>{menu.mods}</a
>
</li>
<li>
<a
href={getLocalePath('/download')}
class="block font-bold text-dark hover:text-coral">{menu.download}</a
<a href={getLocalePath("/download")} class="block font-bold text-dark hover:text-coral"
>{menu.download}</a
>
</li>
</ul>
@ -120,6 +106,7 @@ const {
</div>
<!-- Overlay for Mobile Menu -->
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label
for="mobile-menu-toggle"
class="pointer-events-none fixed inset-0 z-30 bg-black opacity-0 transition-opacity duration-300 peer-checked:pointer-events-auto peer-checked:opacity-50"

View file

@ -1,24 +1,25 @@
import { icon, library } from '@fortawesome/fontawesome-svg-core'
import { faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'
import { useEffect, useState } from 'preact/hooks'
import { useModsSearch } from '~/hooks/useModsSearch'
import type { ZenTheme } from '~/mods'
import { type Locale, getUI } from '~/utils/i18n'
import { icon, library } from "@fortawesome/fontawesome-svg-core"
import { faSort, faSortDown, faSortUp } from "@fortawesome/free-solid-svg-icons"
import { useEffect, useState, type FormEvent } from "react"
import { useModsSearch } from "~/hooks/useModsSearch"
import { type ZenTheme } from "~/mods"
import { getUI, type Locale } from "~/utils/i18n"
// Add icons to the library
library.add(faSort, faSortUp, faSortDown)
// Create icon objects
const defaultSortIcon = icon({ prefix: 'fas', iconName: 'sort' })
const ascSortIcon = icon({ prefix: 'fas', iconName: 'sort-up' })
const descSortIcon = icon({ prefix: 'fas', iconName: 'sort-down' })
const defaultSortIcon = icon({ prefix: "fas", iconName: "sort" })
const ascSortIcon = icon({ prefix: "fas", iconName: "sort-up" })
const descSortIcon = icon({ prefix: "fas", iconName: "sort-down" })
interface ModsListProps {
type ModsListProps = {
allMods: ZenTheme[]
locale: Locale
}
export default function ModsList({ allMods, locale }: ModsListProps) {
const ModsList = ({ allMods, locale }: ModsListProps) => {
const {
search,
createdSort,
@ -43,23 +44,23 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
setPageInput(page.toString())
}, [page])
function getSortIcon(state: 'default' | 'asc' | 'desc') {
if (state === 'asc') return ascSortIcon
if (state === 'desc') return descSortIcon
function getSortIcon(state: "default" | "asc" | "desc") {
if (state === "asc") return ascSortIcon
if (state === "desc") return descSortIcon
return defaultSortIcon
}
function handleSearch(e: Event) {
function handleSearch(e: FormEvent<HTMLInputElement>) {
const target = e.target as HTMLInputElement
setSearch(target.value)
}
function handleLimitChange(e: Event) {
function handleLimitChange(e: FormEvent<HTMLSelectElement>) {
const target = e.target as HTMLSelectElement
setLimit(Number.parseInt(target.value, 10))
}
function handlePageSubmit(e: Event) {
function handlePageSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
const newPage = Number.parseInt(pageInput, 10)
if (!Number.isNaN(newPage) && newPage >= 1 && newPage <= totalPages) {
@ -70,7 +71,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
}
}
function handlePageInputChange(e: Event) {
function handlePageInputChange(e: FormEvent<HTMLInputElement>) {
const target = e.target as HTMLInputElement
setPageInput(target.value)
}
@ -89,17 +90,18 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
return (
<div className="mx-auto mb-12 flex items-center justify-center gap-4 px-8">
<button
className={`px-3 py-2 ${page === 1 ? 'pointer-events-none text-gray-400' : 'text-dark hover:text-gray-600'}`}
className={`px-3 py-2 ${page === 1 ? "pointer-events-none text-gray-400" : "text-dark hover:text-gray-600"}`}
onClick={() => navigatePage(page - 1)}
type="button"
>
&lt;
</button>
<form className="flex items-center gap-2" onSubmit={handlePageSubmit}>
{mods.pagination.pagination.split('{input}').map((value, index) => {
{mods.pagination.pagination.split("{input}").map((value, index) => {
if (index === 0) {
return (
<input
key={index}
aria-label="Page number"
className="w-16 rounded border border-dark bg-transparent px-2 py-1 text-center text-sm"
onInput={handlePageInputChange}
@ -110,13 +112,15 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
}
return (
<span className="text-sm" key={value}>
{value.replace('{totalPages}', totalPages.toString()).replace('{totalItems}', totalItems.toString())}
{value
.replace("{totalPages}", totalPages.toString())
.replace("{totalItems}", totalItems.toString())}
</span>
)
})}
</form>
<button
className={`px-3 py-2 ${page === totalPages ? 'pointer-events-none text-gray-400' : 'text-dark hover:text-gray-600'}`}
className={`px-3 py-2 ${page === totalPages ? "pointer-events-none text-gray-400" : "text-dark hover:text-gray-600"}`}
onClick={() => navigatePage(page + 1)}
type="button"
>
@ -143,7 +147,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
<div className="grid w-full grid-cols-2 place-items-center gap-4 sm:grid-cols-3">
<div className="flex flex-col items-start gap-2">
<button
className="flex items-center gap-2 px-4 py-2 font-semibold text-md"
className="text-md flex items-center gap-2 px-4 py-2 font-semibold"
onClick={toggleCreatedSort}
type="button"
>
@ -159,7 +163,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
<div className="flex flex-col items-center gap-2">
<button
className="flex items-center gap-2 px-4 py-2 font-semibold text-md"
className="text-md flex items-center gap-2 px-4 py-2 font-semibold"
onClick={toggleUpdatedSort}
type="button"
>
@ -174,7 +178,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
</div>
<div className="flex items-center gap-2 px-4 py-2">
<label className="font-semibold text-md" htmlFor="limit">
<label className="text-md font-semibold" htmlFor="limit">
{mods.sort.perPage}
</label>
<select
@ -194,7 +198,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
<div className="grid w-full grid-cols-1 place-items-start gap-12 py-6 md:grid-cols-2 xl:grid-cols-3">
{paginatedMods.length > 0 ? (
paginatedMods.map((mod) => (
paginatedMods.map(mod => (
<a
className="mod-card flex w-full flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
href={`/mods/${mod.id}`}
@ -209,17 +213,17 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
/>
</div>
<div>
<h2 className="font-bold text-lg">
{mod.name} <span className="ml-1 font-normal text-sm">by @{mod.author}</span>
<h2 className="text-lg font-bold">
{mod.name} <span className="ml-1 text-sm font-normal">by @{mod.author}</span>
</h2>
<p className="font-thin text-sm">{mod.description}</p>
<p className="text-sm font-thin">{mod.description}</p>
</div>
</a>
))
) : (
<div className="col-span-4 grid place-items-center gap-4 place-self-center px-8 text-center">
<h2 className="font-bold text-lg">{mods.noResults}</h2>
<p className="font-thin text-sm">{mods.noResultsDescription}</p>
<h2 className="text-lg font-bold">{mods.noResults}</h2>
<p className="text-sm font-thin">{mods.noResultsDescription}</p>
</div>
)}
</div>
@ -228,3 +232,5 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
</div>
)
}
export default ModsList

View file

@ -1,16 +1,14 @@
---
import { Astronav, Dropdown, DropdownItems, MenuItems } from 'astro-navbar'
import { motion } from 'motion/react'
import Button from '~/components/Button.astro'
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
import ChevronDownIcon from '~/icons/ChevronDownIcon.astro'
import DownloadIcon from '~/icons/DownloadIcon.astro'
import MenuIcon from '~/icons/MenuIcon.astro'
import { getLocale, getPath, getUI } from '~/utils/i18n'
import { getTitleAnimation } from '../animations.ts'
import Logo from './Logo.astro'
import MobileMenu from './MobileMenu.astro'
import ThemeSwitch from './ThemeSwitch.astro'
import { Astronav, Dropdown, DropdownItems, MenuItems } from "astro-navbar"
import Button from "~/components/Button.astro"
import ArrowRightIcon from "~/icons/ArrowRightIcon.astro"
import ChevronDownIcon from "~/icons/ChevronDownIcon.astro"
import DownloadIcon from "~/icons/DownloadIcon.astro"
import MenuIcon from "~/icons/MenuIcon.astro"
import { getLocale, getPath, getUI } from "~/utils/i18n"
import Logo from "./Logo.astro"
import MobileMenu from "./MobileMenu.astro"
import ThemeSwitch from "./ThemeSwitch.astro"
const locale = getLocale(Astro)
const getLocalePath = getPath(locale)
@ -26,10 +24,7 @@ const {
<MenuItems
class="container relative z-20 grid w-full grid-cols-2 items-center gap-2 bg-paper py-3 lg:grid lg:grid-cols-[auto_1fr_auto] lg:py-6"
>
<a
class="flex items-center gap-2 text-lg font-bold"
href={getLocalePath('/')}
>
<a class="flex items-center gap-2 text-lg font-bold" href={getLocalePath("/")}>
<Logo class="text-coral" />
<span>{brand}</span>
</a>
@ -44,15 +39,8 @@ const {
/>
</button>
<DropdownItems>
<motion.div
className="navbar-dropdown"
{...getTitleAnimation(0, 0.3, false)}
client:load
>
<a
class="dropdown-item bg-dark/5 row-span-2"
href={getLocalePath('/mods')}
>
<div class="navbar-dropdown">
<a class="dropdown-item bg-dark/5 row-span-2" href={getLocalePath("/mods")}>
<div class="dropdown-title">{menu.zenMods}</div>
<div class="dropdown-description">
{menu.zenModsDesc}
@ -62,7 +50,7 @@ const {
<ArrowRightIcon class="size-4" />
</Button>
</a>
<a class="dropdown-item" href={getLocalePath('/release-notes')}>
<a class="dropdown-item" href={getLocalePath("/release-notes")}>
<div class="dropdown-title">{menu.releaseNotes}</div>
<div class="dropdown-description">
{menu.releaseNotesDesc}
@ -74,7 +62,7 @@ const {
{menu.discordDesc}
</div>
</a>
</motion.div>
</div>
</DropdownItems>
</Dropdown>
<Dropdown class="group">
@ -85,18 +73,14 @@ const {
/>
</button>
<DropdownItems>
<motion.div
className="navbar-dropdown !grid-cols-1 gap-1"
{...getTitleAnimation(0, 0.3, false)}
client:load
>
<a class="dropdown-item" href={getLocalePath('/donate')}>
<div class="navbar-dropdown !grid-cols-1 gap-1">
<a class="dropdown-item" href={getLocalePath("/donate")}>
<div class="dropdown-title">{menu.donate}</div>
<div class="dropdown-description">
{menu.donateDesc}
</div>
</a>
<a class="dropdown-item" href={getLocalePath('/about')}>
<a class="dropdown-item" href={getLocalePath("/about")}>
<div class="dropdown-title">{menu.aboutUs}</div>
<div class="dropdown-description">
{menu.aboutUsDesc}
@ -108,20 +92,16 @@ const {
{menu.documentationDesc}
</div>
</a>
<a
class="dropdown-item"
href="https://github.com/zen-browser"
target="_blank"
>
<a class="dropdown-item" href="https://github.com/zen-browser" target="_blank">
<div class="dropdown-title">{menu.github}</div>
<div class="dropdown-description">
{menu.githubDesc}
</div>
</a>
</motion.div>
</div>
</DropdownItems>
</Dropdown>
<a class="hidden items-center lg:block" href={getLocalePath('/mods')}>
<a class="hidden items-center lg:block" href={getLocalePath("/mods")}>
<span>{menu.mods}</span>
</a>
</div>
@ -138,6 +118,7 @@ const {
<DownloadIcon class="size-4" />
</span>
</Button>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label
for="mobile-menu-toggle"
class="cursor-pointer p-2 text-dark lg:hidden"

View file

@ -1,10 +1,10 @@
---
import InfoIcon from '~/icons/InfoIcon.astro'
import InfoIcon from "~/icons/InfoIcon.astro"
import { releaseNotes as releaseNotesData } from '~/release-notes'
import { getLocale, getPath, getUI } from '~/utils/i18n'
import { type ReleaseNote, getReleaseNoteFirefoxVersion } from '../release-notes'
import ReleaseNoteListItem from './ReleaseNoteListItem.astro'
import { releaseNotes as releaseNotesData } from "~/release-notes"
import { getLocale, getPath, getUI } from "~/utils/i18n"
import { type ReleaseNote, getReleaseNoteFirefoxVersion } from "../release-notes"
import ReleaseNoteListItem from "./ReleaseNoteListItem.astro"
export type Props = ReleaseNote
const { isTwilight, ...props } = Astro.props
@ -20,38 +20,42 @@ const {
let date: Date | undefined
if (props.date) {
const [day, month, year] = props.date.split('/')
const [day, month, year] = props.date.split("/")
date = new Date(Date.parse(`${year}-${month}-${day}`))
}
const ffVersion = getReleaseNoteFirefoxVersion(props)
const currentReleaseIndex = releaseNotesData.findIndex((releaseNote: ReleaseNote) => releaseNote.version === props.version)
const currentReleaseIndex = releaseNotesData.findIndex(
(releaseNote: ReleaseNote) => releaseNote.version === props.version
)
const prevReleaseNote = releaseNotesData[currentReleaseIndex + 1]
let compareLink = ''
let compareLink = ""
if (prevReleaseNote && !isTwilight) {
compareLink = `https://github.com/zen-browser/desktop/compare/${prevReleaseNote.version}...${props.version}`
}
const isLatest = currentReleaseIndex === 0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const listItems = {} as any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const generateItems = (items: any, type: string) => {
if (!items) return
if (!listItems[type]) {
listItems[type] = []
}
// biome-ignore lint/complexity/noForEach: We dont need to use a for loop here
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items.forEach((item: any) => {
switch (type) {
case 'feature':
case "feature":
listItems[type].push({
type: 'feature',
type: "feature",
content: item,
})
break
case 'fix':
case "fix":
listItems[type].push({
type: 'fix',
type: "fix",
content: item.description ?? item,
...(item.issue
? {
@ -63,86 +67,89 @@ const generateItems = (items: any, type: string) => {
: {}),
})
break
case 'security':
case "security":
listItems[type].push({
type: 'security',
type: "security",
link: {
text: 'Various security fixes.',
text: "Various security fixes.",
href: item,
},
})
break
case 'theme':
case "theme":
listItems[type].push({
type: 'theme',
type: "theme",
content: item,
})
break
case 'break':
case "break":
listItems[type].push({
type: 'break',
type: "break",
content: item,
})
break
}
})
}
generateItems(props.security ? [props.security] : null, 'security')
generateItems(props.fixes, 'fix')
generateItems(props.features, 'feature')
generateItems(props.themeChanges, 'theme')
generateItems(props.breakingChanges, 'break')
generateItems(props.knownIssues, 'known')
generateItems(props.security ? [props.security] : null, "security")
generateItems(props.fixes, "fix")
generateItems(props.features, "feature")
generateItems(props.themeChanges, "theme")
generateItems(props.breakingChanges, "break")
generateItems(props.knownIssues, "known")
---
<section
class="release-note-item relative mt-12 flex flex-col pt-24 lg:flex-row"
id={props.version}
>
<div class="px-5 md:px-10 md:pr-32 w-full gap-2 flex flex-col">
<div class="flex w-full flex-col gap-2 px-5 md:px-10 md:pr-32">
{
isTwilight ? (
<a
class="!mb-2 block w-fit rounded-full bg-coral px-3 py-1 text-xs text-paper"
href={getLocalePath('/download?twilight')}
href={getLocalePath("/download?twilight")}
>
{releaseNoteItem.twilight}
</a>
) : null
}
<div class="w-full sm:flex justify-between">
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-0 text-sm font-bold opacity-80">
<div class="w-full justify-between sm:flex">
<div
class="flex flex-col gap-1 text-sm font-bold opacity-80 sm:flex-row sm:items-center sm:gap-0"
>
{
isTwilight ? (
<>
{releaseNoteItem.twilightChanges} {props.version.replaceAll(
'{version}',
props.version,
)}
{releaseNoteItem.twilightChanges}{" "}
{props.version.replaceAll("{version}", props.version)}
</>
) : (
<>
{releaseNoteItem.releaseChanges.replaceAll(
'{version}',
props.version,
)}
</>
<>{releaseNoteItem.releaseChanges.replaceAll("{version}", props.version)}</>
)
}
{
ffVersion ? (
<>
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
<a rel="noopener noreferrer"class="text-xs underline decoration-wavy text-coral opacity-80" href={`https://www.mozilla.org/en-US/firefox/${ffVersion}/releasenotes/`} target="_blank" rel="noopener noreferrer">
{releaseNoteItem.firefoxVersion.replace('{version}', ffVersion)}
<a
rel="noopener noreferrer"
class="text-xs text-coral underline decoration-wavy opacity-80"
href={`https://www.mozilla.org/en-US/firefox/${ffVersion}/releasenotes/`}
target="_blank"
rel="noopener noreferrer"
>
{releaseNoteItem.firefoxVersion.replace("{version}", ffVersion)}
</a>
</>
) : null
}
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
<a
rel="noopener noreferrer"
class="zen-link whitespace-nowrap !no-underline text-xs opacity-80"
class="zen-link whitespace-nowrap text-xs !no-underline opacity-80"
target="_blank"
href={`https://github.com/zen-browser/desktop/releases/tag/${isTwilight ? 'twilight' : props.version}`}
href={`https://github.com/zen-browser/desktop/releases/tag/${isTwilight ? "twilight" : props.version}`}
>{releaseNoteItem.githubRelease}</a
>
{
@ -151,7 +158,7 @@ generateItems(props.knownIssues, 'known')
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
<a
rel="noopener noreferrer"
class="zen-link whitespace-nowrap !no-underline text-xs opacity-80"
class="zen-link whitespace-nowrap text-xs !no-underline opacity-80"
target="_blank"
href={`https://github.com/zen-browser/desktop/actions/runs/${props.workflowId}`}
>
@ -161,12 +168,12 @@ generateItems(props.knownIssues, 'known')
) : null
}
{
compareLink !== '' ? (
compareLink !== "" ? (
<>
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
<a
rel="noopener noreferrer"
class="zen-link whitespace-nowrap !no-underline text-xs opacity-80"
class="zen-link whitespace-nowrap text-xs !no-underline opacity-80"
target="_blank"
href={compareLink}
>
@ -176,19 +183,19 @@ generateItems(props.knownIssues, 'known')
) : null
}
</div>
<div class="text-xs opacity-80 font-bold">
{date && date.toLocaleDateString('en-US', { dateStyle: 'long' })}
<div class="text-xs font-bold opacity-80">
{date && date.toLocaleDateString("en-US", { dateStyle: "long" })}
</div>
</div>
{
props.extra?.length ? (
<p class="text-md text-muted-foreground extra mt-2">
<Fragment set:html={props.extra.replace(/\n/g, '<br />')} />
<Fragment set:html={props.extra.replace(/\n/g, "<br />")} />
</p>
) : null
}
{isTwilight || isLatest ? (
{
isTwilight || isLatest ? (
<div class="text-muted-forground flex text-sm opacity-70">
{isTwilight ? <InfoIcon class="mx-4 my-0 size-6 text-yellow-500" /> : null}
<p class="m-0">
@ -196,27 +203,25 @@ generateItems(props.knownIssues, 'known')
<span set:html={releaseNoteItem.reportIssues} />
</p>
</div>
) : null}
<div class="gap-8 flex flex-col mt-4">
) : null
}
<div class="mt-4 flex flex-col gap-8">
{
Object.keys(listItems).map((type) => {
const items = listItems[type];
if (items.length === 0) return null;
Object.keys(listItems).map(type => {
const items = listItems[type]
if (items.length === 0) return null
return (
<ul class="gap-1 flex flex-col">
<ul class="flex flex-col gap-1">
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{items.map((item: any) => (
<ReleaseNoteListItem
type={item.type}
content={item.content}
link={item.link}
/>
<ReleaseNoteListItem type={item.type} content={item.content} link={item.link} />
))}
</ul>
);
</div>
)
})
}
</div>
</div>
<style is:global>
.ac-accordion-item-title {
@apply !text-dark;
@ -241,10 +246,7 @@ generateItems(props.knownIssues, 'known')
.ac-accordion {
&.ac-accordion--light {
> * + * {
border-color: light-dark(
rgba(0, 0, 0, 0.1),
rgba(255, 255, 255, 0.1)
) !important;
border-color: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1)) !important;
width: 100%;
}
}

View file

@ -1,8 +1,8 @@
---
import { getLocale, getPath, getUI } from '~/utils/i18n'
import { getLocale, getPath, getUI } from "~/utils/i18n"
const { type, content, link } = Astro.props as {
type: 'security' | 'feature' | 'fix' | 'theme' | 'break' | 'known'
type: "security" | "feature" | "fix" | "theme" | "break" | "known"
content: string
link?: {
text: string
@ -22,29 +22,25 @@ const {
<li class="flex gap-2">
<div
class:list={[
(type === 'security' && 'text-[#e3401f]') ||
(type === 'feature' && 'text-[#bf3316] dark:text-[#ffb1a1]') ||
(type === 'fix' && 'text-[#fe846b]') ||
(type === 'theme' && 'text-[#f76f53]') ||
(type === 'break' && 'text-[#471308] dark:text-[#D02908]') || ''
, 'opacity-80 font-bold min-w-16']}
(type === "security" && "text-[#e3401f]") ||
(type === "feature" && "text-[#bf3316] dark:text-[#ffb1a1]") ||
(type === "fix" && "text-[#fe846b]") ||
(type === "theme" && "text-[#f76f53]") ||
(type === "break" && "text-[#471308] dark:text-[#D02908]") ||
"",
"min-w-16 font-bold opacity-80",
]}
>
{itemType[type]}
</div>
<div>
{content && (
<span
class="text-base opacity-80"
set:html={content}
/>
)}
{link && (
<a
href={link.href}
class="text-base text-blue inline-block underline"
>
{content && <span class="text-base opacity-80" set:html={content} />}
{
link && (
<a href={link.href} class="text-blue inline-block text-base underline">
{link.text}
</a>
)}
)
}
</div>
</li>

View file

@ -1,15 +1,21 @@
---
const { gap = 4 } = Astro.props
import { icon, library } from '@fortawesome/fontawesome-svg-core'
import { faBluesky, faGithub, faMastodon, faReddit, faXTwitter } from '@fortawesome/free-brands-svg-icons'
import { icon, library } from "@fortawesome/fontawesome-svg-core"
import {
faBluesky,
faGithub,
faMastodon,
faReddit,
faXTwitter,
} from "@fortawesome/free-brands-svg-icons"
library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit)
const Mastodon = icon({ prefix: 'fab', iconName: 'mastodon' })
const Bluesky = icon({ prefix: 'fab', iconName: 'bluesky' })
const Github = icon({ prefix: 'fab', iconName: 'github' })
const XTwitter = icon({ prefix: 'fab', iconName: 'x-twitter' })
const Reddit = icon({ prefix: 'fab', iconName: 'reddit' })
const Mastodon = icon({ prefix: "fab", iconName: "mastodon" })
const Bluesky = icon({ prefix: "fab", iconName: "bluesky" })
const Github = icon({ prefix: "fab", iconName: "github" })
const XTwitter = icon({ prefix: "fab", iconName: "x-twitter" })
const Reddit = icon({ prefix: "fab", iconName: "reddit" })
---
<ul class={`flex items-center opacity-80 gap-${gap}`}>

View file

@ -1,14 +1,14 @@
---
import { motion } from 'motion/react'
import { getTitleAnimation } from '~/animations'
import Description from '~/components/Description.astro'
import { getLocale, getUI } from '~/utils/i18n'
import { motion } from "motion/react"
import { getTitleAnimation } from "~/animations"
import Description 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_monochrome.svg"
import Image from 'astro/components/Image.astro'
import Image from "astro/components/Image.astro"
const { showSponsors = true } = Astro.props
const {
@ -18,7 +18,7 @@ const {
} = getUI(locale)
---
<section id="sponsors" class:list={['py-12', !showSponsors && 'hidden']}>
<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-6xl font-bold">Our Sponsors</Description>
@ -28,10 +28,10 @@ const {
</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">
<a href={sponsors.sponsors["tuta"].url} target="_blank" class="w-fit">
<Image
src={tutaLogo}
alt={sponsors.sponsors['tuta'].name}
alt={sponsors.sponsors["tuta"].name}
class="h-16 w-fit object-contain"
/>
</a>

View file

@ -4,17 +4,14 @@ interface Props {
className?: string
}
const { label, className = '' } = Astro.props
const { label, className = "" } = Astro.props
---
<button
type="button"
class:list={[
'inline-flex h-8 w-8 cursor-pointer items-center justify-center',
className,
]}
class:list={["inline-flex h-8 w-8 cursor-pointer items-center justify-center", className]}
id="theme-switcher"
aria-label={label || 'Toggle theme'}
aria-label={label || "Toggle theme"}
>
<svg class="hidden h-5 w-5 dark:block" viewBox="0 0 24 24">
<path
@ -29,26 +26,24 @@ const { label, className = '' } = Astro.props
</button>
<script>
const themeSwitch = document.getElementById(
'theme-switcher',
) as HTMLButtonElement
const themeSwitch = document.getElementById("theme-switcher") as HTMLButtonElement
const resolveTheme = () => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme') ?? 'light'
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
return localStorage.getItem("theme") ?? "light"
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark'
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark"
}
return 'light'
return "light"
}
const html = document.documentElement
themeSwitch.addEventListener('click', () => {
const newTheme = resolveTheme() === 'light' ? 'dark' : 'light'
html.setAttribute('data-theme', newTheme)
html.classList.toggle('dark', newTheme === 'dark')
localStorage.setItem('theme', newTheme)
themeSwitch.addEventListener("click", () => {
const newTheme = resolveTheme() === "light" ? "dark" : "light"
html.setAttribute("data-theme", newTheme)
html.classList.toggle("dark", newTheme === "dark")
localStorage.setItem("theme", newTheme)
})
</script>

View file

@ -1,13 +1,10 @@
---
import { cn } from '~/utils/merge'
import { cn } from "~/utils/merge"
const { class: className } = Astro.props
---
<h1
class={cn(
"text-dark leading-[0.9] mb-[0.4rem] font-junicode font-semibold text-5xl",
className,
)}
class={cn("mb-[0.4rem] font-junicode text-5xl font-semibold leading-[0.9] text-dark", className)}
>
<slot />
</h1>

View file

@ -1,28 +1,22 @@
---
const { src, class: className, ...rest } = Astro.props
const type = src.split('.').pop() || 'webm'
const type = src.split(".").pop() || "webm"
---
<video
class:list={['w-fit', className]}
data-src={src}
preload="none"
{...rest}
>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video class:list={["w-fit", className]} data-src={src} preload="none" {...rest}>
<source src="" type={`video/${type}`} />
</video>
<script>
const videos = document.querySelectorAll(
'video[data-src]',
) as NodeListOf<HTMLVideoElement>
const videos = document.querySelectorAll("video[data-src]") as NodeListOf<HTMLVideoElement>
const loadVideo = (video: HTMLVideoElement) => {
const source = video.querySelector('source')
const dataSrc = video.getAttribute('data-src')
const source = video.querySelector("source")
const dataSrc = video.getAttribute("data-src")
if (dataSrc && source) {
source.src = dataSrc
video.removeAttribute('data-src')
video.removeAttribute("data-src")
video.load()
}
}

View file

@ -45,7 +45,7 @@ const { label, href, checksum } = Astro.props
Show SHA-256
</span>
<span class="checksum-tooltip popover absolute -left-14 -top-12 z-50 hidden min-w-[220px] items-center gap-2 whitespace-nowrap rounded-md border border-subtle bg-[rgba(255,255,255,0.98)] px-3 py-2 text-xs text-gray-700 opacity-100 shadow transition-opacity duration-150 group-focus-within/checksum:flex dark:bg-[rgba(24,24,27,0.98)] dark:text-gray-100">
<span class="flex-1 truncate font-mono text-xs">{checksum}</span>
<span class="font-mono flex-1 truncate text-xs">{checksum}</span>
<button
type="button"
class="copy-btn rounded bg-coral px-2 py-1 text-xs text-white hover:bg-coral/80 data-[twilight='true']:bg-zen-blue data-[twilight='true']:hover:bg-zen-blue/80"
@ -89,14 +89,12 @@ const { label, href, checksum } = Astro.props
<script>
const checksumButtons = document.querySelectorAll(
'.checksum-icon-btn',
".checksum-icon-btn"
) as NodeListOf<HTMLButtonElement>
const checksumTooltips = document.querySelectorAll(
'.checksum-tooltip',
".checksum-tooltip"
) as NodeListOf<HTMLDivElement>
const copyButtons = document.querySelectorAll(
'.copy-btn',
) as NodeListOf<HTMLButtonElement>
const copyButtons = document.querySelectorAll(".copy-btn") as NodeListOf<HTMLButtonElement>
function stopEvent(e: Event) {
e.preventDefault?.()
@ -109,28 +107,25 @@ const { label, href, checksum } = Astro.props
navigator.clipboard.writeText(checksum)
const btn = e.currentTarget as HTMLButtonElement
const original = btn.innerText
btn.innerText = 'Copied!'
btn.innerText = "Copied!"
setTimeout(() => (btn.innerText = original), 1200)
}
checksumButtons.forEach((btn) => {
btn.addEventListener('click', stopEvent)
checksumButtons.forEach(btn => {
btn.addEventListener("click", stopEvent)
})
checksumTooltips.forEach((tooltip) => {
tooltip.addEventListener('mousedown', stopEvent)
tooltip.addEventListener('click', stopEvent)
checksumTooltips.forEach(tooltip => {
tooltip.addEventListener("mousedown", stopEvent)
tooltip.addEventListener("click", stopEvent)
})
copyButtons.forEach((btn) => {
btn.addEventListener('click', (e) =>
copyButtons.forEach(btn => {
btn.addEventListener("click", e =>
copyChecksum(
e,
(
btn
.closest('.checksum-tooltip')
?.querySelector('.font-mono') as HTMLSpanElement
)?.innerText,
),
(btn.closest(".checksum-tooltip")?.querySelector(".font-mono") as HTMLSpanElement)
?.innerText
)
btn.addEventListener('mousedown', stopEvent)
)
btn.addEventListener("mousedown", stopEvent)
})
</script>

View file

@ -1,17 +1,17 @@
<script>
// Handle platform selection
const platformButtons = document.querySelectorAll('.platform-selector')
const platformSections = document.querySelectorAll('.platform-section')
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
let detectedOS = 'mac' // Default to macOS
let detectedOS = "mac" // Default to macOS
if (userAgent.indexOf('Windows') !== -1) {
detectedOS = 'windows'
} else if (userAgent.indexOf('Linux') !== -1) {
detectedOS = 'linux'
if (userAgent.indexOf("Windows") !== -1) {
detectedOS = "windows"
} else if (userAgent.indexOf("Linux") !== -1) {
detectedOS = "linux"
}
return detectedOS
@ -27,28 +27,28 @@
async function selectPlatform(platform: string) {
// Update button styling
for (const button of platformButtons) {
const buttonPlatform = button.getAttribute('data-platform')
const buttonPlatform = button.getAttribute("data-platform")
if (buttonPlatform === platform) {
button.setAttribute('data-active', 'true')
button.setAttribute("data-active", "true")
} else {
button.setAttribute('data-active', 'false')
button.setAttribute("data-active", "false")
}
}
// Show/hide platform sections
for (const section of platformSections) {
if (section.id === `${platform}-downloads`) {
section.setAttribute('data-active', 'true')
section.setAttribute("data-active", "true")
} else {
section.setAttribute('data-active', 'false')
section.setAttribute("data-active", "false")
}
}
}
// Handle platform button clicks
for (const button of platformButtons) {
button.addEventListener('click', () => {
const platform = button.getAttribute('data-platform') ?? ''
button.addEventListener("click", () => {
const platform = button.getAttribute("data-platform") ?? ""
selectPlatform(platform)
})
}
@ -56,43 +56,40 @@
// Check for twilight mode
async function checkTwilightMode() {
const urlParams = new URLSearchParams(window.location.search)
const isTwilight = urlParams.has('twilight')
const isTwilight = urlParams.has("twilight")
if (isTwilight) {
const twilightInfoElem = document.getElementById('twilight-info')
twilightInfoElem?.setAttribute('data-twilight', 'true')
const twilightInfoElem = document.getElementById("twilight-info")
twilightInfoElem?.setAttribute("data-twilight", "true")
// Update UI to show twilight mode with animation
const titleElem = document.getElementById('download-title')
const titleElem = document.getElementById("download-title")
if (titleElem) {
const zenText = titleElem.innerHTML
titleElem.innerHTML = zenText.replace('Zen', 'Twilight')
titleElem.innerHTML = zenText.replace("Zen", "Twilight")
}
const tags = document.querySelectorAll('.release-type-tag')
const tags = document.querySelectorAll(".release-type-tag")
for (const tag of tags) {
tag.innerHTML = tag.innerHTML.replace('Beta', 'Twilight')
tag.innerHTML = tag.innerHTML.replace("Beta", "Twilight")
}
// Apply twilight mode to all relevant elements
const coralElements = document.querySelectorAll(
'.download-browser-logo, .release-type-tag, .decorative-gradient, .download-link, .download-arrow-icon, .download-card__icon, .checksum-icon-btn, .copy-btn, .flathub-download',
".download-browser-logo, .release-type-tag, .decorative-gradient, .download-link, .download-arrow-icon, .download-card__icon, .checksum-icon-btn, .copy-btn, .flathub-download"
)
for (const element of coralElements) {
element.setAttribute('data-twilight', 'true')
element.setAttribute("data-twilight", "true")
}
// Replace all download links with twilight versions
const downloadLinks = document.querySelectorAll('a.download-link')
const downloadLinks = document.querySelectorAll("a.download-link")
for (const link of downloadLinks) {
if (!link.id.includes('beta')) {
const href = link.getAttribute('href')
if (href && href.includes('/latest/download/')) {
const twilightHref = href.replace(
'/latest/download/',
'/download/twilight/',
)
link.setAttribute('href', twilightHref)
if (!link.id.includes("beta")) {
const href = link.getAttribute("href")
if (href && href.includes("/latest/download/")) {
const twilightHref = href.replace("/latest/download/", "/download/twilight/")
link.setAttribute("href", twilightHref)
}
}
}

View file

@ -22,7 +22,7 @@ interface PlatformReleases {
}
interface Props {
platform: 'mac' | 'windows' | 'linux'
platform: "mac" | "windows" | "linux"
icon: string[]
title: string
description: string
@ -30,13 +30,13 @@ interface Props {
}
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'
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"
function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
return !!obj && typeof obj === 'object' && 'link' in obj
return !!obj && typeof obj === "object" && "link" in obj
}
---
@ -73,22 +73,14 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
{releases.x86_64 &&
typeof releases.x86_64 === "object" &&
"tarball" in releases.x86_64 &&
(releases.x86_64.tarball) && (
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
: ""
}
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}
/>
@ -99,22 +91,16 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
{releases.aarch64 &&
typeof releases.aarch64 === "object" &&
"tarball" in releases.aarch64 &&
(releases.aarch64.tarball) && (
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
: ""
}
href={
releases.aarch64.tarball.link
? releases.aarch64.tarball.link
: ""
releases.aarch64.tarball.label ? releases.aarch64.tarball.label : ""
}
href={releases.aarch64.tarball.link ? releases.aarch64.tarball.link : ""}
variant="aarch64"
checksum={releases.aarch64.tarball.checksum}
/>
@ -133,9 +119,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
checksum={releases.universal.checksum}
/>
)}
{releases.x86_64 &&
isFlatReleaseInfo(releases.x86_64) &&
releases.x86_64.label && (
{releases.x86_64 && isFlatReleaseInfo(releases.x86_64) && releases.x86_64.label && (
<DownloadCard
label={releases.x86_64.label}
href={releases.x86_64.link}
@ -158,11 +142,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
<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={AppIconDark} alt="Zen Browser" class="w-32 translate-y-6 transform dark:hidden" />
<Image
src={AppIconLight}
alt="Zen Browser"

View file

@ -6,42 +6,42 @@ export function getReleasesWithChecksums(checksums: Record<string, string>) {
return {
macos: {
universal: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg',
label: 'Universal',
checksum: checksums['zen.macos-universal.dmg'],
link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg",
label: "Universal",
checksum: checksums["zen.macos-universal.dmg"],
},
},
windows: {
x86_64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe',
label: '64-bit (Recommended)',
checksum: checksums['zen.installer.exe'],
link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe",
label: "64-bit (Recommended)",
checksum: checksums["zen.installer.exe"],
},
arm64: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe',
label: 'ARM64',
checksum: checksums['zen.installer-arm64.exe'],
link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe",
label: "ARM64",
checksum: checksums["zen.installer-arm64.exe"],
},
},
linux: {
x86_64: {
tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
label: 'Tarball',
checksum: checksums['zen.linux-x86_64.tar.xz'],
link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz",
label: "Tarball",
checksum: checksums["zen.linux-x86_64.tar.xz"],
},
},
aarch64: {
tarball: {
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
label: 'Tarball',
checksum: checksums['zen.linux-aarch64.tar.xz'],
link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz",
label: "Tarball",
checksum: checksums["zen.linux-aarch64.tar.xz"],
},
},
flathub: {
all: {
link: 'https://flathub.org/apps/app.zen_browser.zen',
label: 'Flathub',
link: "https://flathub.org/apps/app.zen_browser.zen",
label: "Flathub",
},
},
},

View file

@ -1,7 +1,7 @@
export const CHECKSUMS = {
'zen.macos-universal.dmg': 'macsum',
'zen.installer.exe': 'winsum',
'zen.installer-arm64.exe': 'winarmsum',
'zen.linux-x86_64.tar.xz': 'linuxsum',
'zen.linux-aarch64.tar.xz': 'linuxarmsum',
"zen.macos-universal.dmg": "macsum",
"zen.installer.exe": "winsum",
"zen.installer-arm64.exe": "winarmsum",
"zen.linux-x86_64.tar.xz": "linuxsum",
"zen.linux-aarch64.tar.xz": "linuxarmsum",
}

View file

@ -1,4 +1,4 @@
export const I18N = {
DEFAULT_LOCALE: 'en',
LOCALES: [{ label: 'English', value: 'en' }],
DEFAULT_LOCALE: "en",
LOCALES: [{ label: "English", value: "en" }],
} as const

View file

@ -1,5 +1,5 @@
import { CHECKSUMS } from './checksum'
import { I18N } from './i18n'
import { CHECKSUMS } from "./checksum"
import { I18N } from "./i18n"
export const CONSTANT = {
I18N,

View file

@ -1,9 +1,10 @@
import { useEffect, useState } from 'preact/hooks'
import type { ZenTheme } from '../mods'
import { useEffect, useState } from "react"
type SortOrder = 'default' | 'asc' | 'desc'
import { type ZenTheme } from "../mods"
interface ModsSearchState {
type SortOrder = "default" | "asc" | "desc"
type ModsSearchState = {
search: string
createdSort: SortOrder
updatedSort: SortOrder
@ -16,9 +17,9 @@ const DEFAULT_LIMIT = 12
export function useModsSearch(mods: ZenTheme[]) {
const [searchParams, setSearchParams] = useState<URLSearchParams>()
const [state, setState] = useState<ModsSearchState>({
search: '',
createdSort: 'desc',
updatedSort: 'default',
search: "",
createdSort: "desc",
updatedSort: "default",
page: 1,
limit: DEFAULT_LIMIT,
})
@ -28,11 +29,11 @@ export function useModsSearch(mods: ZenTheme[]) {
const params = new URLSearchParams(window.location.search)
setSearchParams(params)
setState({
search: params.get('q') || '',
createdSort: (params.get('created') as SortOrder) || 'desc',
updatedSort: (params.get('updated') as SortOrder) || 'default',
page: Number.parseInt(params.get('page') || '1', 10),
limit: Number.parseInt(params.get('limit') || String(DEFAULT_LIMIT), 10),
search: params.get("q") || "",
createdSort: (params.get("created") as SortOrder) || "desc",
updatedSort: (params.get("updated") as SortOrder) || "default",
page: Number.parseInt(params.get("page") || "1", 10),
limit: Number.parseInt(params.get("limit") || String(DEFAULT_LIMIT), 10),
})
}, [])
@ -41,41 +42,41 @@ export function useModsSearch(mods: ZenTheme[]) {
if (!searchParams) return
if (state.search) {
searchParams.set('q', state.search)
searchParams.set("q", state.search)
} else {
searchParams.delete('q')
searchParams.delete("q")
}
if (state.createdSort !== 'default') {
searchParams.set('created', state.createdSort)
if (state.createdSort !== "default") {
searchParams.set("created", state.createdSort)
} else {
searchParams.delete('created')
searchParams.delete("created")
}
if (state.updatedSort !== 'default') {
searchParams.set('updated', state.updatedSort)
if (state.updatedSort !== "default") {
searchParams.set("updated", state.updatedSort)
} else {
searchParams.delete('updated')
searchParams.delete("updated")
}
if (state.page > 1) {
searchParams.set('page', state.page.toString())
searchParams.set("page", state.page.toString())
} else {
searchParams.delete('page')
searchParams.delete("page")
}
if (state.limit !== DEFAULT_LIMIT) {
searchParams.set('limit', state.limit.toString())
searchParams.set("limit", state.limit.toString())
} else {
searchParams.delete('limit')
searchParams.delete("limit")
}
const newUrl = `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`
const newUrl = `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`
if (state.page > 1) {
window.history.pushState({}, '', newUrl)
window.history.pushState({}, "", newUrl)
} else {
window.history.replaceState({}, '', newUrl)
window.history.replaceState({}, "", newUrl)
}
}, [state, searchParams])
@ -86,27 +87,27 @@ export function useModsSearch(mods: ZenTheme[]) {
const searchTerm = state.search.toLowerCase()
if (searchTerm) {
filtered = filtered.filter(
(mod) =>
mod =>
mod.name.toLowerCase().includes(searchTerm) ||
mod.description.toLowerCase().includes(searchTerm) ||
mod.author.toLowerCase().includes(searchTerm) ||
(mod.tags?.some((tag) => tag.toLowerCase().includes(searchTerm)) ?? false),
(mod.tags?.some(tag => tag.toLowerCase().includes(searchTerm)) ?? false)
)
}
// Sort by createdAt if chosen
if (state.createdSort !== 'default') {
if (state.createdSort !== "default") {
filtered.sort((a, b) => {
const diff = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
return state.createdSort === 'asc' ? diff : -diff
return state.createdSort === "asc" ? diff : -diff
})
}
// Sort by updatedAt if chosen
if (state.updatedSort !== 'default') {
if (state.updatedSort !== "default") {
filtered.sort((a, b) => {
const diff = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
return state.updatedSort === 'asc' ? diff : -diff
return state.updatedSort === "asc" ? diff : -diff
})
}
@ -120,34 +121,36 @@ export function useModsSearch(mods: ZenTheme[]) {
const paginatedMods = filteredMods.slice(startIndex, endIndex)
const setSearch = (search: string) => {
setState((prev) => ({ ...prev, search, page: 1 })) // Reset page when search changes
setState(prev => ({ ...prev, search, page: 1 })) // Reset page when search changes
}
const toggleCreatedSort = () => {
setState((prev) => ({
setState(prev => ({
...prev,
createdSort: prev.createdSort === 'default' ? 'asc' : prev.createdSort === 'asc' ? 'desc' : 'default',
createdSort:
prev.createdSort === "default" ? "asc" : prev.createdSort === "asc" ? "desc" : "default",
page: 1, // Reset page when sort changes
}))
}
const toggleUpdatedSort = () => {
setState((prev) => ({
setState(prev => ({
...prev,
updatedSort: prev.updatedSort === 'default' ? 'asc' : prev.updatedSort === 'asc' ? 'desc' : 'default',
updatedSort:
prev.updatedSort === "default" ? "asc" : prev.updatedSort === "asc" ? "desc" : "default",
page: 1, // Reset page when sort changes
}))
}
const setPage = (page: number) => {
setState((prev) => ({
setState(prev => ({
...prev,
page: Math.max(1, Math.min(page, totalPages)),
}))
}
const setLimit = (limit: number) => {
setState((prev) => ({ ...prev, limit, page: 1 })) // Reset page when limit changes
setState(prev => ({ ...prev, limit, page: 1 })) // Reset page when limit changes
}
return {

View file

@ -2,4 +2,16 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-arrow-left-icon lucide-arrow-left", className]} {...props}><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-arrow-left-icon lucide-arrow-left", className]}
{...props}><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg
>

View file

@ -2,4 +2,16 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-arrow-right-icon lucide-arrow-right", className]} {...props}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-arrow-right-icon lucide-arrow-right", className]}
{...props}><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg
>

View file

@ -2,4 +2,16 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-arrow-up-icon lucide-arrow-up", className]} {...props}><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-arrow-up-icon lucide-arrow-up", className]}
{...props}><path d="m5 12 7-7 7 7"></path><path d="M12 19V5"></path></svg
>

View file

@ -2,4 +2,16 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-check-icon lucide-check", className]} {...props}><path d="M20 6 9 17l-5-5"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-check-icon lucide-check", className]}
{...props}><path d="M20 6 9 17l-5-5"></path></svg
>

View file

@ -2,4 +2,16 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-chevron-down-icon lucide-chevron-down", className]} {...props}><path d="m6 9 6 6 6-6"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-chevron-down-icon lucide-chevron-down", className]}
{...props}><path d="m6 9 6 6 6-6"></path></svg
>

View file

@ -2,4 +2,18 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-download-icon lucide-download", className]} {...props}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-download-icon lucide-download", className]}
{...props}
><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"
></polyline><line x1="12" x2="12" y1="15" y2="3"></line></svg
>

View file

@ -2,4 +2,18 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-external-link-icon lucide-external-link", className]} {...props}><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-external-link-icon lucide-external-link", className]}
{...props}
><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path
d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg
>

View file

@ -2,4 +2,19 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-github-icon lucide-github", className]} {...props}><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-github-icon lucide-github", className]}
{...props}
><path
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg
>

View file

@ -2,4 +2,18 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-info-icon lucide-info", className]} {...props}><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-info-icon lucide-info", className]}
{...props}
><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"
></path></svg
>

View file

@ -2,4 +2,18 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-lock-icon lucide-lock", className]} {...props}><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-lock-icon lucide-lock", className]}
{...props}
><rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"
></path></svg
>

View file

@ -2,4 +2,16 @@
const { class: className, ...props } = Astro.props
---
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-menu-icon lucide-menu", className]} {...props}><path d="M4 12h16"/><path d="M4 18h16"/><path d="M4 6h16"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class:list={["lucide lucide-menu-icon lucide-menu", className]}
{...props}><path d="M4 12h16"></path><path d="M4 18h16"></path><path d="M4 6h16"></path></svg
>

View file

@ -9,37 +9,35 @@ interface Props {
const { title, description, ogImage, isHome, redirect } = Astro.props
const defaultDescription =
'Zen Browser is built for speed, security, and true privacy. Download now to enjoy a beautifully-designed, distraction-free web experience packed with features.'
const defaultOgImage = '/share-pic.png'
import '@fontsource/bricolage-grotesque/400.css'
import '@fontsource/bricolage-grotesque/500.css'
import '@fontsource/bricolage-grotesque/600.css'
import Footer from '~/components/Footer.astro'
import NavBar from '~/components/NavBar.astro'
import { getLocale } from '~/utils/i18n'
"Zen Browser is built for speed, security, and true privacy. Download now to enjoy a beautifully-designed, distraction-free web experience packed with features."
const defaultOgImage = "/share-pic.png"
import "@fontsource/bricolage-grotesque/400.css"
import "@fontsource/bricolage-grotesque/500.css"
import "@fontsource/bricolage-grotesque/600.css"
import Footer from "~/components/Footer.astro"
import NavBar from "~/components/NavBar.astro"
import { getLocale } from "~/utils/i18n"
const locale = getLocale(Astro)
---
<script is:inline data-cfasync="false">
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme') ?? 'light'
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
return localStorage.getItem("theme") ?? "light"
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark'
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark"
}
return 'light'
return "light"
})()
if (theme === 'light') {
document.documentElement.setAttribute('data-theme', 'light')
if (theme === "light") {
document.documentElement.setAttribute("data-theme", "light")
} else {
document.documentElement.setAttribute('data-theme', 'dark')
document.documentElement.setAttribute("data-theme", "dark")
}
</script>
<!doctype html>
<html lang={locale}>
<head>
<meta charset="UTF-8" />
@ -49,17 +47,23 @@ const locale = getLocale(Astro)
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="sitemap" href="/sitemap-0.xml" />
{isHome && (
// @prettier-ignore
<!-- Injecting schema to homepage only (for SEO) -->
<script is:inline type="application/ld+json">
{
isHome && (
<>
{/* Injecting schema to homepage only (for SEO) */}
<script
is:inline
type="application/ld+json"
set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name":"Zen Browser",
"url":"https://zen-browser.app/"
name: "Zen Browser",
url: "https://zen-browser.app/",
})}
/>
</>
)
}
</script>)}
<!-- ICO favicon as a fallback for browsers that don't support SVG favicons (Safari) -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
@ -72,10 +76,7 @@ const locale = getLocale(Astro)
<meta property="og:title" content={title} />
<meta property="og:type" content="website" />
<meta property="og:image" content={ogImage ?? defaultOgImage} />
<meta
property="og:description"
content={description ?? defaultDescription}
/>
<meta property="og:description" content={description ?? defaultDescription} />
<meta property="og:color" content="#da755b" />
<!-- Twitter card -->
<meta name="twitter:card" content="summary_large_image" />
@ -88,17 +89,15 @@ const locale = getLocale(Astro)
/>
<script>
// eslint-disable-next-line no-console
console.log(
'%c✌ Zen-Browser%c\nWelcome to a calmer internet!',
"%c✌ Zen-Browser%c\nWelcome to a calmer internet!",
'filter: invert(1); font-size: 28px; font-weight: bolder; font-family: "Rubik"; margin-top: 20px; margin-bottom: 8px;',
'color: #f76f53; font-size: 16px; font-family: "Rubik"; margin-bottom: 20px;'
);
)
</script>
</head>
<body
class="overflow-x-hidden bg-paper font-['bricolage-grotesque'] text-dark text-balance"
>
<body class="overflow-x-hidden text-balance bg-paper font-['bricolage-grotesque'] text-dark">
<NavBar />
<slot />
<Footer />
@ -106,14 +105,14 @@ const locale = getLocale(Astro)
</html>
<style is:global>
@font-face {
font-family: 'Junicode';
src: url('/fonts/JunicodeVF-Roman-subset.woff2') format('woff2');
font-family: "Junicode";
src: url("/fonts/JunicodeVF-Roman-subset.woff2") format("woff2");
font-display: swap;
}
@font-face {
font-family: 'Junicode-Italic';
src: url('/fonts/JunicodeVF-Italic-subset.woff2') format('woff2');
font-family: "Junicode-Italic";
src: url("/fonts/JunicodeVF-Italic-subset.woff2") format("woff2");
font-display: swap;
}
@ -129,7 +128,7 @@ const locale = getLocale(Astro)
--zen-muted: rgba(0, 0, 0, 0.05);
--zen-subtle: rgba(0, 0, 0, 0.05);
&[data-theme='dark'] {
&[data-theme="dark"] {
--zen-paper: #1f1f1f;
--zen-dark: #d1cfc0;
--zen-muted: rgba(255, 255, 255, 0.05);
@ -143,17 +142,17 @@ const locale = getLocale(Astro)
body,
body > * {
font-family: 'Bricolage Grotesque', sans-serif;
font-family: "Bricolage Grotesque", sans-serif;
font-optical-sizing: auto;
font-style: normal;
font-weight: 500;
font-variation-settings: 'wdth' 100;
font-variation-settings: "wdth" 100;
}
h1 .italic {
font-family: 'Junicode-Italic', serif;
font-family: "Junicode-Italic", serif;
font-weight: 400;
font-feature-settings: 'swsh' 0;
font-feature-settings: "swsh" 0;
font-style: normal;
}

View file

@ -1,6 +1,6 @@
import { format } from 'date-fns'
import { format } from "date-fns"
export interface ZenTheme {
export type ZenTheme = {
name: string
description: string
image: string
@ -17,14 +17,14 @@ export interface ZenTheme {
updatedAt: Date
}
const THEME_API = 'https://zen-browser.github.io/theme-store/themes.json'
const THEME_API = "https://zen-browser.github.io/theme-store/themes.json"
export async function getAllMods(): Promise<ZenTheme[]> {
try {
const res = await fetch(THEME_API)
const json = await res.json()
// convert dict to array
const mods = Object.keys(json).map((key) => json[key])
const mods = Object.keys(json).map(key => json[key])
return mods
} catch (error) {
console.error(error)
@ -37,5 +37,5 @@ export function getAuthorLink(author: string): string {
}
export function getLocalizedDate(date: Date): string {
return format(date, 'PP')
return format(date, "PP")
}

View file

@ -1,5 +1,5 @@
---
import NotFound from './[...locale]/404.astro'
import NotFound from "./[...locale]/404.astro"
---
<NotFound />

View file

@ -1,10 +1,10 @@
---
import Button from '~/components/Button.astro'
import Description from '~/components/Description.astro'
import Title from '~/components/Title.astro'
import Layout from '~/layouts/Layout.astro'
import { getLocale, getPath, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
import Button from "~/components/Button.astro"
import Description from "~/components/Description.astro"
import Title from "~/components/Title.astro"
import Layout from "~/layouts/Layout.astro"
import { getLocale, getPath, getUI } from "~/utils/i18n"
export { getStaticPaths } from "~/utils/i18n"
const locale = getLocale(Astro)
const getLocalePath = getPath(locale)
@ -17,9 +17,7 @@ const {
<main
class="container flex min-h-[70vh] flex-col items-center justify-center gap-6 py-24 text-center"
>
<Title class="text-7xl font-bold text-coral md:text-9xl xl:text-9xl">
404
</Title>
<Title class="text-7xl font-bold text-coral md:text-9xl xl:text-9xl"> 404 </Title>
<div class="flex flex-col items-center gap-6">
<Description class="text-xl md:text-2xl">
{notFound.title}

View file

@ -1,10 +1,10 @@
---
import { Image } from 'astro:assets'
import Description from '~/components/Description.astro'
import Layout from '~/layouts/Layout.astro'
import { getLocale, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
import Button from '~/components/Button.astro'
import { Image } from "astro:assets"
import Description from "~/components/Description.astro"
import Layout from "~/layouts/Layout.astro"
import { getLocale, getUI } from "~/utils/i18n"
export { getStaticPaths } from "~/utils/i18n"
import Button from "~/components/Button.astro"
const locale = getLocale(Astro)
@ -14,35 +14,31 @@ const {
} = getUI(locale)
---
<Layout
title={layout.about.title}
description={layout.about.description}
>
<main
class="flex min-h-screen flex-col py-24 container w-full gap-24"
>
<div class="w-full flex flex-col gap-6">
<Layout title={layout.about.title} description={layout.about.description}>
<main class="container flex min-h-screen w-full flex-col gap-24 py-24">
<div class="flex w-full flex-col gap-6">
<Description class="text-6xl font-bold leading-none">{about.title}</Description>
<Description class="max-w-4xl">
{about.description}
</Description>
<Button href="/donate" class="w-fit" isPrimary
>{about.littleHelp}</Button
>
<Button href="/donate" class="w-fit" isPrimary>{about.littleHelp}</Button>
</div>
<div class="flex flex-col gap-4 w-full">
<div class="text-4xl lg:text-5xl font-bold leading-none">{about.mainTeam.title}</div>
<div class="flex w-full flex-col gap-4">
<div class="text-4xl font-bold leading-none lg:text-5xl">{about.mainTeam.title}</div>
<Description>
{about.mainTeam.description}
</Description>
<div class="flex flex-col gap-6">
{Object.entries(about.mainTeam.members).map(([team, members]) => (
{
Object.entries(about.mainTeam.members).map(([team, members]) => (
<div class="flex flex-col gap-2">
<div class="text-3xl font-semibold">{about.mainTeam.subTitle[team as keyof typeof about.mainTeam.subTitle]}</div>
<div class="text-3xl font-semibold">
{about.mainTeam.subTitle[team as keyof typeof about.mainTeam.subTitle]}
</div>
<ul class="flex flex-col gap-2">
{Object.entries(members).map(([_key, member]) => (
<li class="text-sm">
{member.link && typeof member.link === 'string' ? (
{member.link && typeof member.link === "string" ? (
<a href={member.link}>
<strong class="zen-link font-bold">{member.name}</strong>
</a>
@ -54,15 +50,19 @@ const {
))}
</ul>
</div>
))}
))
}
</div>
</div>
<div class="flex flex-col gap-4 w-full">
<div class="text-4xl lg:text-5xl font-bold leading-none">{about.contributors.title}</div>
<div class="flex w-full flex-col gap-4">
<div class="text-4xl font-bold leading-none lg:text-5xl">{about.contributors.title}</div>
<Description>
{about.contributors.description}
</Description>
<div class="flex flex-col gap-4 w-fit"><Description class="text-3xl font-semibold lg:text-4xl">{about.contributors.browser}</Description>
<div class="flex w-fit flex-col gap-4">
<Description class="text-3xl font-semibold lg:text-4xl"
>{about.contributors.browser}</Description
>
<a href="https://github.com/zen-browser/desktop/graphs/contributors"
><Image
src="https://contributors-img.web.app/image?repo=zen-browser/desktop"
@ -70,8 +70,12 @@ const {
width={500}
height={500}
/></a
></div>
<div class="flex flex-col gap-4 w-fit"><Description class="text-3xl font-semibold lg:text-4xl">{about.contributors.website}</Description>
>
</div>
<div class="flex w-fit flex-col gap-4">
<Description class="text-3xl font-semibold lg:text-4xl"
>{about.contributors.website}</Description
>
<a href="https://github.com/zen-browser/www/graphs/contributors"
><Image
src="https://contributors-img.web.app/image?repo=zen-browser/www"
@ -79,7 +83,8 @@ const {
width={500}
height={500}
/></a
></div></div>
>
</div>
</div>
</main>
</Layout>

View file

@ -1,10 +1,10 @@
---
import Button from '~/components/Button.astro'
import Description from '~/components/Description.astro'
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
import Layout from '~/layouts/Layout.astro'
import { getLocale, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
import Button from "~/components/Button.astro"
import Description from "~/components/Description.astro"
import ArrowRightIcon from "~/icons/ArrowRightIcon.astro"
import Layout from "~/layouts/Layout.astro"
import { getLocale, getUI } from "~/utils/i18n"
export { getStaticPaths } from "~/utils/i18n"
const locale = getLocale(Astro)
const {
@ -14,26 +14,20 @@ const {
---
<Layout title={layout.donate.title} description={layout.donate.description}>
<main class="container pb-52 pt-24 flex flex-col items-center gap-12">
<main class="container flex flex-col items-center gap-12 pb-52 pt-24">
<div class="flex flex-col gap-4 lg:text-center">
<Description class="text-6xl font-bold">{donate.title}</Description>
<Description class="max-w-3xl">
{donate.description}
</Description>
</div>
<div
class="grid max-w-5xl grid-cols-1 gap-12 text-center lg:grid-cols-[1fr_1px_1fr]"
>
<div class="grid max-w-5xl grid-cols-1 gap-12 text-center lg:grid-cols-[1fr_1px_1fr]">
<div class="flex flex-col items-center gap-4">
<div class="text-6xl font-bold">{donate.patreon.title}</div>
<Description>
{donate.patreon.description}
</Description>
<Button
isPrimary
href="https://www.patreon.com/zen_browser"
class="w-fit"
>
<Button isPrimary href="https://www.patreon.com/zen_browser" class="w-fit">
{donate.patreon.button}
<ArrowRightIcon class="size-4" />
</Button>

View file

@ -1,18 +1,18 @@
---
import Description from '~/components/Description.astro'
import DownloadScript from '~/components/download/DownloadScript.astro'
import PlatformDownload from '~/components/download/PlatformDownload.astro'
import { getReleasesWithChecksums } from '~/components/download/release-data'
import Layout from '~/layouts/Layout.astro'
import { getChecksums } from '~/utils/githubChecksums'
import { getLocale, getUI } from '~/utils/i18n'
import Description from "~/components/Description.astro"
import DownloadScript from "~/components/download/DownloadScript.astro"
import PlatformDownload from "~/components/download/PlatformDownload.astro"
import { getReleasesWithChecksums } from "~/components/download/release-data"
import Layout from "~/layouts/Layout.astro"
import { getChecksums } from "~/utils/githubChecksums"
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 { 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"
export { getStaticPaths } from '~/utils/i18n'
export { getStaticPaths } from "~/utils/i18n"
const locale = getLocale(Astro)
const {
@ -21,10 +21,10 @@ const {
} = getUI(locale)
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 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(checksums)
@ -39,9 +39,7 @@ const platformDescriptions = download.platformDescriptions
<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 id="download-title" class="text-6xl font-bold">{download.title}</Description>
<Description class="max-w-xl text-pretty">
{download.description}
</Description>
@ -191,9 +189,7 @@ const platformDescriptions = download.platformDescriptions
</section>
<!-- Security Notice -->
<div
class="bg-opaicty-10 grid grid-cols-[auto,1fr] gap-4 rounded-2xl bg-subtle p-6"
>
<div class="bg-opaicty-10 grid grid-cols-[auto,1fr] gap-4 rounded-2xl bg-subtle p-6">
<div class="h-fit rounded-xl bg-subtle p-3">
<LockIcon class="h-5 w-5" />
</div>
@ -202,10 +198,7 @@ const platformDescriptions = download.platformDescriptions
<h3 class="mb-2 text-lg font-medium">
{download.securityNotice.title}
</h3>
<p
class="text-muted-foreground"
set:html={download.securityNotice.description}
/>
<p class="text-muted-foreground" set:html={download.securityNotice.description} />
</div>
</div>
</div>

View file

@ -1,7 +1,8 @@
import rss, { type RSSOptions } from '@astrojs/rss'
import { releaseNotes } from '~/release-notes'
import type { ReleaseNote } from '~/release-notes'
export { getStaticPaths } from '~/utils/i18n'
import rss, { type RSSOptions } from "@astrojs/rss"
import { releaseNotes, type ReleaseNote } from "~/release-notes"
export { getStaticPaths } from "~/utils/i18n"
/** The default number of entries to include in the RSS feed. */
const RSS_ENTRY_LIMIT = 20
@ -12,11 +13,12 @@ const RSS_ENTRY_LIMIT = 20
*/
export function GET(context: { url: URL }) {
// Just in case the release notes array is empty for whatever reason.
const latestDate = releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date as string) : new Date()
const latestDate =
releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date as string) : new Date()
const rssData: RSSOptions = {
title: 'Zen Browser Release Notes',
description: 'Release Notes for the Zen Browser',
title: "Zen Browser Release Notes",
description: "Release Notes for the Zen Browser",
site: context.url,
items: [],
customData: `
@ -53,9 +55,9 @@ export function GET(context: { url: URL }) {
* @returns The passed in date string as a Date object.
*/
function formatRssDate(dateStr: string) {
const splitDate = dateStr.split('/')
const splitDate = dateStr.split("/")
if (splitDate.length !== 3) {
throw new Error('Invalid date format')
throw new Error("Invalid date format")
}
const day = Number(splitDate[0])
@ -76,40 +78,43 @@ function formatReleaseNote(releaseNote: ReleaseNote) {
</p>`
if (releaseNote.extra) {
content += `<p>${releaseNote.extra.replace(/(\n)/g, '<br />')}</p>`
content += `<p>${releaseNote.extra.replace(/(\n)/g, "<br />")}</p>`
}
content += addReleaseNoteSection('⚠️ Breaking changes', releaseNote.breakingChanges?.map(breakingChangeToReleaseNote))
content += addReleaseNoteSection('✓ Fixes', releaseNote.fixes?.map(fixToReleaseNote))
content += addReleaseNoteSection('🖌 Theme Changes', releaseNote.themeChanges)
content += addReleaseNoteSection('⭐ Features', releaseNote.features)
content += addReleaseNoteSection(
"⚠️ Breaking changes",
releaseNote.breakingChanges?.map(breakingChangeToReleaseNote)
)
content += addReleaseNoteSection("✓ Fixes", releaseNote.fixes?.map(fixToReleaseNote))
content += addReleaseNoteSection("🖌 Theme Changes", releaseNote.themeChanges)
content += addReleaseNoteSection("⭐ Features", releaseNote.features)
return content
}
function addReleaseNoteSection(title: string, items?: string[]): string {
if (!items) {
return ''
return ""
}
let content = `<h2>${title}</h2>`
content += '<ul>'
content += "<ul>"
for (const item of items) {
if (item && item.length > 0) {
content += `<li>${item}</li>`
}
}
content += '</ul>'
content += "</ul>"
return content
}
function fixToReleaseNote(fix?: Exclude<ReleaseNote['fixes'], undefined>[number]) {
if (typeof fix === 'string') {
function fixToReleaseNote(fix?: Exclude<ReleaseNote["fixes"], undefined>[number]) {
if (typeof fix === "string") {
return fix
}
if (!fix || !fix.description || fix.description.length === 0) {
return ''
return ""
}
let note = fix.description
@ -119,13 +124,15 @@ function fixToReleaseNote(fix?: Exclude<ReleaseNote['fixes'], undefined>[number]
return note
}
function breakingChangeToReleaseNote(breakingChange?: Exclude<ReleaseNote['breakingChanges'], undefined>[number]) {
if (typeof breakingChange === 'string') {
function breakingChangeToReleaseNote(
breakingChange?: Exclude<ReleaseNote["breakingChanges"], undefined>[number]
) {
if (typeof breakingChange === "string") {
return breakingChange
}
if (!breakingChange || !breakingChange.description || breakingChange.description.length === 0) {
return ''
return ""
}
return `${breakingChange.description} (<a href="${breakingChange.link}" target="_blank">Learn more</a>)`
@ -134,10 +141,10 @@ function breakingChangeToReleaseNote(breakingChange?: Exclude<ReleaseNote['break
function pubDate(date?: Date) {
const newDate = date ?? new Date()
const pieces = newDate.toString().split(' ')
const pieces = newDate.toString().split(" ")
const offsetTime = pieces[5].match(/[-+]\d{4}/)
const offset = offsetTime ? offsetTime : pieces[5]
const parts = [`${pieces[0]},`, pieces[2], pieces[1], pieces[3], pieces[4], offset]
return parts.join(' ')
return parts.join(" ")
}

View file

@ -1,22 +1,18 @@
---
import Community from '~/components/Community.astro'
import Features from '~/components/Features.astro'
import Hero from '~/components/Hero.astro'
import Sponsors from '~/components/Sponsors.astro'
import Layout from '~/layouts/Layout.astro'
import { getLocale, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
import Community from "~/components/Community.astro"
import Features from "~/components/Features.astro"
import Hero from "~/components/Hero.astro"
import Sponsors from "~/components/Sponsors.astro"
import Layout from "~/layouts/Layout.astro"
import { getLocale, getUI } from "~/utils/i18n"
export { getStaticPaths } from "~/utils/i18n"
const locale = getLocale(Astro)
const { layout } = getUI(locale)
---
<Layout
title={layout.index.title}
description={layout.index.description}
isHome
>
<Layout title={layout.index.title} description={layout.index.description} isHome>
<main class="container">
<Hero />
<Features />

View file

@ -1,18 +1,18 @@
---
import BackButton from '~/components/BackButton.astro'
import Button from '~/components/Button.astro'
import Description from '~/components/Description.astro'
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
import InfoIcon from '~/icons/InfoIcon.astro'
import Layout from '~/layouts/Layout.astro'
import { getAllMods, getAuthorLink, getLocalizedDate } from '~/mods'
import { getUI } from '~/utils/i18n'
import { getLocale, getOtherLocales } from '~/utils/i18n'
import BackButton from "~/components/BackButton.astro"
import Button from "~/components/Button.astro"
import Description from "~/components/Description.astro"
import ArrowRightIcon from "~/icons/ArrowRightIcon.astro"
import InfoIcon from "~/icons/InfoIcon.astro"
import Layout from "~/layouts/Layout.astro"
import { getAllMods, getAuthorLink, getLocalizedDate } from "~/mods"
import { getUI } from "~/utils/i18n"
import { getLocale, getOtherLocales } from "~/utils/i18n"
export async function getStaticPaths() {
const mods = await getAllMods()
return mods.flatMap((mod) => [
...getOtherLocales().map((locale) => ({
return mods.flatMap(mod => [
...getOtherLocales().map(locale => ({
params: {
slug: mod.id,
locale: locale,
@ -54,8 +54,8 @@ const {
---
<Layout
title={slug.title.replace('{name}', mod.name)}
description={slug.description.replace('{name}', mod.name)}
title={slug.title.replace("{name}", mod.name)}
description={slug.description.replace("{name}", mod.name)}
ogImage={mod.image}
>
<main class="mt-6 2xl:mt-0">
@ -93,49 +93,32 @@ const {
<div class="flex flex-shrink-0 flex-col gap-2 font-normal">
<p
set:html={slug.createdBy
.replace('{author}', mod.author)
.replace('{version}', mod.version)
.replace('{link}', getAuthorLink(mod.author))}
/>
<p
set:html={slug.creationDate.replace('{createdAt}', dates.createdAt)}
.replace("{author}", mod.author)
.replace("{version}", mod.version)
.replace("{link}", getAuthorLink(mod.author))}
/>
<p set:html={slug.creationDate.replace("{createdAt}", dates.createdAt)} />
{
dates.createdAt !== dates.updatedAt && (
<p
set:html={slug.latestUpdate.replace(
'{updatedAt}',
dates.updatedAt,
)}
/>
<p set:html={slug.latestUpdate.replace("{updatedAt}", dates.updatedAt)} />
)
}
{
mod.homepage && (
<a
href={mod.homepage}
target="_blank"
rel="noopener noreferrer"
class="zen-link"
>
<a href={mod.homepage} target="_blank" rel="noopener noreferrer" class="zen-link">
{slug.visitModHomepage}
</a>
)
}
</div>
<div class="flex flex-col sm:items-end">
<Button
class="hidden"
id="install-theme"
extra={{ 'zen-theme-id': mod.id }}
isPrimary
>
<Button class="hidden" id="install-theme" extra={{ "zen-theme-id": mod.id }} isPrimary>
{slug.installMod}
</Button>
<Button
class="hidden"
id="install-theme-uninstall"
extra={{ 'zen-theme-id': mod.id }}
extra={{ "zen-theme-id": mod.id }}
isPrimary
>
{slug.uninstallMod}

View file

@ -1,11 +1,11 @@
---
import Description from '~/components/Description.astro'
import ModsList from '~/components/ModsList'
import { CONSTANT } from '~/constants'
import Layout from '~/layouts/Layout.astro'
import { getAllMods } from '~/mods'
import { getLocale, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
import Description from "~/components/Description.astro"
import ModsList from "~/components/ModsList"
import { CONSTANT } from "~/constants"
import Layout from "~/layouts/Layout.astro"
import { getAllMods } from "~/mods"
import { getLocale, getUI } from "~/utils/i18n"
export { getStaticPaths } from "~/utils/i18n"
const locale = getLocale(Astro)
@ -27,10 +27,6 @@ const allMods = (await getAllMods()) || []
</header>
<!-- Importing ModList component -->
<ModsList
allMods={allMods}
locale={locale ?? CONSTANT.I18N.DEFAULT_LOCALE}
client:load
/>
<ModsList allMods={allMods} locale={locale ?? CONSTANT.I18N.DEFAULT_LOCALE} client:load />
</main>
</Layout>

View file

@ -1,8 +1,8 @@
---
import Title from '~/components/Title.astro'
import Layout from '~/layouts/Layout.astro'
import { getLocale, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
import Title from "~/components/Title.astro"
import Layout from "~/layouts/Layout.astro"
import { getLocale, getUI } from "~/utils/i18n"
export { getStaticPaths } from "~/utils/i18n"
const locale = getLocale(Astro)
@ -12,10 +12,7 @@ const {
} = getUI(locale)
---
<Layout
title={layout.privacyPolicy.title}
description={layout.privacyPolicy.description}
>
<Layout title={layout.privacyPolicy.title} description={layout.privacyPolicy.description}>
<main class="mx-auto mt-52 w-1/2 pb-24">
<Title id="privacy-policy" class="xl:text-6xl">{privacyPolicy.title}</Title>
<div class="ml-4 font-bold">{privacyPolicy.lastUpdated}</div>
@ -26,54 +23,39 @@ const {
<div class="mx-12 my-12 flex gap-4 font-bold">
{privacyPolicy.sections.introduction.summary}
</div>
<Title
class="mt-16 text-4xl font-bold"
id="1-information-we-do-not-collect"
>
<Title class="mt-16 text-4xl font-bold" id="1-information-we-do-not-collect">
{privacyPolicy.sections.noCollect.title}
</Title>
<p>{privacyPolicy.sections.noCollect.body}</p>
<h3 class="mt-4 text-xl font-bold" id="-1-1-no-telemetry-">
<strong class="font-bold"
>{privacyPolicy.sections.noTelemetry.title}</strong
>
<strong class="font-bold">{privacyPolicy.sections.noTelemetry.title}</strong>
</h3>
<p>{privacyPolicy.sections.noTelemetry.body}</p>
<p>{privacyPolicy.sections.noTelemetry.body2}</p>
<h3 class="mt-4 text-xl font-bold" id="-1-2-no-personal-data-collection-">
<strong class="font-bold"
>{privacyPolicy.sections.noPersonalData.title}</strong
>
<strong class="font-bold">{privacyPolicy.sections.noPersonalData.title}</strong>
</h3>
<p>{privacyPolicy.sections.noPersonalData.body}</p>
<h3 class="mt-4 text-xl font-bold" id="-1-4-no-third-party-tracking-">
<strong class="font-bold"
>{privacyPolicy.sections.noThirdParty.title}</strong
>
<strong class="font-bold">{privacyPolicy.sections.noThirdParty.title}</strong>
</h3>
<p>{privacyPolicy.sections.noThirdParty.body}</p>
<h3 class="mt-4 text-xl font-bold" id="-1-3-no-third-party-tracking-">
<strong class="font-bold"
>{privacyPolicy.sections.externalConnections.title}</strong
>
<strong class="font-bold">{privacyPolicy.sections.externalConnections.title}</strong>
</h3>
<p>{privacyPolicy.sections.externalConnections.body}</p>
<Title
class="mt-16 text-4xl font-bold"
id="2-information-stored-locally-on-your-device"
>
<Title class="mt-16 text-4xl font-bold" id="2-information-stored-locally-on-your-device">
{privacyPolicy.sections.localStorage.title}
</Title>
<h3 class="mt-4 text-xl font-bold" id="-2-1-browsing-data-">
<strong class="font-bold"
>{privacyPolicy.sections.browsingData.title}</strong
>
<strong class="font-bold">{privacyPolicy.sections.browsingData.title}</strong>
</h3>
<p>{privacyPolicy.sections.browsingData.body}</p>
<ul>
<li>
<strong class="font-bold">{privacyPolicy.sections.cookies.title}</strong
>: {privacyPolicy.sections.cookies.body}
<strong class="font-bold">{privacyPolicy.sections.cookies.title}</strong>: {
privacyPolicy.sections.cookies.body
}
</li>
<li>
<strong class="font-bold">{privacyPolicy.sections.cache.title}</strong>: {
@ -91,9 +73,7 @@ const {
<p>{privacyPolicy.sections.sync.body}</p>
<ul>
<li>
<a
class="zen-link"
href="https://www.mozilla.org/en-US/privacy/mozilla-accounts/"
<a class="zen-link" href="https://www.mozilla.org/en-US/privacy/mozilla-accounts/"
>{privacyPolicy.sections.sync.link1}</a
>
</li>
@ -120,9 +100,7 @@ const {
{privacyPolicy.sections.control.title}
</Title>
<h3 class="mt-4 text-xl font-bold" id="-6-1-data-deletion-">
<strong class="font-bold"
>{privacyPolicy.sections.control.deletionTitle}</strong
>
<strong class="font-bold">{privacyPolicy.sections.control.deletionTitle}</strong>
</h3>
<p>{privacyPolicy.sections.control.deletionBody}</p>
<Title class="mt-16 text-4xl font-bold" id="7-our-website-and-services">
@ -130,24 +108,16 @@ const {
</Title>
<p>{privacyPolicy.sections.website.body}</p>
<h3 class="mt-4 text-xl font-bold" id="-7-1-external-links-">
<strong class="font-bold"
>{privacyPolicy.sections.website.externalLinksTitle}</strong
>
<strong class="font-bold">{privacyPolicy.sections.website.externalLinksTitle}</strong>
</h3>
<p>{privacyPolicy.sections.website.externalLinksBody}</p>
<Title
class="mt-16 text-4xl font-bold"
id="8-changes-to-this-privacy-policy"
>
<Title class="mt-16 text-4xl font-bold" id="8-changes-to-this-privacy-policy">
{privacyPolicy.sections.changes.title}
</Title>
<p>
{privacyPolicy.sections.changes.body}
</p>
<Title
class="mt-16 text-4xl font-bold"
id="9-other-telemetry-done-by-mozilla-firefox"
>
<Title class="mt-16 text-4xl font-bold" id="9-other-telemetry-done-by-mozilla-firefox">
{privacyPolicy.sections.otherTelemetry.title}
</Title>
<p>
@ -155,9 +125,7 @@ const {
</p>
<ul>
<li>
Please check <a
class="zen-link"
href="https://www.mozilla.org/en-US/privacy/"
Please check <a class="zen-link" href="https://www.mozilla.org/en-US/privacy/"
>{privacyPolicy.sections.otherTelemetry.firefoxPrivacyNotice}</a
>
{privacyPolicy.sections.otherTelemetry.forMoreInformation}

View file

@ -1,7 +1,7 @@
---
import Layout from '~/layouts/Layout.astro'
import { releaseNotes } from '~/release-notes'
import { getStaticPaths as getI18nPaths, getLocale, getUI } from '~/utils/i18n'
import Layout from "~/layouts/Layout.astro"
import { releaseNotes } from "~/release-notes"
import { getStaticPaths as getI18nPaths, getLocale, getUI } from "~/utils/i18n"
const locale = getLocale(Astro)
@ -15,12 +15,12 @@ export async function getStaticPaths() {
const i18nPaths = getI18nPaths()
return i18nPaths.flatMap(({ params: { locale } }) => [
...releaseNotes.map((release) => ({
...releaseNotes.map(release => ({
params: { slug: release.version, locale },
props: { ...release },
})),
{
params: { slug: 'latest', locale },
params: { slug: "latest", locale },
props: { ...releaseNotes[0] },
},
])
@ -31,6 +31,6 @@ const release = Astro.props
<Layout title={slug.title} redirect={`/release-notes#${release.version}`}>
<main class="flex flex-col items-center pb-52 pt-36">
{slug.redirect.replaceAll('{version}', release.version)}
{slug.redirect.replaceAll("{version}", release.version)}
</main>
</Layout>

View file

@ -1,13 +1,13 @@
---
import { Modal, ModalBody, ModalHeader } from 'free-astro-components'
import Button from '~/components/Button.astro'
import Description from '~/components/Description.astro'
import ReleaseNoteItem from '~/components/ReleaseNoteItem.astro'
import ArrowUpIcon from '~/icons/ArrowUp.astro'
import Layout from '~/layouts/Layout.astro'
import { releaseNotes as releaseNotesData, releaseNotesTwilight } from '~/release-notes'
import { getLocale, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
import { Modal, ModalBody, ModalHeader } from "free-astro-components"
import Button from "~/components/Button.astro"
import Description from "~/components/Description.astro"
import ReleaseNoteItem from "~/components/ReleaseNoteItem.astro"
import ArrowUpIcon from "~/icons/ArrowUp.astro"
import Layout from "~/layouts/Layout.astro"
import { releaseNotes as releaseNotesData, releaseNotesTwilight } from "~/release-notes"
import { getLocale, getUI } from "~/utils/i18n"
export { getStaticPaths } from "~/utils/i18n"
const locale = getLocale(Astro)
@ -21,21 +21,16 @@ const {
<main
class="container flex h-full min-h-[1000px] flex-1 flex-col items-center justify-center py-4"
>
<div
id="release-notes"
class="py-42 flex min-h-screen gap-8 w-full flex-col justify-center"
>
<div id="release-notes" class="py-42 flex min-h-screen w-full flex-col justify-center gap-8">
<Description class="mt-48 text-6xl font-bold">Changelog</Description>
<p
class="text-base opacity-55"
set:html={releaseNotes.topSection.description.replaceAll(
'{latestVersion}',
releaseNotesData[0].version,
"{latestVersion}",
releaseNotesData[0].version
)}
/>
<div
class="mt-8 flex w-fit flex-col gap-4 sm:mr-0 sm:flex-row sm:items-center"
>
<div class="mt-8 flex w-fit flex-col gap-4 sm:mr-0 sm:flex-row sm:items-center">
<Button class="flex" isPrimary href="/donate">
{releaseNotes.list.support}
</Button>
@ -44,12 +39,11 @@ const {
</Button>
</div>
{
releaseNotesTwilight.features.length ||
releaseNotesTwilight.fixes.length ? (
releaseNotesTwilight.features.length || releaseNotesTwilight.fixes.length ? (
<ReleaseNoteItem {...releaseNotesTwilight} isTwilight />
) : null
}
{releaseNotesData.map((notes: any) => <ReleaseNoteItem {...notes} />)}
{releaseNotesData.map(notes => <ReleaseNoteItem {...notes} />)}
</div>
</main>
<Button href="#" id="scroll-top" isPrimary class="fixed bottom-8 right-8">
@ -67,7 +61,7 @@ const {
<ModalBody>
<div id="version-list" class="flex flex-col gap-2 text-xl text-dark">
{
releaseNotesData.map((note) => (
releaseNotesData.map(note => (
<button
aria-label={`Navigate to version ${note.version}`}
class="w-full text-left transition-colors duration-150 hover:text-coral"
@ -81,41 +75,39 @@ const {
</ModalBody>
</Modal>
<script>
import { openModal, closeModal } from 'free-astro-components'
import { closeModal, openModal } from "free-astro-components"
const scrollTopButton = document.getElementById('scroll-top')
const versionButton = document.getElementById('navigate-to-version')
const container = document.getElementById('release-notes')
const modal = document.getElementById('version-modal')
const versionList = document.getElementById('version-list')
const scrollTopButton = document.getElementById("scroll-top")
const versionButton = document.getElementById("navigate-to-version")
const container = document.getElementById("release-notes")
const modal = document.getElementById("version-modal")
const versionList = document.getElementById("version-list")
const toggleScrollButton = () => {
if (!scrollTopButton || !versionButton) return
const descriptionPosition = versionButton.getBoundingClientRect().bottom
if (descriptionPosition < 0) {
scrollTopButton.classList.remove('hidden')
scrollTopButton.classList.add('block')
scrollTopButton.classList.remove("hidden")
scrollTopButton.classList.add("block")
} else {
scrollTopButton.classList.remove('block')
scrollTopButton.classList.add('hidden')
scrollTopButton.classList.remove("block")
scrollTopButton.classList.add("hidden")
}
}
const navigateToVersion = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!container || !target?.closest('[data-version]')) return
if (!container || !target?.closest("[data-version]")) return
const version = target.getAttribute('data-version')
const version = target.getAttribute("data-version")
if (!version) return
window.location.hash = version
const versionDetails = document
.getElementById(version)
?.getElementsByTagName('details')
const versionDetails = document.getElementById(version)?.getElementsByTagName("details")
if (versionDetails && versionDetails.length > 0) {
Array.from(versionDetails).forEach((accordion) => {
accordion.setAttribute('open', '')
Array.from(versionDetails).forEach(accordion => {
accordion.setAttribute("open", "")
})
}
@ -127,21 +119,21 @@ const {
openModal(modal)
}
window.addEventListener('scroll', toggleScrollButton)
versionButton?.addEventListener('click', openVersionModal)
versionList?.addEventListener('click', navigateToVersion)
window.addEventListener("scroll", toggleScrollButton)
versionButton?.addEventListener("click", openVersionModal)
versionList?.addEventListener("click", navigateToVersion)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal?.hasAttribute('open')) {
document.addEventListener("keydown", e => {
if (e.key === "Escape" && modal?.hasAttribute("open")) {
closeModal(modal)
// Remove scroll lock if present
document.body.style.overflow = ''
document.body.style.overflow = ""
}
})
</script>
<style is:global>
#version-modal > * {
font-family: 'Bricolage Grotesque', sans-serif !important;
font-family: "Bricolage Grotesque", sans-serif !important;
}
</style>

View file

@ -1,8 +1,8 @@
---
import Features from '~/components/Features.astro'
import Layout from '~/layouts/Layout.astro'
import { getLocale, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
import Features from "~/components/Features.astro"
import Layout from "~/layouts/Layout.astro"
import { getLocale, getUI } from "~/utils/i18n"
export { getStaticPaths } from "~/utils/i18n"
const locale = getLocale(Astro)
@ -14,10 +14,6 @@ const {
<Layout title={layout.welcome.title} description={layout.welcome.description}>
<main class="container">
<Features
title1={welcome.title[0]}
title2={welcome.title[1]}
title3={welcome.title[2]}
/>
<Features title1={welcome.title[0]} title2={welcome.title[1]} title3={welcome.title[2]} />
</main>
</Layout>

View file

@ -1,16 +1,16 @@
---
import Button from '~/components/Button.astro'
import Description from '~/components/Description.astro'
import SocialMediaStrip from '~/components/SocialMediaStrip.astro'
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
import Layout from '~/layouts/Layout.astro'
import Button from "~/components/Button.astro"
import Description from "~/components/Description.astro"
import SocialMediaStrip from "~/components/SocialMediaStrip.astro"
import ArrowRightIcon from "~/icons/ArrowRightIcon.astro"
import Layout from "~/layouts/Layout.astro"
import whatsNewVideo from '~/assets/whats-new.mp4'
import Video from '~/components/Video.astro'
import { releaseNotes } from '~/release-notes'
import whatsNewText from '~/release-notes/whats-new.json'
import { getLocale, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n'
import whatsNewVideo from "~/assets/whats-new.mp4"
import Video from "~/components/Video.astro"
import { releaseNotes } from "~/release-notes"
import whatsNewText from "~/release-notes/whats-new.json"
import { getLocale, getUI } from "~/utils/i18n"
export { getStaticPaths } from "~/utils/i18n"
const latestVersion = releaseNotes[0]
@ -22,40 +22,27 @@ const {
} = getUI(locale)
// Just redirect to the release notes if we are in a patch version
if (latestVersion.version.split('.').length > 2 && whatsNewText[1] !== latestVersion.version) {
if (latestVersion.version.split(".").length > 2 && whatsNewText[1] !== latestVersion.version) {
return Astro.redirect(`/release-notes#${latestVersion.version}`)
}
---
<Layout
title={layout.whatsNew.title.replace(
'{latestVersion.version}',
latestVersion.version,
)}
>
<Layout title={layout.whatsNew.title.replace("{latestVersion.version}", latestVersion.version)}>
<main
class="xl:mt-22 container flex flex-col gap-12 py-12 xl:grid xl:min-h-[calc(100vh-12rem)] xl:grid-cols-[2fr_3fr]"
>
<div class="flex flex-col gap-8">
<div>
<Description class="text-5xl font-bold md:text-6xl"
>{
whatsNew.title.replace(
'{latestVersion.version}',
latestVersion.version,
)
}</Description
>{whatsNew.title.replace("{latestVersion.version}", latestVersion.version)}</Description
>
<Description>{latestVersion.date}</Description>
</div>
<div>
<Fragment set:html={whatsNewText[0].replace(/\n/g, '<br>')} />
<Fragment set:html={whatsNewText[0].replace(/\n/g, "<br>")} />
</div>
<ul class="hidden list-disc flex-col gap-2 xl:container xl:flex">
<a
href="https://github.com/zen-browser/desktop/issues/new/choose"
target="_blank"
>
<a href="https://github.com/zen-browser/desktop/issues/new/choose" target="_blank">
<li>
<Description class="text-base font-bold">
{whatsNew.reportIssue}

View file

@ -1,6 +1,6 @@
import releaseNotesStable from './release-notes/stable.json'
import releaseNotesStable from "./release-notes/stable.json"
interface FixWithIssue {
type FixWithIssue = {
description: string
issue?: number
}
@ -9,7 +9,7 @@ type Fix = string | FixWithIssue
export type BreakingChange = string | { description: string; link: string }
export interface ReleaseNote {
export type ReleaseNote = {
version: string
date?: string // optional for twilight
extra?: string
@ -25,12 +25,12 @@ export interface ReleaseNote {
}
export const releaseNotes: ReleaseNote[] = releaseNotesStable.reverse()
export { default as releaseNotesTwilight } from './release-notes/twilight.json'
export { default as releaseNotesTwilight } from "./release-notes/twilight.json"
export function getReleaseNoteFirefoxVersion(releaseNote: ReleaseNote): string | null {
// Check if "firefox" is on the feature list
for (const feature of releaseNote.features || []) {
if (feature.toLowerCase().includes('firefox')) {
if (feature.toLowerCase().includes("firefox")) {
// may be X or X.X or X.X.X
const match = feature.match(/(\d+(\.\d+){0,2})/)
if (match) {

View file

@ -601,7 +601,10 @@
"version": "1.0.0-a.30",
"date": "26/08/2024",
"extra": "This release is the thirtieth alpha release of the 1.0.0-alpha series.",
"features": ["Added support for 24 more languages!", "Update installed mods from the browser settings"],
"features": [
"Added support for 24 more languages!",
"Update installed mods from the browser settings"
],
"fixes": [
{
"description": "Letterboxing option is missing",
@ -924,7 +927,11 @@
"date": "24/09/2024",
"workflowId": 11020784612,
"extra": "This update is a small patch to fix some issues that weren't addressed in the previous release!",
"features": ["Moved application menu button to the right", "Added new shortcuts", "Collapsed tab sidebar is now smaller"],
"features": [
"Moved application menu button to the right",
"Added new shortcuts",
"Collapsed tab sidebar is now smaller"
],
"fixes": [
{
"description": "Fixed issue with hovering over window control buttons (macOS)"
@ -949,7 +956,9 @@
"Improved Expand Tabs on Hover layout"
],
"themeChanges": ["Toggle inputs will not use the themed tertiary color"],
"breakingChanges": ["The keyboard shortcuts will be overriden by the defaults ones in this update"],
"breakingChanges": [
"The keyboard shortcuts will be overriden by the defaults ones in this update"
],
"fixes": [
{
"description": "Fixed Firefox add-ons not updating",
@ -1121,7 +1130,10 @@
"description": "Fixed about page linking 'global Community' to a Mozilla page"
}
],
"features": ["About page will now display the Firefox version used", "Disabled forcing container grouping for workspaces"]
"features": [
"About page will now display the Firefox version used",
"Disabled forcing container grouping for workspaces"
]
},
{
"version": "1.0.1-a.11",
@ -1258,7 +1270,9 @@
"description": "Fixed sidebar webpanels being in a darker contrast"
}
],
"features": ["Added a confirmation dialog when the gradient generator has successfully saved the gradient"]
"features": [
"Added a confirmation dialog when the gradient generator has successfully saved the gradient"
]
},
{
"version": "1.0.1-a.15",
@ -2097,7 +2111,10 @@
"date": "30/01/2025",
"workflowId": 13062083313,
"extra": "Quick fix for a critical bug that was introduced in the previous release.",
"fixes": ["Fixed the browser not opening when having multiple windows", "Fixed macos fullscreen having a weird shadow"]
"fixes": [
"Fixed the browser not opening when having multiple windows",
"Fixed macos fullscreen having a weird shadow"
]
},
{
"version": "1.7.5b",
@ -2154,7 +2171,9 @@
"Fixed opening glance tabs on essentials messing up the sidebar",
"Fixed pinned tabs appearing on normal container after a restart"
],
"features": ["Tabs can now be dragged into pinned tabs by dragging them into the workspace indicator"],
"features": [
"Tabs can now be dragged into pinned tabs by dragging them into the workspace indicator"
],
"workflowId": 13209591935,
"date": "08/02/2025"
},
@ -2684,7 +2703,10 @@
"'All tabs' menu not showing any text when collapsed toolbar is enabled."
],
"security": "https://www.mozilla.org/en-US/security/advisories/mfsa2025-36/",
"features": ["Updated to Firefox 138.0.4", "Better compact mode support for multiple toolbars."],
"features": [
"Updated to Firefox 138.0.4",
"Better compact mode support for multiple toolbars."
],
"knownIssues": ["Selecting a tab on private mode doesn't scroll to make the tab visible."],
"themeChanges": [
"Changed the layout of workspaces and their icons internally to provide a more stable layout that doesn't require floating elements. We finally managed to get it to how we wanted it to be, so it will change less in the future."

View file

@ -1,100 +1,101 @@
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
import { beforeEach, describe, expect, it } from 'vitest'
import Button from '~/components/Button.astro'
import { experimental_AstroContainer as AstroContainer } from "astro/container"
import { beforeEach, describe, expect, it } from "vitest"
describe('<Button />', () => {
import Button from "~/components/Button.astro"
describe("<Button />", () => {
let container: Awaited<ReturnType<typeof AstroContainer.create>>
beforeEach(async () => {
container = await AstroContainer.create()
})
describe('as <button>', () => {
it('renders default <button> with slot', async () => {
describe("as <button>", () => {
it("renders default <button> with slot", async () => {
const result = await container.renderToString(Button, {
props: {},
slots: { default: 'Click me' },
slots: { default: "Click me" },
})
expect(result).toContain('<button')
expect(result).toContain('Click me')
expect(result).toContain("<button")
expect(result).toContain("Click me")
})
it.each([
['isPrimary', { isPrimary: true }, 'bg-dark'],
['isAlert', { isAlert: true }, 'bg-red-300'],
['isBordered', { isBordered: true }, 'border-2'],
])('applies %s style', async (_label, propObj, expectedClass) => {
["isPrimary", { isPrimary: true }, "bg-dark"],
["isAlert", { isAlert: true }, "bg-red-300"],
["isBordered", { isBordered: true }, "border-2"],
])("applies %s style", async (_label, propObj, expectedClass) => {
const result = await container.renderToString(Button, {
props: { ...propObj },
slots: { default: 'Test' },
slots: { default: "Test" },
})
expect(result).toContain('<button')
expect(result).toContain("<button")
expect(result).toContain(expectedClass)
})
it('applies id and extra props', async () => {
it("applies id and extra props", async () => {
const result = await container.renderToString(Button, {
props: {
id: 'my-btn',
extra: { 'data-test': 'foo' },
id: "my-btn",
extra: { "data-test": "foo" },
},
slots: { default: 'Test' },
slots: { default: "Test" },
})
expect(result).toContain('id="my-btn"')
expect(result).toContain('data-test="foo"')
})
})
describe('as <a>', () => {
it('renders <a> with slot and href', async () => {
describe("as <a>", () => {
it("renders <a> with slot and href", async () => {
const result = await container.renderToString(Button, {
props: { href: '/link' },
slots: { default: 'Go' },
props: { href: "/link" },
slots: { default: "Go" },
})
expect(result).toContain('<a')
expect(result).toContain('Go')
expect(result).toContain("<a")
expect(result).toContain("Go")
expect(result).toContain('href="/en/link"')
})
it.each([
['isPrimary', { isPrimary: true }, 'bg-dark'],
['isAlert', { isAlert: true }, 'bg-red-300'],
['isBordered', { isBordered: true }, 'border-2'],
])('applies %s style', async (_label, propObj, expectedClass) => {
["isPrimary", { isPrimary: true }, "bg-dark"],
["isAlert", { isAlert: true }, "bg-red-300"],
["isBordered", { isBordered: true }, "border-2"],
])("applies %s style", async (_label, propObj, expectedClass) => {
const result = await container.renderToString(Button, {
props: { href: '/link', ...propObj },
slots: { default: 'Test' },
props: { href: "/link", ...propObj },
slots: { default: "Test" },
})
expect(result).toContain('<a')
expect(result).toContain("<a")
expect(result).toContain(expectedClass)
})
it('applies id and extra props', async () => {
it("applies id and extra props", async () => {
const result = await container.renderToString(Button, {
props: {
href: '/link',
id: 'my-link',
extra: { 'data-test': 'bar' },
href: "/link",
id: "my-link",
extra: { "data-test": "bar" },
},
slots: { default: 'Test' },
slots: { default: "Test" },
})
expect(result).toContain('id="my-link"')
expect(result).toContain('data-test="bar"')
})
})
it('applies custom className', async () => {
it("applies custom className", async () => {
const result = await container.renderToString(Button, {
props: { class: 'custom-class' },
slots: { default: 'Test' },
props: { class: "custom-class" },
slots: { default: "Test" },
})
expect(result).toContain('custom-class')
expect(result).toContain("custom-class")
})
it('uses locale path for href', async () => {
it("uses locale path for href", async () => {
const result = await container.renderToString(Button, {
props: { href: '/foo' },
slots: { default: 'Test' },
props: { href: "/foo" },
slots: { default: "Test" },
})
expect(result).toContain('href="/en/foo"')
})

View file

@ -1,47 +1,48 @@
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
import { beforeEach, describe, expect, it } from 'vitest'
import ButtonCard from '~/components/download/ButtonCard.astro'
import { experimental_AstroContainer as AstroContainer } from "astro/container"
import { beforeEach, describe, expect, it } from "vitest"
describe('<ButtonCard />', () => {
import ButtonCard from "~/components/download/ButtonCard.astro"
describe("<ButtonCard />", () => {
let container: Awaited<ReturnType<typeof AstroContainer.create>>
beforeEach(async () => {
container = await AstroContainer.create()
})
it('renders with required props', async () => {
it("renders with required props", async () => {
const result = await container.renderToString(ButtonCard, {
props: {
label: 'Download',
href: '/download',
label: "Download",
href: "/download",
},
})
expect(result).toContain('Download')
expect(result).toContain("Download")
expect(result).toContain('href="/download"')
expect(result).not.toContain('Show SHA-256')
expect(result).not.toContain("Show SHA-256")
})
it('renders with checksum', async () => {
it("renders with checksum", async () => {
const result = await container.renderToString(ButtonCard, {
props: {
label: 'Download',
href: '/download',
checksum: 'sha256sum',
label: "Download",
href: "/download",
checksum: "sha256sum",
},
})
expect(result).toContain('Show SHA-256')
expect(result).toContain('sha256sum')
expect(result).toContain('Copy')
expect(result).toContain("Show SHA-256")
expect(result).toContain("sha256sum")
expect(result).toContain("Copy")
})
it('renders with variant', async () => {
it("renders with variant", async () => {
const result = await container.renderToString(ButtonCard, {
props: {
label: 'Download',
href: '/download',
variant: 'flathub',
label: "Download",
href: "/download",
variant: "flathub",
},
})
expect(result).toContain('Download')
expect(result).toContain('Beta')
expect(result).toContain("Download")
expect(result).toContain("Beta")
})
})

View file

@ -1,125 +1,126 @@
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
import { beforeEach, describe, expect, it } from 'vitest'
import PlatformDownload from '~/components/download/PlatformDownload.astro'
import { experimental_AstroContainer as AstroContainer } from "astro/container"
import { beforeEach, describe, expect, it } from "vitest"
const mockIcon = ['<svg></svg>']
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' } },
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 />', () => {
describe("<PlatformDownload />", () => {
let container: Awaited<ReturnType<typeof AstroContainer.create>>
beforeEach(async () => {
container = await AstroContainer.create()
})
it('renders mac platform', async () => {
it("renders mac platform", async () => {
const result = await container.renderToString(PlatformDownload, {
props: {
platform: 'mac',
platform: "mac",
icon: mockIcon,
title: 'Mac Title',
description: 'Mac Desc',
title: "Mac Title",
description: "Mac Desc",
releases: mockReleases,
},
})
expect(result).toContain('Mac Title')
expect(result).toContain('Mac Desc')
expect(result).toContain('Universal')
expect(result).toContain("Mac Title")
expect(result).toContain("Mac Desc")
expect(result).toContain("Universal")
})
it('renders windows platform', async () => {
it("renders windows platform", async () => {
const result = await container.renderToString(PlatformDownload, {
props: {
platform: 'windows',
platform: "windows",
icon: mockIcon,
title: 'Win Title',
description: 'Win Desc',
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')
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 () => {
it("renders linux platform with flathub and tarball", async () => {
const linuxReleases = {
flathub: { all: { label: 'Flathub', link: '/flathub' } },
flathub: { all: { label: "Flathub", link: "/flathub" } },
x86_64: {
tarball: {
label: 'Tarball x86_64',
link: '/tarball-x86_64',
checksum: 'sha256',
label: "Tarball x86_64",
link: "/tarball-x86_64",
checksum: "sha256",
},
},
}
const result = await container.renderToString(PlatformDownload, {
props: {
platform: 'linux',
platform: "linux",
icon: mockIcon,
title: 'Linux Title',
description: 'Linux Desc',
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')
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 () => {
it("renders linux platform with all branches", async () => {
const linuxReleases = {
flathub: { all: { label: 'Flathub', link: '/flathub' } },
flathub: { all: { label: "Flathub", link: "/flathub" } },
x86_64: {
tarball: {
label: 'Tarball x86_64',
link: '/tarball-x86_64',
checksum: 'sha256',
label: "Tarball x86_64",
link: "/tarball-x86_64",
checksum: "sha256",
},
},
aarch64: {
tarball: {
label: 'Tarball ARM64',
link: '/tarball-arm64',
checksum: 'sha256-arm64',
label: "Tarball ARM64",
link: "/tarball-arm64",
checksum: "sha256-arm64",
},
},
}
const result = await container.renderToString(PlatformDownload, {
props: {
platform: 'linux',
platform: "linux",
icon: mockIcon,
title: 'Linux Title',
description: 'Linux Desc',
title: "Linux Title",
description: "Linux Desc",
releases: linuxReleases,
},
})
// Test basic content
expect(result).toContain('Linux Title')
expect(result).toContain('Linux Desc')
expect(result).toContain("Linux Title")
expect(result).toContain("Linux Desc")
// Test Flathub section
expect(result).toContain('Flathub')
expect(result).toContain('/flathub')
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')
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')
expect(result).toContain("ARM64")
expect(result).toContain("Tarball ARM64")
expect(result).toContain("/tarball-arm64")
expect(result).toContain("sha256-arm64")
})
})

View file

@ -1,22 +1,23 @@
import { describe, expect, it } from 'vitest'
import { getReleasesWithChecksums } from '~/components/download/release-data'
import { describe, expect, it } from "vitest"
describe('getReleasesWithChecksums', () => {
it('returns correct structure with checksums', () => {
import { getReleasesWithChecksums } from "~/components/download/release-data"
describe("getReleasesWithChecksums", () => {
it("returns correct structure with checksums", () => {
const checksums = {
'zen.macos-universal.dmg': 'macsum',
'zen.installer.exe': 'winsum',
'zen.installer-arm64.exe': 'winarmsum',
'zen.linux-x86_64.tar.xz': 'linx86sum',
'zen.linux-aarch64.tar.xz': 'linaarchsum',
"zen.macos-universal.dmg": "macsum",
"zen.installer.exe": "winsum",
"zen.installer-arm64.exe": "winarmsum",
"zen.linux-x86_64.tar.xz": "linx86sum",
"zen.linux-aarch64.tar.xz": "linaarchsum",
}
const releases = getReleasesWithChecksums(checksums)
expect(releases.macos.universal.checksum).toBe('macsum')
expect(releases.windows.x86_64.checksum).toBe('winsum')
expect(releases.windows.arm64.checksum).toBe('winarmsum')
expect(releases.linux.x86_64.tarball.checksum).toBe('linx86sum')
expect(releases.linux.aarch64.tarball.checksum).toBe('linaarchsum')
expect(releases.linux.flathub.all.label).toBe('Flathub')
expect(releases.linux.flathub.all.link).toBe('https://flathub.org/apps/app.zen_browser.zen')
expect(releases.macos.universal.checksum).toBe("macsum")
expect(releases.windows.x86_64.checksum).toBe("winsum")
expect(releases.windows.arm64.checksum).toBe("winarmsum")
expect(releases.linux.x86_64.tarball.checksum).toBe("linx86sum")
expect(releases.linux.aarch64.tarball.checksum).toBe("linaarchsum")
expect(releases.linux.flathub.all.label).toBe("Flathub")
expect(releases.linux.flathub.all.link).toBe("https://flathub.org/apps/app.zen_browser.zen")
})
})

View file

@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test'
import type { BrowserContextOptions, Page } from '@playwright/test'
import { getReleasesWithChecksums } from '~/components/download/release-data'
import { CONSTANT } from '~/constants'
import { expect, test, type BrowserContextOptions, type Page } from "@playwright/test"
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) =>
@ -12,90 +12,100 @@ const getPlatformButton = (page: Page, platform: string) =>
page.locator(`button.platform-selector[data-platform='${platform}']`)
// Helper to get the platform download link
const getPlatformDownloadLink = (page: Page, platform: string, label: string) =>
const _ = (page: Page, platform: string, label: string) =>
page.locator(`#${platform}-downloads .download-link:has-text('${label}')`)
const platformConfigs: { name: string; userAgent: string; platform: string }[] = [
{
name: 'windows',
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',
"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",
},
{
name: 'mac',
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',
"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",
},
{
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',
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",
},
]
test.describe('Download page default tab per platform', () => {
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 }) => {
const context = await browser.newContext({
userAgent,
locale: 'en-US',
locale: "en-US",
platform,
} as BrowserContextOptions)
const page = await context.newPage()
await page.goto('/download')
await page.goto("/download")
await expect(getPlatformSection(page, name)).toBeVisible()
await expect(getPlatformButton(page, name)).toHaveAttribute('data-active', 'true')
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)) {
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 expect(getPlatformButton(page, other.name)).not.toHaveAttribute("data-active", "true")
}
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']
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')
await expect(getPlatformButton(page, platform)).toHaveAttribute("data-active", "true")
// other platform sections should be hidden
for (const otherPlatform of platforms.filter((p) => p !== platform)) {
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')
await expect(getPlatformButton(page, otherPlatform)).not.toHaveAttribute(
"data-active",
"true"
)
}
}
})
})
test.describe('Download page download links', () => {
test.describe("Download page download links", () => {
const releases = getReleasesWithChecksums(CONSTANT.CHECKSUMS)
function getPlatformLinks(releases: ReturnType<typeof getReleasesWithChecksums>) {
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],
linux: [
releases.linux.x86_64.tarball,
releases.linux.aarch64.tarball,
releases.linux.flathub.all,
],
}
}
test('all platform download links are correct', async ({ page }) => {
const platforms = ['windows', 'mac', 'linux']
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')
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]) {
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)
await expect(downloadLink).toHaveAttribute('href', link)
await expect(downloadLink).toHaveAttribute("href", link)
}
}
})

View file

@ -1,11 +1,11 @@
import { expect, test } from '@playwright/test'
import { expect, test } from "@playwright/test"
test('clicking back button navigates to previous page', async ({ page }) => {
await page.goto('/mods?created=asc')
test("clicking back button navigates to previous page", async ({ page }) => {
await page.goto("/mods?created=asc")
const currentUrl = page.url()
const modCards = await page.locator('.mod-card').all()
const modCards = await page.locator(".mod-card").all()
await modCards[0].click()
await page.getByRole('button', { name: 'Back' }).click()
await page.getByRole("button", { name: "Back" }).click()
await page.waitForURL(currentUrl)
expect(page.url()).toStrictEqual(currentUrl)
})

View file

@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test'
import { expect, test } from "@playwright/test"
test('all routes do not return 404', async ({ page }) => {
const routes = ['/', '/welcome', '/about', '/privacy-policy', '/download', '/donate', '/whatsnew']
test("all routes do not return 404", async ({ page }) => {
const routes = ["/", "/welcome", "/about", "/privacy-policy", "/download", "/donate", "/whatsnew"]
for (const route of routes) {
const response = await page.goto(route)
expect(response?.status()).not.toBe(404)

View file

@ -1,8 +1,9 @@
import { vi } from 'vitest'
import translation from '~/i18n/en/translation.json'
import { vi } from "vitest"
vi.mock('~/utils/i18n', () => ({
getLocale: () => 'en',
import translation from "~/i18n/en/translation.json"
vi.mock("~/utils/i18n", () => ({
getLocale: () => "en",
getPath: () => (href: string) => `/en${href}`,
getUI: () => translation,
}))

View file

@ -1,4 +1,4 @@
import { CONSTANT } from '~/constants'
import { CONSTANT } from "~/constants"
/**
* Fetches the latest release notes from GitHub and parses the SHA-256 checksums.
@ -8,11 +8,11 @@ export async function getChecksums() {
if (import.meta.env.DEV) {
return CONSTANT.CHECKSUMS
}
const res = await fetch('https://api.github.com/repos/zen-browser/desktop/releases/latest', {
const res = await fetch("https://api.github.com/repos/zen-browser/desktop/releases/latest", {
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'zen-browser-checksum-fetcher',
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "zen-browser-checksum-fetcher",
},
})
if (!res.ok) throw new Error(`Failed to fetch GitHub release: ${res.statusText}`)
@ -23,7 +23,7 @@ export async function getChecksums() {
const match = body.match(/File Checksums \(SHA-256\)[\s\S]*?```([\s\S]*?)```/)
const checksums: Record<string, string> = {}
if (match?.[1]) {
for (const line of match[1].split('\n')) {
for (const line of match[1].split("\n")) {
const [hash, filename] = line.trim().split(/\s+/, 2)
if (hash && filename) checksums[filename] = hash
}

View file

@ -1,6 +1,7 @@
import type { GetStaticPaths } from 'astro'
import { CONSTANT } from '~/constants'
import UI_EN from '~/i18n/en/translation.json'
import { type GetStaticPaths } from "astro"
import { CONSTANT } from "~/constants"
import UI_EN from "~/i18n/en/translation.json"
/**
* Represents the available locales in the application
@ -15,7 +16,7 @@ export type Locale = (typeof locales)[number]
*/
export const getPath = (locale?: Locale) => (path: string) => {
if (locale && locale !== CONSTANT.I18N.DEFAULT_LOCALE && !path.startsWith(`/${locale}`)) {
return `/${locale}${path.startsWith('/') ? '' : '/'}${path}`
return `/${locale}${path.startsWith("/") ? "" : "/"}${path}`
}
return path
}
@ -44,7 +45,9 @@ export const locales = CONSTANT.I18N.LOCALES.map(({ value }) => value)
* List of locales excluding the default locale
* @type {Locale[]}
*/
const otherLocales = CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE)
const otherLocales = CONSTANT.I18N.LOCALES.filter(
({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE
)
/**
* Retrieves locales other than the default locale
@ -83,10 +86,10 @@ export const getUI = (locale?: Locale | string): UI => {
*/
function deepMerge<T extends object>(defaultObj: T, overrideObj: Partial<T>): T {
// Handle non-object cases
if (typeof defaultObj !== 'object' || defaultObj === null) {
if (typeof defaultObj !== "object" || defaultObj === null) {
return (overrideObj ?? defaultObj) as T
}
if (typeof overrideObj !== 'object' || overrideObj === null) {
if (typeof overrideObj !== "object" || overrideObj === null) {
return (overrideObj ?? defaultObj) as T
}
@ -94,7 +97,7 @@ export const getUI = (locale?: Locale | string): UI => {
const result = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj }
// Merge properties from the default object
for (const key of Object.keys(defaultObj) as Array<keyof T>) {
for (const key of Object.keys(defaultObj) as (keyof T)[]) {
const defaultValue = defaultObj[key]
const overrideValue = overrideObj[key]
@ -102,11 +105,14 @@ export const getUI = (locale?: Locale | string): UI => {
if (
defaultValue !== null &&
overrideValue !== null &&
typeof defaultValue === 'object' &&
typeof overrideValue === 'object'
typeof defaultValue === "object" &&
typeof overrideValue === "object"
) {
// Type assertion to handle nested merging
;(result as Record<keyof T, unknown>)[key] = deepMerge(defaultValue as object, overrideValue as Partial<object>)
;(result as Record<keyof T, unknown>)[key] = deepMerge(
defaultValue as object,
overrideValue as Partial<object>
)
} else if (overrideValue !== undefined) {
// Override with the new value if it exists
;(result as Record<keyof T, unknown>)[key] = overrideValue
@ -114,7 +120,7 @@ export const getUI = (locale?: Locale | string): UI => {
}
// Add any new properties from overrideObj
for (const key of Object.keys(overrideObj) as Array<keyof T>) {
for (const key of Object.keys(overrideObj) as (keyof T)[]) {
if (!(key in defaultObj)) {
;(result as Record<keyof T, unknown>)[key] = overrideObj[key]
}
@ -137,12 +143,14 @@ export const getStaticPaths = (() => {
params: { locale: undefined },
props: { locale: CONSTANT.I18N.DEFAULT_LOCALE },
},
...CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE).map(({ value }) => ({
...CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE).map(
({ value }) => ({
params: { locale: value },
props: {
locale: value,
},
})),
})
),
]
}) satisfies GetStaticPaths

View file

@ -1,5 +1,5 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs))

View file

@ -2,7 +2,7 @@
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact",
"jsxImportSource": "react",
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]