mirror of
https://github.com/zen-browser/www.git
synced 2025-07-08 17:30:01 +02:00
Merge f615a43af5
into 1937be58a6
This commit is contained in:
commit
ab0873f3a9
76 changed files with 7794 additions and 2749 deletions
28
.eslint/astro.ts
Normal file
28
.eslint/astro.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { type Linter } from 'eslint'
|
||||||
|
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
||||||
|
|
||||||
|
import { astroFiles } from './shared'
|
||||||
|
|
||||||
|
export const astroConfig: Linter.Config = {
|
||||||
|
name: 'eslint/astro',
|
||||||
|
files: astroFiles,
|
||||||
|
plugins: {
|
||||||
|
'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
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
},
|
||||||
|
}
|
73
.eslint/base.ts
Normal file
73
.eslint/base.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { type Linter } from 'eslint'
|
||||||
|
|
||||||
|
import { sharedFiles } from './shared'
|
||||||
|
|
||||||
|
export const baseConfig: Linter.Config = {
|
||||||
|
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',
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
},
|
||||||
|
}
|
21
.eslint/config-files.ts
Normal file
21
.eslint/config-files.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { type Linter } from "eslint";
|
||||||
|
|
||||||
|
import { configFiles } from "./shared";
|
||||||
|
|
||||||
|
export const configFilesConfig: Linter.Config = {
|
||||||
|
name: "eslint/config-files",
|
||||||
|
files: configFiles,
|
||||||
|
rules: {
|
||||||
|
"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",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
|
"prefer-const": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
},
|
||||||
|
};
|
64
.eslint/import.ts
Normal file
64
.eslint/import.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { type Linter } from 'eslint'
|
||||||
|
// @ts-expect-error - no types available
|
||||||
|
import importPlugin from 'eslint-plugin-import'
|
||||||
|
|
||||||
|
import { sharedFiles } from './shared'
|
||||||
|
|
||||||
|
export const importConfigArray: Linter.Config[] = [
|
||||||
|
{
|
||||||
|
name: 'eslint/import',
|
||||||
|
files: sharedFiles,
|
||||||
|
plugins: {
|
||||||
|
import: importPlugin,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...importPlugin.configs.recommended.rules,
|
||||||
|
...importPlugin.configs.typescript.rules,
|
||||||
|
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: [
|
||||||
|
'builtin',
|
||||||
|
'external',
|
||||||
|
'internal',
|
||||||
|
['parent', 'sibling'],
|
||||||
|
'index',
|
||||||
|
'object',
|
||||||
|
'type',
|
||||||
|
],
|
||||||
|
'newlines-between': 'always',
|
||||||
|
alphabetize: {
|
||||||
|
order: 'asc',
|
||||||
|
caseInsensitive: true,
|
||||||
|
},
|
||||||
|
pathGroups: [
|
||||||
|
{
|
||||||
|
pattern: '@/**',
|
||||||
|
group: 'internal',
|
||||||
|
position: 'before',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
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
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
},
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
'import/parsers': {
|
||||||
|
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
36
.eslint/javascript.ts
Normal file
36
.eslint/javascript.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { type Linter } from 'eslint'
|
||||||
|
|
||||||
|
import { javascriptFiles } from './shared'
|
||||||
|
|
||||||
|
export const javascriptConfig: Linter.Config = {
|
||||||
|
name: 'eslint/javascript',
|
||||||
|
files: javascriptFiles,
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
}
|
31
.eslint/jsx-a11y.ts
Normal file
31
.eslint/jsx-a11y.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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',
|
||||||
|
files: [
|
||||||
|
...astroFiles,
|
||||||
|
...typescriptFiles.filter(f => f.includes('tsx')),
|
||||||
|
...javascriptFiles.filter(f => f.includes('jsx')),
|
||||||
|
],
|
||||||
|
plugins: {
|
||||||
|
'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',
|
||||||
|
},
|
||||||
|
}
|
59
.eslint/react.ts
Normal file
59
.eslint/react.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
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 = {
|
||||||
|
name: "eslint/react",
|
||||||
|
files: [
|
||||||
|
...typescriptFiles.filter((f) => f.includes("tsx")),
|
||||||
|
...javascriptFiles.filter((f) => f.includes("jsx")),
|
||||||
|
],
|
||||||
|
plugins: {
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
|
||||||
|
"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",
|
||||||
|
"react/no-unescaped-entities": "off",
|
||||||
|
"react/jsx-key": "error",
|
||||||
|
"react/jsx-no-useless-fragment": "error",
|
||||||
|
"react/self-closing-comp": "error",
|
||||||
|
"react/jsx-boolean-value": ["error", "never"],
|
||||||
|
"react/jsx-curly-brace-presence": [
|
||||||
|
"error",
|
||||||
|
{ props: "never", children: "never" },
|
||||||
|
],
|
||||||
|
"react/function-component-definition": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
namedComponents: "arrow-function",
|
||||||
|
unnamedComponents: "arrow-function",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "18.2", // React version
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reactHooksConfig: Linter.Config = {
|
||||||
|
name: "eslint/react-hooks",
|
||||||
|
files: [
|
||||||
|
...typescriptFiles.filter((f) => f.includes("tsx")),
|
||||||
|
...javascriptFiles.filter((f) => f.includes("jsx")),
|
||||||
|
],
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
};
|
35
.eslint/shared.ts
Normal file
35
.eslint/shared.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
export const sharedFiles = [
|
||||||
|
'**/*.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}',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const astroFiles = ['**/*.astro']
|
||||||
|
|
||||||
|
export const typescriptFiles = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '**/*.d.ts']
|
||||||
|
|
||||||
|
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.*',
|
||||||
|
]
|
20
.eslint/test.ts
Normal file
20
.eslint/test.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { type Linter } from 'eslint'
|
||||||
|
|
||||||
|
import { sharedTestFiles } from './shared'
|
||||||
|
|
||||||
|
export const testConfig: Linter.Config = {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
}
|
45
.eslint/typescript.ts
Normal file
45
.eslint/typescript.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { type Linter } from 'eslint'
|
||||||
|
|
||||||
|
import { typescriptFiles } from './shared'
|
||||||
|
|
||||||
|
export function createTypescriptConfig(tsConfigPath: string): Linter.Config {
|
||||||
|
return {
|
||||||
|
name: 'eslint/typescript',
|
||||||
|
files: typescriptFiles,
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
project: tsConfigPath,
|
||||||
|
tsconfigRootDir: process.cwd(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// Basic TypeScript rules that work without type information
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
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/consistent-type-definitions': ['error', 'type'],
|
||||||
|
|
||||||
|
// Override base rules for TypeScript
|
||||||
|
'no-unused-vars': 'off', // Handled by TypeScript
|
||||||
|
'no-undef': 'off', // TypeScript handles this
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
27
.github/workflows/ci-pipeline.yml
vendored
27
.github/workflows/ci-pipeline.yml
vendored
|
@ -3,6 +3,10 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-pipeline-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -23,7 +27,7 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
biome:
|
eslint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: setup
|
needs: setup
|
||||||
steps:
|
steps:
|
||||||
|
@ -39,8 +43,25 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
node_modules
|
node_modules
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
|
||||||
- name: Run Biome check
|
- name: Run Eslint check
|
||||||
run: npx biome check ./src
|
run: npm run lint
|
||||||
|
|
||||||
|
prettier:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: setup
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
- name: Restore node_modules from cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
node_modules
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
|
||||||
|
- name: Run Prettier check
|
||||||
|
run: npm run format:check
|
||||||
|
|
||||||
vitest:
|
vitest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
47
.prettierignore
Normal file
47
.prettierignore
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Test outputs
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|
||||||
|
# Package files
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Config files that should maintain their format
|
||||||
|
wrangler.toml
|
||||||
|
|
||||||
|
# Binary files
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
|
*.gif
|
||||||
|
*.svg
|
||||||
|
*.ico
|
||||||
|
*.woff
|
||||||
|
*.woff2
|
||||||
|
*.ttf
|
||||||
|
*.eot
|
||||||
|
|
||||||
|
# IDE and OS
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Static assets
|
||||||
|
public/fonts/
|
||||||
|
public/favicon.ico
|
||||||
|
public/favicon.svg
|
44
.vscode/settings.json
vendored
Normal file
44
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"astro"
|
||||||
|
],
|
||||||
|
"eslint.format.enable": false,
|
||||||
|
"prettier.requireConfig": true,
|
||||||
|
"[astro]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"files.associations": {
|
||||||
|
"*.astro": "astro"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,11 @@
|
||||||
import tailwind from '@astrojs/tailwind'
|
import react from '@astrojs/react'
|
||||||
// @ts-check
|
|
||||||
import { defineConfig } from 'astro/config'
|
|
||||||
|
|
||||||
import preact from '@astrojs/preact'
|
|
||||||
|
|
||||||
import sitemap from '@astrojs/sitemap'
|
import sitemap from '@astrojs/sitemap'
|
||||||
|
import tailwind from '@astrojs/tailwind'
|
||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [tailwind(), preact({ compat: true }), sitemap()],
|
integrations: [tailwind(), react(), sitemap()],
|
||||||
site: 'https://zen-browser.app',
|
site: 'https://zen-browser.app',
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'en',
|
||||||
|
@ -18,4 +15,16 @@ export default defineConfig({
|
||||||
prefixDefaultLocale: false,
|
prefixDefaultLocale: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
vite: {
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
vendor: ['react', 'react-dom'],
|
||||||
|
astro: ['astro'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
61
biome.json
61
biome.json
|
@ -1,61 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
||||||
"organizeImports": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"nursery": {
|
|
||||||
"useSortedClasses": "info"
|
|
||||||
},
|
|
||||||
"suspicious": {
|
|
||||||
"noExplicitAny": "warn"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"indentWidth": 2,
|
|
||||||
"lineWidth": 128,
|
|
||||||
"useEditorconfig": true
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"ignore": ["node_modules", ".git", "dist"]
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"quoteStyle": "single",
|
|
||||||
"semicolons": "asNeeded"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"assists": {
|
|
||||||
"actions": {
|
|
||||||
"source": {
|
|
||||||
"sortJsxProps": "on"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vcs": {
|
|
||||||
"enabled": true,
|
|
||||||
"clientKind": "git",
|
|
||||||
"defaultBranch": "main",
|
|
||||||
"root": ".",
|
|
||||||
"useIgnoreFile": true
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"include": ["*.astro"],
|
|
||||||
"linter": {
|
|
||||||
"rules": {
|
|
||||||
"style": {
|
|
||||||
"useConst": "off",
|
|
||||||
"useImportType": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
94
eslint.config.ts
Normal file
94
eslint.config.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
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";
|
||||||
|
import { baseConfig } from "./.eslint/base";
|
||||||
|
import { configFilesConfig } from "./.eslint/config-files";
|
||||||
|
import { importConfigArray } from "./.eslint/import";
|
||||||
|
import { javascriptConfig } from "./.eslint/javascript";
|
||||||
|
import { jsxA11yConfig } from "./.eslint/jsx-a11y";
|
||||||
|
import { reactConfig, reactHooksConfig } from "./.eslint/react";
|
||||||
|
import { testConfig } from "./.eslint/test";
|
||||||
|
import { createTypescriptConfig } from "./.eslint/typescript";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const tsConfigPath = resolve(__dirname, "./tsconfig.json");
|
||||||
|
|
||||||
|
const ignoresConfig = {
|
||||||
|
name: "eslint/ignores",
|
||||||
|
ignores: [
|
||||||
|
// Build outputs
|
||||||
|
"**/dist/**",
|
||||||
|
"**/build/**",
|
||||||
|
"**/.astro/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
|
||||||
|
// Test outputs
|
||||||
|
"**/coverage/**",
|
||||||
|
"**/playwright-report/**",
|
||||||
|
"**/test-results/**",
|
||||||
|
|
||||||
|
// Config files that don't need linting
|
||||||
|
"**/*.config.js",
|
||||||
|
"**/*.config.mjs",
|
||||||
|
"**/wrangler.toml",
|
||||||
|
|
||||||
|
// Other common ignores
|
||||||
|
"**/.next/**",
|
||||||
|
"**/.nuxt/**",
|
||||||
|
"**/.output/**",
|
||||||
|
"**/.vercel/**",
|
||||||
|
"**/.netlify/**",
|
||||||
|
"**/public/**",
|
||||||
|
"**/*.min.js",
|
||||||
|
"**/*.d.ts",
|
||||||
|
"**/CHANGELOG.md",
|
||||||
|
"**/README.md",
|
||||||
|
],
|
||||||
|
} satisfies TSESLint.FlatConfig.Config;
|
||||||
|
|
||||||
|
const config: TSESLint.FlatConfig.ConfigArray = tseslint.config(
|
||||||
|
// Include .gitignore patterns
|
||||||
|
includeIgnoreFile(resolve(__dirname, ".gitignore")),
|
||||||
|
|
||||||
|
// Core configurations
|
||||||
|
ignoresConfig,
|
||||||
|
baseConfig,
|
||||||
|
// TypeScript ecosystem
|
||||||
|
...configs.strict,
|
||||||
|
...configs.stylistic,
|
||||||
|
createTypescriptConfig(tsConfigPath),
|
||||||
|
|
||||||
|
// Import management
|
||||||
|
...importConfigArray,
|
||||||
|
|
||||||
|
// Framework specific
|
||||||
|
reactConfig,
|
||||||
|
reactHooksConfig,
|
||||||
|
jsxA11yConfig,
|
||||||
|
|
||||||
|
// Astro specific
|
||||||
|
...astro.configs.recommended,
|
||||||
|
astroConfig,
|
||||||
|
|
||||||
|
// Language specific
|
||||||
|
javascriptConfig,
|
||||||
|
|
||||||
|
// Special cases
|
||||||
|
testConfig,
|
||||||
|
configFilesConfig,
|
||||||
|
|
||||||
|
// Prettier integration (must be last)
|
||||||
|
prettierConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
export default config;
|
8376
package-lock.json
generated
8376
package-lock.json
generated
File diff suppressed because it is too large
Load diff
46
package.json
46
package.json
|
@ -9,8 +9,10 @@
|
||||||
"preview": "astro preview --port 3000",
|
"preview": "astro preview --port 3000",
|
||||||
"wrangler": "wrangler",
|
"wrangler": "wrangler",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"lint": "biome lint ./src",
|
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.astro",
|
||||||
"format": "biome format ./src",
|
"lint:fix": "eslint ./src --ext .js,.jsx,.ts,.tsx,.astro --fix",
|
||||||
|
"format": "prettier --write ./src",
|
||||||
|
"format:check": "prettier --check ./src",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"test": "npx vitest run",
|
"test": "npx vitest run",
|
||||||
"test:coverage": "npx vitest --coverage",
|
"test:coverage": "npx vitest --coverage",
|
||||||
|
@ -19,7 +21,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.4",
|
||||||
"@astrojs/cloudflare": "^12.5.2",
|
"@astrojs/cloudflare": "^12.5.2",
|
||||||
"@astrojs/preact": "^4.0.11",
|
"@astrojs/react": "^4.3.0",
|
||||||
"@astrojs/rss": "^4.0.11",
|
"@astrojs/rss": "^4.0.11",
|
||||||
"@astrojs/sitemap": "^3.3.1",
|
"@astrojs/sitemap": "^3.3.1",
|
||||||
"@astrojs/tailwind": "^6.0.2",
|
"@astrojs/tailwind": "^6.0.2",
|
||||||
|
@ -27,6 +29,8 @@
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.1",
|
"@fortawesome/fontawesome-svg-core": "^6.7.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.7.1",
|
"@fortawesome/free-brands-svg-icons": "^6.7.1",
|
||||||
"@fortawesome/free-solid-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": "^5.7.10",
|
||||||
"astro-navbar": "^2.3.7",
|
"astro-navbar": "^2.3.7",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
|
@ -37,28 +41,56 @@
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"motion": "^11.13.5",
|
"motion": "^11.13.5",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"preact": "^10.26.2",
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.0",
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@eslint/compat": "^1.2.9",
|
||||||
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
"@eslint/js": "^9.27.0",
|
||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.52.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/eslint-plugin-jsx-a11y": "^6.10.0",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^22.15.18",
|
"@types/node": "^22.15.18",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
||||||
|
"@typescript-eslint/parser": "^8.33.0",
|
||||||
|
"@typescript-eslint/utils": "^8.33.0",
|
||||||
"@vitest/coverage-istanbul": "^3.1.3",
|
"@vitest/coverage-istanbul": "^3.1.3",
|
||||||
|
"eslint": "^9.27.0",
|
||||||
|
"eslint-config-prettier": "^10.1.5",
|
||||||
|
"eslint-import-resolver-typescript": "^4.4.1",
|
||||||
|
"eslint-plugin-astro": "^1.3.1",
|
||||||
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"lint-staged": "^15.2.7",
|
"lint-staged": "^15.2.7",
|
||||||
|
"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",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.1.3",
|
"vitest": "^3.1.3",
|
||||||
"wrangler": "^3.114.8"
|
"wrangler": "^4.17.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"jiti": "2.4.2"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"src/**/*.{ts,tsx,astro,js,jsx}": ["biome check --write ."]
|
"src/**/*.{ts,tsx,astro,js,jsx}": [
|
||||||
|
"eslint --fix",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"src/**/*.{json,md,css}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default defineConfig({
|
||||||
testDir: './src/tests',
|
testDir: './src/tests',
|
||||||
testIgnore: ['**.test.ts'],
|
testIgnore: ['**.test.ts'],
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: Boolean(process.env.CI),
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
|
58
prettier.config.js
Normal file
58
prettier.config.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* @type {import('prettier').Config}
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
// Basic formatting
|
||||||
|
printWidth: 100,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
semi: false,
|
||||||
|
singleQuote: true,
|
||||||
|
quoteProps: "as-needed",
|
||||||
|
trailingComma: "es5",
|
||||||
|
bracketSpacing: true,
|
||||||
|
bracketSameLine: false,
|
||||||
|
arrowParens: "avoid",
|
||||||
|
|
||||||
|
// Language-specific formatting
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: "*.astro",
|
||||||
|
options: {
|
||||||
|
parser: "astro",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["*.json", "*.jsonc"],
|
||||||
|
options: {
|
||||||
|
trailingComma: "none",
|
||||||
|
singleQuote: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["*.md", "*.mdx"],
|
||||||
|
options: {
|
||||||
|
printWidth: 80,
|
||||||
|
proseWrap: "never",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["*.yml", "*.yaml"],
|
||||||
|
options: {
|
||||||
|
singleQuote: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
plugins: [
|
||||||
|
"prettier-plugin-astro",
|
||||||
|
"prettier-plugin-tailwindcss", // Must be last
|
||||||
|
],
|
||||||
|
|
||||||
|
// Plugin-specific options
|
||||||
|
tailwindFunctions: ["clsx", "cn", "twMerge"],
|
||||||
|
|
||||||
|
// Astro-specific options
|
||||||
|
astroAllowShorthand: false,
|
||||||
|
};
|
|
@ -7,7 +7,7 @@ export function getTitleAnimation(delay = 0, duration = 0.3, once = true) {
|
||||||
filter: 'blur(0px)',
|
filter: 'blur(0px)',
|
||||||
transition: { duration, delay },
|
transition: { duration, delay },
|
||||||
},
|
},
|
||||||
viewport: { once: once },
|
viewport: { once },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,7 @@ const {
|
||||||
} = getUI(locale)
|
} = getUI(locale)
|
||||||
---
|
---
|
||||||
|
|
||||||
<button
|
<button type="button" onclick="window.history.back()" class="mb-8 flex w-min items-center gap-2">
|
||||||
onclick="window.history.back()"
|
|
||||||
class="mb-8 flex w-min items-center gap-2"
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon class="size-4" />
|
<ArrowLeftIcon class="size-4" />
|
||||||
{slug.back}
|
{slug.back}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -4,10 +4,7 @@ const sizes = [216, 396, 576, 756]
|
||||||
const borderWidths = [20, 30, 40, 50]
|
const borderWidths = [20, 30, 40, 50]
|
||||||
---
|
---
|
||||||
|
|
||||||
<div
|
<div id="circles" class:list={['pointer-events-none inset-0 overflow-hidden', classList]}>
|
||||||
id="circles"
|
|
||||||
class:list={['pointer-events-none inset-0 overflow-hidden', classList]}
|
|
||||||
>
|
|
||||||
<div class="mx-auto opacity-10 lg:opacity-100">
|
<div class="mx-auto opacity-10 lg:opacity-100">
|
||||||
{
|
{
|
||||||
[...Array(4)].map((_, i) => (
|
[...Array(4)].map((_, i) => (
|
||||||
|
|
|
@ -33,11 +33,7 @@ const {
|
||||||
{community.title[2]}
|
{community.title[2]}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</Description>
|
</Description>
|
||||||
<motion.p
|
<motion.p client:load {...getTitleAnimation(0.6)} className="lg:w-1/2 lg:px-0">
|
||||||
client:load
|
|
||||||
{...getTitleAnimation(0.6)}
|
|
||||||
className="lg:w-1/2 lg:px-0"
|
|
||||||
>
|
|
||||||
{community.description}
|
{community.description}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
<div class="flex w-full flex-wrap gap-3 sm:gap-10 md:justify-center">
|
<div class="flex w-full flex-wrap gap-3 sm:gap-10 md:justify-center">
|
||||||
|
@ -47,19 +43,11 @@ const {
|
||||||
<span>{community.lists.freeAndOpenSource.title}</span>
|
<span>{community.lists.freeAndOpenSource.title}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</motion.span>
|
</motion.span>
|
||||||
<motion.div
|
<motion.div client:load {...getTitleAnimation(1)} className="flex items-center gap-4">
|
||||||
client:load
|
|
||||||
{...getTitleAnimation(1)}
|
|
||||||
className="flex items-center gap-4"
|
|
||||||
>
|
|
||||||
<CheckIcon class="size-4" />
|
<CheckIcon class="size-4" />
|
||||||
<span>{community.lists.simpleYetPowerful.title}</span>
|
<span>{community.lists.simpleYetPowerful.title}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div client:load {...getTitleAnimation(1.2)} className="flex items-center gap-4">
|
||||||
client:load
|
|
||||||
{...getTitleAnimation(1.2)}
|
|
||||||
className="flex items-center gap-4"
|
|
||||||
>
|
|
||||||
<CheckIcon class="size-4" />
|
<CheckIcon class="size-4" />
|
||||||
<span>{community.lists.privateAndAlwaysUpToDate.title}</span>
|
<span>{community.lists.privateAndAlwaysUpToDate.title}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
@ -21,14 +21,11 @@ const {
|
||||||
|
|
||||||
const { title1 = features.title1, title2 = features.title2, title3 = features.title3 } = Astro.props
|
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
|
<section id="Features" class="relative flex w-full flex-col py-12 text-start lg:py-36">
|
||||||
id="Features"
|
<Description class="mb-2 text-4xl font-bold sm:text-6xl">
|
||||||
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">
|
|
||||||
<motion.span client:load {...getTitleAnimation(0.2)}>
|
<motion.span client:load {...getTitleAnimation(0.2)}>
|
||||||
{title1}
|
{title1}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
|
@ -49,7 +46,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
<motion.button
|
<motion.button
|
||||||
client:load
|
client:load
|
||||||
{...getTitleAnimation()}
|
{...getTitleAnimation()}
|
||||||
class="feature-tab whitespace-nowrap"
|
className="feature-tab whitespace-nowrap"
|
||||||
data-active="true"
|
data-active="true"
|
||||||
>
|
>
|
||||||
{features.featureTabs.workspaces.title}
|
{features.featureTabs.workspaces.title}
|
||||||
|
@ -57,21 +54,21 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
<motion.button
|
<motion.button
|
||||||
client:load
|
client:load
|
||||||
{...getTitleAnimation(0.2)}
|
{...getTitleAnimation(0.2)}
|
||||||
class="feature-tab whitespace-nowrap"
|
className="feature-tab whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{features.featureTabs.compactMode.title}
|
{features.featureTabs.compactMode.title}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
<motion.button
|
<motion.button
|
||||||
client:load
|
client:load
|
||||||
{...getTitleAnimation(0.4)}
|
{...getTitleAnimation(0.4)}
|
||||||
class="feature-tab whitespace-nowrap"
|
className="feature-tab whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{features.featureTabs.glance.title}
|
{features.featureTabs.glance.title}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
<motion.button
|
<motion.button
|
||||||
client:load
|
client:load
|
||||||
{...getTitleAnimation(0.6)}
|
{...getTitleAnimation(0.6)}
|
||||||
class="feature-tab whitespace-nowrap"
|
className="feature-tab whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{features.featureTabs.splitView.title}
|
{features.featureTabs.splitView.title}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
@ -79,12 +76,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
|
|
||||||
<!-- Desktop features list -->
|
<!-- Desktop features list -->
|
||||||
<div id="features-list" class="hidden lg:flex lg:flex-col lg:gap-3">
|
<div id="features-list" class="hidden lg:flex lg:flex-col lg:gap-3">
|
||||||
<motion.div
|
<motion.div client:load {...getTitleAnimation(0.8)} className="feature" data-active="true">
|
||||||
client:load
|
|
||||||
{...getTitleAnimation(0.8)}
|
|
||||||
className="feature"
|
|
||||||
data-active="true"
|
|
||||||
>
|
|
||||||
<Description class="text-2xl font-bold">
|
<Description class="text-2xl font-bold">
|
||||||
{features.featureTabs.workspaces.title}
|
{features.featureTabs.workspaces.title}
|
||||||
</Description>
|
</Description>
|
||||||
|
@ -119,14 +111,10 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile description -->
|
<!-- Mobile description -->
|
||||||
<div
|
<div class="feature-description mt-4 lg:hidden" data-descriptions={descriptions}></div>
|
||||||
class="feature-description mt-4 lg:hidden"
|
|
||||||
data-descriptions={descriptions}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</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="relative w-full">
|
||||||
<div class="video-stack relative h-full w-full">
|
<div class="video-stack relative h-full w-full">
|
||||||
<Video
|
<Video
|
||||||
|
@ -173,24 +161,16 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const features = document.querySelectorAll(
|
const features = document.querySelectorAll('.feature, .feature-tab') as NodeListOf<HTMLElement>
|
||||||
'.feature, .feature-tab',
|
|
||||||
) as NodeListOf<HTMLElement>
|
|
||||||
|
|
||||||
// Set initial description
|
// Set initial description
|
||||||
const descriptionEl = document.querySelector(
|
const descriptionEl = document.querySelector('.feature-description') as HTMLDivElement
|
||||||
'.feature-description',
|
|
||||||
) as HTMLDivElement
|
|
||||||
const descriptions = descriptionEl?.dataset.descriptions?.split(',')
|
const descriptions = descriptionEl?.dataset.descriptions?.split(',')
|
||||||
if (descriptionEl && descriptions) {
|
if (descriptionEl && descriptions) {
|
||||||
descriptionEl.textContent = descriptions[0]
|
descriptionEl.textContent = descriptions[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeToFeature({
|
function changeToFeature({ target }: { target: HTMLElement | undefined | null }) {
|
||||||
target,
|
|
||||||
}: {
|
|
||||||
target: HTMLElement | undefined | null
|
|
||||||
}) {
|
|
||||||
target = target?.closest('.feature, .feature-tab')
|
target = target?.closest('.feature, .feature-tab')
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
|
@ -212,9 +192,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
descriptionEl.textContent = descriptions[index]
|
descriptionEl.textContent = descriptions[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
const videos = document.querySelectorAll(
|
const videos = document.querySelectorAll('.feature-video') as NodeListOf<HTMLVideoElement>
|
||||||
'.feature-video',
|
|
||||||
) as NodeListOf<HTMLVideoElement>
|
|
||||||
videos.forEach((vid, i) => {
|
videos.forEach((vid, i) => {
|
||||||
const yOffset = (i - index) * 20
|
const yOffset = (i - index) * 20
|
||||||
const zOffset = i === index ? 0 : -100 - Math.abs(i - index) * 50
|
const zOffset = i === index ? 0 : -100 - Math.abs(i - index) * 50
|
||||||
|
@ -240,9 +218,11 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
feature.addEventListener('click', changeToFeature as any)
|
feature.addEventListener('click', changeToFeature as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
changeToFeature({ target: features[0] as any })
|
changeToFeature({ target: features[0] as any })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -19,13 +19,8 @@ const {
|
||||||
role="contentinfo"
|
role="contentinfo"
|
||||||
aria-label="Site footer"
|
aria-label="Site footer"
|
||||||
>
|
>
|
||||||
<div
|
<div class="container flex w-full flex-col items-start justify-between gap-12">
|
||||||
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">
|
||||||
>
|
|
||||||
<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"
|
<Description id="footer-title" class="text-6xl font-bold !text-paper"
|
||||||
>{footer.title}</Description
|
>{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"
|
class="grid w-full max-w-5xl place-items-center gap-12 text-center sm:text-left"
|
||||||
aria-label="Footer navigation and links"
|
aria-label="Footer navigation and links"
|
||||||
>
|
>
|
||||||
<div
|
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3 lg:w-full">
|
||||||
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 gap-8 sm:col-span-2 sm:grid-cols-2 md:col-span-1 md:grid-cols-1"
|
|
||||||
>
|
|
||||||
<section
|
<section
|
||||||
class="flex flex-col items-center gap-2 sm:items-start"
|
class="flex flex-col items-center gap-2 sm:items-start"
|
||||||
aria-labelledby="follow-us-heading"
|
aria-labelledby="follow-us-heading"
|
||||||
|
@ -63,10 +54,7 @@ const {
|
||||||
</h2>
|
</h2>
|
||||||
<SocialMediaStrip />
|
<SocialMediaStrip />
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section class="flex flex-col gap-2" aria-labelledby="about-us-heading">
|
||||||
class="flex flex-col gap-2"
|
|
||||||
aria-labelledby="about-us-heading"
|
|
||||||
>
|
|
||||||
<h2 id="about-us-heading" class="text-base !font-semibold">
|
<h2 id="about-us-heading" class="text-base !font-semibold">
|
||||||
{footer.aboutUs}
|
{footer.aboutUs}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -92,23 +80,17 @@ const {
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="grid gap-2 opacity-80">
|
<ul class="grid gap-2 opacity-80">
|
||||||
<li>
|
<li>
|
||||||
<a href="https://docs.zen-browser.app/" class="font-normal"
|
<a href="https://docs.zen-browser.app/" class="font-normal">{footer.documentation}</a>
|
||||||
>{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>
|
||||||
<li>
|
<li>
|
||||||
<a href={getLocalePath('/mods')} class="font-normal"
|
<a href={getLocalePath('/download?twilight')} class="font-normal">{footer.twilight}</a
|
||||||
>{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
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -119,19 +101,15 @@ const {
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="grid gap-2 opacity-80">
|
<ul class="grid gap-2 opacity-80">
|
||||||
<li>
|
<li>
|
||||||
<a href="https://discord.gg/zen-browser" class="font-normal"
|
<a href="https://discord.gg/zen-browser" class="font-normal">{footer.discord}</a>
|
||||||
>{footer.discord}</a
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://uptime.zen-browser.app/" class="font-normal">{footer.uptimeStatus}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://uptime.zen-browser.app/" class="font-normal"
|
<a href="https://github.com/zen-browser/desktop/issues/new/choose" class="font-normal"
|
||||||
>{footer.uptimeStatus}</a
|
>{footer.reportAnIssue}</a
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/zen-browser/desktop/issues/new/choose"
|
|
||||||
class="font-normal">{footer.reportAnIssue}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -148,11 +126,7 @@ const {
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section class="absolute bottom-0 right-0">
|
<section class="absolute bottom-0 right-0">
|
||||||
<Circles
|
<Circles white multiplier={0.7} class="mb-[-100px] ml-auto mr-[-80px] hidden lg:block" />
|
||||||
white
|
|
||||||
multiplier={0.7}
|
|
||||||
class="mb-[-100px] ml-auto mr-[-80px] hidden lg:block"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -36,9 +36,7 @@ const {
|
||||||
class="flex w-full flex-col items-center gap-[20%] py-32 text-center lg:gap-[25%]"
|
class="flex w-full flex-col items-center gap-[20%] py-32 text-center lg:gap-[25%]"
|
||||||
>
|
>
|
||||||
<div class="flex h-full flex-col items-center justify-center">
|
<div class="flex h-full flex-col items-center justify-center">
|
||||||
<Title
|
<Title class="relative px-12 text-center font-normal leading-8 md:text-7xl lg:px-0 lg:text-9xl">
|
||||||
class="relative px-12 text-center font-normal leading-8 md:text-7xl lg:px-0 lg:text-9xl"
|
|
||||||
>
|
|
||||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||||
{hero.title[0]}
|
{hero.title[0]}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
|
@ -49,11 +47,7 @@ const {
|
||||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||||
{hero.title[2]}
|
{hero.title[2]}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
<motion.span
|
<motion.span client:load {...getHeroTitleAnimation()} className="italic text-coral">
|
||||||
client:load
|
|
||||||
{...getHeroTitleAnimation()}
|
|
||||||
className="italic text-coral"
|
|
||||||
>
|
|
||||||
{hero.title[3]}
|
{hero.title[3]}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||||
|
@ -69,19 +63,19 @@ const {
|
||||||
</motion.span>
|
</motion.span>
|
||||||
<div class="mt-6 flex w-2/3 flex-col gap-3 sm:gap-6 md:w-fit md:flex-row">
|
<div class="mt-6 flex w-2/3 flex-col gap-3 sm:gap-6 md:w-fit md:flex-row">
|
||||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||||
<Button class="w-full" href={getLocalePath("/download")} isPrimary>
|
<Button class="w-full" href={getLocalePath('/download')} isPrimary>
|
||||||
{hero.buttons.beta}
|
{hero.buttons.beta}
|
||||||
<ArrowRightIcon class="size-4" />
|
<ArrowRightIcon class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</motion.span>
|
</motion.span>
|
||||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||||
<Button href={getLocalePath("/donate")}>{hero.buttons.support}</Button>
|
<Button href={getLocalePath('/donate')}>{hero.buttons.support}</Button>
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</div>
|
</div>
|
||||||
<motion.span
|
<motion.span
|
||||||
client:load
|
client:load
|
||||||
{...getHeroTitleAnimation()}
|
{...getHeroTitleAnimation()}
|
||||||
class="mx-auto translate-y-16 !transform"
|
className="mx-auto translate-y-16 !transform"
|
||||||
>
|
>
|
||||||
<SocialMediaStrip />
|
<SocialMediaStrip />
|
||||||
</motion.span>
|
</motion.span>
|
||||||
|
|
|
@ -11,12 +11,7 @@ const {
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Hidden checkbox for menu toggle -->
|
<!-- Hidden checkbox for menu toggle -->
|
||||||
<input
|
<input type="checkbox" id="mobile-menu-toggle" class="peer sr-only lg:hidden" aria-hidden="true" />
|
||||||
type="checkbox"
|
|
||||||
id="mobile-menu-toggle"
|
|
||||||
class="peer sr-only lg:hidden"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Mobile Slide Menu -->
|
<!-- Mobile Slide Menu -->
|
||||||
<div
|
<div
|
||||||
|
@ -25,17 +20,16 @@ const {
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between border-b border-dark px-4 py-2">
|
<div class="flex items-center justify-between border-b border-dark px-4 py-2">
|
||||||
<div class="text-lg font-bold">{menu.menu}</div>
|
<div class="text-lg font-bold">{menu.menu}</div>
|
||||||
<label
|
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||||
for="mobile-menu-toggle"
|
<label for="mobile-menu-toggle" class="cursor-pointer p-2 text-dark">
|
||||||
class="cursor-pointer p-2 text-dark"
|
<span class="sr-only">Close menu</span>
|
||||||
aria-label="Close menu"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
|
@ -52,21 +46,18 @@ const {
|
||||||
<div class="mb-2 font-bold">{menu.gettingStarted}</div>
|
<div class="mb-2 font-bold">{menu.gettingStarted}</div>
|
||||||
<ul class="ml-4 space-y-2">
|
<ul class="ml-4 space-y-2">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath('/mods')} class="block text-dark hover:text-coral"
|
||||||
href={getLocalePath('/mods')}
|
>{menu.zenMods}</a
|
||||||
class="block text-dark hover:text-coral">{menu.zenMods}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath('/release-notes')} class="block text-dark hover:text-coral"
|
||||||
href={getLocalePath('/release-notes')}
|
>{menu.releaseNotes}</a
|
||||||
class="block text-dark hover:text-coral">{menu.releaseNotes}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="https://discord.gg/zen-browser" class="block text-dark hover:text-coral"
|
||||||
href="https://discord.gg/zen-browser"
|
>{menu.discord}</a
|
||||||
class="block text-dark hover:text-coral">{menu.discord}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -76,21 +67,18 @@ const {
|
||||||
<div class="mb-2 font-bold">{menu.usefulLinks}</div>
|
<div class="mb-2 font-bold">{menu.usefulLinks}</div>
|
||||||
<ul class="ml-4 space-y-2">
|
<ul class="ml-4 space-y-2">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath('/donate')} class="block text-dark hover:text-coral"
|
||||||
href={getLocalePath('/donate')}
|
>{menu.donate}</a
|
||||||
class="block text-dark hover:text-coral">{menu.donate}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath('/about')} class="block text-dark hover:text-coral"
|
||||||
href={getLocalePath('/about')}
|
>{menu.aboutUs}</a
|
||||||
class="block text-dark hover:text-coral">{menu.aboutUs}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="https://docs.zen-browser.app" class="block text-dark hover:text-coral"
|
||||||
href="https://docs.zen-browser.app"
|
>{menu.documentation}</a
|
||||||
class="block text-dark hover:text-coral">{menu.documentation}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -104,15 +92,13 @@ const {
|
||||||
</li>
|
</li>
|
||||||
<!-- Extra Links -->
|
<!-- Extra Links -->
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath('/mods')} class="block font-bold text-dark hover:text-coral"
|
||||||
href={getLocalePath('/mods')}
|
>{menu.mods}</a
|
||||||
class="block font-bold text-dark hover:text-coral">{menu.mods}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath('/download')} class="block font-bold text-dark hover:text-coral"
|
||||||
href={getLocalePath('/download')}
|
>{menu.download}</a
|
||||||
class="block font-bold text-dark hover:text-coral">{menu.download}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -120,6 +106,7 @@ const {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Overlay for Mobile Menu -->
|
<!-- Overlay for Mobile Menu -->
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||||
<label
|
<label
|
||||||
for="mobile-menu-toggle"
|
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"
|
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"
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { icon, library } from '@fortawesome/fontawesome-svg-core'
|
import { icon, library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'
|
import { faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState, type FormEvent } from 'react'
|
||||||
|
|
||||||
import { useModsSearch } from '~/hooks/useModsSearch'
|
import { useModsSearch } from '~/hooks/useModsSearch'
|
||||||
import type { ZenTheme } from '~/mods'
|
import { type ZenTheme } from '~/mods'
|
||||||
import { type Locale, getUI } from '~/utils/i18n'
|
import { getUI, type Locale } from '~/utils/i18n'
|
||||||
|
|
||||||
// Add icons to the library
|
// Add icons to the library
|
||||||
library.add(faSort, faSortUp, faSortDown)
|
library.add(faSort, faSortUp, faSortDown)
|
||||||
|
@ -13,12 +14,12 @@ const defaultSortIcon = icon({ prefix: 'fas', iconName: 'sort' })
|
||||||
const ascSortIcon = icon({ prefix: 'fas', iconName: 'sort-up' })
|
const ascSortIcon = icon({ prefix: 'fas', iconName: 'sort-up' })
|
||||||
const descSortIcon = icon({ prefix: 'fas', iconName: 'sort-down' })
|
const descSortIcon = icon({ prefix: 'fas', iconName: 'sort-down' })
|
||||||
|
|
||||||
interface ModsListProps {
|
type ModsListProps = {
|
||||||
allMods: ZenTheme[]
|
allMods: ZenTheme[]
|
||||||
locale: Locale
|
locale: Locale
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ModsList({ allMods, locale }: ModsListProps) {
|
const ModsList = ({ allMods, locale }: ModsListProps) => {
|
||||||
const {
|
const {
|
||||||
search,
|
search,
|
||||||
createdSort,
|
createdSort,
|
||||||
|
@ -49,17 +50,17 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
return defaultSortIcon
|
return defaultSortIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch(e: Event) {
|
function handleSearch(e: FormEvent<HTMLInputElement>) {
|
||||||
const target = e.target as HTMLInputElement
|
const target = e.target as HTMLInputElement
|
||||||
setSearch(target.value)
|
setSearch(target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLimitChange(e: Event) {
|
function handleLimitChange(e: FormEvent<HTMLSelectElement>) {
|
||||||
const target = e.target as HTMLSelectElement
|
const target = e.target as HTMLSelectElement
|
||||||
setLimit(Number.parseInt(target.value, 10))
|
setLimit(Number.parseInt(target.value, 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageSubmit(e: Event) {
|
function handlePageSubmit(e: FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const newPage = Number.parseInt(pageInput, 10)
|
const newPage = Number.parseInt(pageInput, 10)
|
||||||
if (!Number.isNaN(newPage) && newPage >= 1 && newPage <= totalPages) {
|
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
|
const target = e.target as HTMLInputElement
|
||||||
setPageInput(target.value)
|
setPageInput(target.value)
|
||||||
}
|
}
|
||||||
|
@ -100,6 +101,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
|
key={index}
|
||||||
aria-label="Page number"
|
aria-label="Page number"
|
||||||
className="w-16 rounded border border-dark bg-transparent px-2 py-1 text-center text-sm"
|
className="w-16 rounded border border-dark bg-transparent px-2 py-1 text-center text-sm"
|
||||||
onInput={handlePageInputChange}
|
onInput={handlePageInputChange}
|
||||||
|
@ -110,7 +112,9 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="text-sm" key={value}>
|
<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>
|
</span>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -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="grid w-full grid-cols-2 place-items-center gap-4 sm:grid-cols-3">
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div className="flex flex-col items-start gap-2">
|
||||||
<button
|
<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}
|
onClick={toggleCreatedSort}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -159,7 +163,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<button
|
<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}
|
onClick={toggleUpdatedSort}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -174,7 +178,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 px-4 py-2">
|
<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}
|
{mods.sort.perPage}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<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">
|
<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.length > 0 ? (
|
||||||
paginatedMods.map((mod) => (
|
paginatedMods.map(mod => (
|
||||||
<a
|
<a
|
||||||
className="mod-card flex w-full flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
|
className="mod-card flex w-full flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
|
||||||
href={`/mods/${mod.id}`}
|
href={`/mods/${mod.id}`}
|
||||||
|
@ -209,17 +213,17 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-bold text-lg">
|
<h2 className="text-lg font-bold">
|
||||||
{mod.name} <span className="ml-1 font-normal text-sm">by @{mod.author}</span>
|
{mod.name} <span className="ml-1 text-sm font-normal">by @{mod.author}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="font-thin text-sm">{mod.description}</p>
|
<p className="text-sm font-thin">{mod.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="col-span-4 grid place-items-center gap-4 place-self-center px-8 text-center">
|
<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>
|
<h2 className="text-lg font-bold">{mods.noResults}</h2>
|
||||||
<p className="font-thin text-sm">{mods.noResultsDescription}</p>
|
<p className="text-sm font-thin">{mods.noResultsDescription}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -228,3 +232,5 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default ModsList
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
---
|
---
|
||||||
import { Astronav, Dropdown, DropdownItems, MenuItems } from 'astro-navbar'
|
import { Astronav, Dropdown, DropdownItems, MenuItems } from 'astro-navbar'
|
||||||
import { motion } from 'motion/react'
|
|
||||||
import Button from '~/components/Button.astro'
|
import Button from '~/components/Button.astro'
|
||||||
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
|
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
|
||||||
import ChevronDownIcon from '~/icons/ChevronDownIcon.astro'
|
import ChevronDownIcon from '~/icons/ChevronDownIcon.astro'
|
||||||
import DownloadIcon from '~/icons/DownloadIcon.astro'
|
import DownloadIcon from '~/icons/DownloadIcon.astro'
|
||||||
import MenuIcon from '~/icons/MenuIcon.astro'
|
import MenuIcon from '~/icons/MenuIcon.astro'
|
||||||
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
||||||
import { getTitleAnimation } from '../animations.ts'
|
|
||||||
import Logo from './Logo.astro'
|
import Logo from './Logo.astro'
|
||||||
import MobileMenu from './MobileMenu.astro'
|
import MobileMenu from './MobileMenu.astro'
|
||||||
import ThemeSwitch from './ThemeSwitch.astro'
|
import ThemeSwitch from './ThemeSwitch.astro'
|
||||||
|
@ -26,10 +24,7 @@ const {
|
||||||
<MenuItems
|
<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"
|
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
|
<a class="flex items-center gap-2 text-lg font-bold" href={getLocalePath('/')}>
|
||||||
class="flex items-center gap-2 text-lg font-bold"
|
|
||||||
href={getLocalePath('/')}
|
|
||||||
>
|
|
||||||
<Logo class="text-coral" />
|
<Logo class="text-coral" />
|
||||||
<span>{brand}</span>
|
<span>{brand}</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -44,15 +39,8 @@ const {
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<DropdownItems>
|
<DropdownItems>
|
||||||
<motion.div
|
<div class="navbar-dropdown">
|
||||||
className="navbar-dropdown"
|
<a class="dropdown-item bg-dark/5 row-span-2" href={getLocalePath('/mods')}>
|
||||||
{...getTitleAnimation(0, 0.3, false)}
|
|
||||||
client:load
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="dropdown-item bg-dark/5 row-span-2"
|
|
||||||
href={getLocalePath('/mods')}
|
|
||||||
>
|
|
||||||
<div class="dropdown-title">{menu.zenMods}</div>
|
<div class="dropdown-title">{menu.zenMods}</div>
|
||||||
<div class="dropdown-description">
|
<div class="dropdown-description">
|
||||||
{menu.zenModsDesc}
|
{menu.zenModsDesc}
|
||||||
|
@ -74,7 +62,7 @@ const {
|
||||||
{menu.discordDesc}
|
{menu.discordDesc}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</motion.div>
|
</div>
|
||||||
</DropdownItems>
|
</DropdownItems>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown class="group">
|
<Dropdown class="group">
|
||||||
|
@ -85,11 +73,7 @@ const {
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<DropdownItems>
|
<DropdownItems>
|
||||||
<motion.div
|
<div class="navbar-dropdown !grid-cols-1 gap-1">
|
||||||
className="navbar-dropdown !grid-cols-1 gap-1"
|
|
||||||
{...getTitleAnimation(0, 0.3, false)}
|
|
||||||
client:load
|
|
||||||
>
|
|
||||||
<a class="dropdown-item" href={getLocalePath('/donate')}>
|
<a class="dropdown-item" href={getLocalePath('/donate')}>
|
||||||
<div class="dropdown-title">{menu.donate}</div>
|
<div class="dropdown-title">{menu.donate}</div>
|
||||||
<div class="dropdown-description">
|
<div class="dropdown-description">
|
||||||
|
@ -108,17 +92,13 @@ const {
|
||||||
{menu.documentationDesc}
|
{menu.documentationDesc}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a class="dropdown-item" href="https://github.com/zen-browser" target="_blank">
|
||||||
class="dropdown-item"
|
|
||||||
href="https://github.com/zen-browser"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div class="dropdown-title">{menu.github}</div>
|
<div class="dropdown-title">{menu.github}</div>
|
||||||
<div class="dropdown-description">
|
<div class="dropdown-description">
|
||||||
{menu.githubDesc}
|
{menu.githubDesc}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</motion.div>
|
</div>
|
||||||
</DropdownItems>
|
</DropdownItems>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<a class="hidden items-center lg:block" href={getLocalePath('/mods')}>
|
<a class="hidden items-center lg:block" href={getLocalePath('/mods')}>
|
||||||
|
@ -138,6 +118,7 @@ const {
|
||||||
<DownloadIcon class="size-4" />
|
<DownloadIcon class="size-4" />
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||||
<label
|
<label
|
||||||
for="mobile-menu-toggle"
|
for="mobile-menu-toggle"
|
||||||
class="cursor-pointer p-2 text-dark lg:hidden"
|
class="cursor-pointer p-2 text-dark lg:hidden"
|
||||||
|
|
|
@ -25,7 +25,9 @@ if (props.date) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ffVersion = getReleaseNoteFirefoxVersion(props)
|
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]
|
const prevReleaseNote = releaseNotesData[currentReleaseIndex + 1]
|
||||||
let compareLink = ''
|
let compareLink = ''
|
||||||
if (prevReleaseNote && !isTwilight) {
|
if (prevReleaseNote && !isTwilight) {
|
||||||
|
@ -34,13 +36,15 @@ if (prevReleaseNote && !isTwilight) {
|
||||||
|
|
||||||
const isLatest = currentReleaseIndex === 0
|
const isLatest = currentReleaseIndex === 0
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const listItems = {} as any
|
const listItems = {} as any
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const generateItems = (items: any, type: string) => {
|
const generateItems = (items: any, type: string) => {
|
||||||
if (!items) return
|
if (!items) return
|
||||||
if (!listItems[type]) {
|
if (!listItems[type]) {
|
||||||
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) => {
|
items.forEach((item: any) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'feature':
|
case 'feature':
|
||||||
|
@ -99,7 +103,7 @@ generateItems(props.knownIssues, 'known')
|
||||||
class="release-note-item relative mt-12 flex flex-col pt-24 lg:flex-row"
|
class="release-note-item relative mt-12 flex flex-col pt-24 lg:flex-row"
|
||||||
id={props.version}
|
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 ? (
|
isTwilight ? (
|
||||||
<a
|
<a
|
||||||
|
@ -110,37 +114,40 @@ generateItems(props.knownIssues, 'known')
|
||||||
</a>
|
</a>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<div class="w-full sm:flex justify-between">
|
<div class="w-full justify-between sm:flex">
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-0 text-sm font-bold opacity-80">
|
<div
|
||||||
|
class="flex flex-col gap-1 text-sm font-bold opacity-80 sm:flex-row sm:items-center sm:gap-0"
|
||||||
|
>
|
||||||
{
|
{
|
||||||
isTwilight ? (
|
isTwilight ? (
|
||||||
<>
|
<>
|
||||||
{releaseNoteItem.twilightChanges} {props.version.replaceAll(
|
{releaseNoteItem.twilightChanges}{' '}
|
||||||
'{version}',
|
{props.version.replaceAll('{version}', props.version)}
|
||||||
props.version,
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>{releaseNoteItem.releaseChanges.replaceAll('{version}', props.version)}</>
|
||||||
{releaseNoteItem.releaseChanges.replaceAll(
|
|
||||||
'{version}',
|
|
||||||
props.version,
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ffVersion ? (
|
ffVersion ? (
|
||||||
|
<>
|
||||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
<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">
|
<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)}
|
{releaseNoteItem.firefoxVersion.replace('{version}', ffVersion)}
|
||||||
</a>
|
</a>
|
||||||
|
</>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||||
<a
|
<a
|
||||||
rel="noopener noreferrer"
|
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"
|
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
|
>{releaseNoteItem.githubRelease}</a
|
||||||
|
@ -151,7 +158,7 @@ generateItems(props.knownIssues, 'known')
|
||||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||||
<a
|
<a
|
||||||
rel="noopener noreferrer"
|
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"
|
target="_blank"
|
||||||
href={`https://github.com/zen-browser/desktop/actions/runs/${props.workflowId}`}
|
href={`https://github.com/zen-browser/desktop/actions/runs/${props.workflowId}`}
|
||||||
>
|
>
|
||||||
|
@ -166,7 +173,7 @@ generateItems(props.knownIssues, 'known')
|
||||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||||
<a
|
<a
|
||||||
rel="noopener noreferrer"
|
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"
|
target="_blank"
|
||||||
href={compareLink}
|
href={compareLink}
|
||||||
>
|
>
|
||||||
|
@ -176,7 +183,7 @@ generateItems(props.knownIssues, 'known')
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs opacity-80 font-bold">
|
<div class="text-xs font-bold opacity-80">
|
||||||
{date && date.toLocaleDateString('en-US', { dateStyle: 'long' })}
|
{date && date.toLocaleDateString('en-US', { dateStyle: 'long' })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -187,8 +194,8 @@ generateItems(props.knownIssues, 'known')
|
||||||
</p>
|
</p>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
{isTwilight || isLatest ? (
|
{
|
||||||
|
isTwilight || isLatest ? (
|
||||||
<div class="text-muted-forground flex text-sm opacity-70">
|
<div class="text-muted-forground flex text-sm opacity-70">
|
||||||
{isTwilight ? <InfoIcon class="mx-4 my-0 size-6 text-yellow-500" /> : null}
|
{isTwilight ? <InfoIcon class="mx-4 my-0 size-6 text-yellow-500" /> : null}
|
||||||
<p class="m-0">
|
<p class="m-0">
|
||||||
|
@ -196,27 +203,25 @@ generateItems(props.knownIssues, 'known')
|
||||||
<span set:html={releaseNoteItem.reportIssues} />
|
<span set:html={releaseNoteItem.reportIssues} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null
|
||||||
<div class="gap-8 flex flex-col mt-4">
|
}
|
||||||
|
<div class="mt-4 flex flex-col gap-8">
|
||||||
{
|
{
|
||||||
Object.keys(listItems).map((type) => {
|
Object.keys(listItems).map(type => {
|
||||||
const items = listItems[type];
|
const items = listItems[type]
|
||||||
if (items.length === 0) return null;
|
if (items.length === 0) return null
|
||||||
return (
|
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) => (
|
{items.map((item: any) => (
|
||||||
<ReleaseNoteListItem
|
<ReleaseNoteListItem type={item.type} content={item.content} link={item.link} />
|
||||||
type={item.type}
|
|
||||||
content={item.content}
|
|
||||||
link={item.link}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
)
|
||||||
</div>
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<style is:global>
|
<style is:global>
|
||||||
.ac-accordion-item-title {
|
.ac-accordion-item-title {
|
||||||
@apply !text-dark;
|
@apply !text-dark;
|
||||||
|
@ -241,10 +246,7 @@ generateItems(props.knownIssues, 'known')
|
||||||
.ac-accordion {
|
.ac-accordion {
|
||||||
&.ac-accordion--light {
|
&.ac-accordion--light {
|
||||||
> * + * {
|
> * + * {
|
||||||
border-color: light-dark(
|
border-color: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1)) !important;
|
||||||
rgba(0, 0, 0, 0.1),
|
|
||||||
rgba(255, 255, 255, 0.1)
|
|
||||||
) !important;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,25 +26,21 @@ const {
|
||||||
(type === 'feature' && 'text-[#bf3316] dark:text-[#ffb1a1]') ||
|
(type === 'feature' && 'text-[#bf3316] dark:text-[#ffb1a1]') ||
|
||||||
(type === 'fix' && 'text-[#fe846b]') ||
|
(type === 'fix' && 'text-[#fe846b]') ||
|
||||||
(type === 'theme' && 'text-[#f76f53]') ||
|
(type === 'theme' && 'text-[#f76f53]') ||
|
||||||
(type === 'break' && 'text-[#471308] dark:text-[#D02908]') || ''
|
(type === 'break' && 'text-[#471308] dark:text-[#D02908]') ||
|
||||||
, 'opacity-80 font-bold min-w-16']}
|
'',
|
||||||
>
|
'min-w-16 font-bold opacity-80',
|
||||||
|
]}
|
||||||
|
>
|
||||||
{itemType[type]}
|
{itemType[type]}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{content && (
|
{content && <span class="text-base opacity-80" set:html={content} />}
|
||||||
<span
|
{
|
||||||
class="text-base opacity-80"
|
link && (
|
||||||
set:html={content}
|
<a href={link.href} class="text-blue inline-block text-base underline">
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{link && (
|
|
||||||
<a
|
|
||||||
href={link.href}
|
|
||||||
class="text-base text-blue inline-block underline"
|
|
||||||
>
|
|
||||||
{link.text}
|
{link.text}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -2,7 +2,13 @@
|
||||||
const { gap = 4 } = Astro.props
|
const { gap = 4 } = Astro.props
|
||||||
|
|
||||||
import { icon, library } from '@fortawesome/fontawesome-svg-core'
|
import { icon, library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faBluesky, faGithub, faMastodon, faReddit, faXTwitter } from '@fortawesome/free-brands-svg-icons'
|
import {
|
||||||
|
faBluesky,
|
||||||
|
faGithub,
|
||||||
|
faMastodon,
|
||||||
|
faReddit,
|
||||||
|
faXTwitter,
|
||||||
|
} from '@fortawesome/free-brands-svg-icons'
|
||||||
|
|
||||||
library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit)
|
library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit)
|
||||||
const Mastodon = icon({ prefix: 'fab', iconName: 'mastodon' })
|
const Mastodon = icon({ prefix: 'fab', iconName: 'mastodon' })
|
||||||
|
|
|
@ -9,10 +9,7 @@ const { label, className = '' } = Astro.props
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class:list={[
|
class:list={['inline-flex h-8 w-8 cursor-pointer items-center justify-center', className]}
|
||||||
'inline-flex h-8 w-8 cursor-pointer items-center justify-center',
|
|
||||||
className,
|
|
||||||
]}
|
|
||||||
id="theme-switcher"
|
id="theme-switcher"
|
||||||
aria-label={label || 'Toggle theme'}
|
aria-label={label || 'Toggle theme'}
|
||||||
>
|
>
|
||||||
|
@ -29,9 +26,7 @@ const { label, className = '' } = Astro.props
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const themeSwitch = document.getElementById(
|
const themeSwitch = document.getElementById('theme-switcher') as HTMLButtonElement
|
||||||
'theme-switcher',
|
|
||||||
) as HTMLButtonElement
|
|
||||||
|
|
||||||
const resolveTheme = () => {
|
const resolveTheme = () => {
|
||||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||||
|
|
|
@ -4,10 +4,7 @@ const { class: className } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
class={cn(
|
class={cn('mb-[0.4rem] font-junicode text-5xl font-semibold leading-[0.9] text-dark', className)}
|
||||||
"text-dark leading-[0.9] mb-[0.4rem] font-junicode font-semibold text-5xl",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -3,19 +3,13 @@ const { src, class: className, ...rest } = Astro.props
|
||||||
const type = src.split('.').pop() || 'webm'
|
const type = src.split('.').pop() || 'webm'
|
||||||
---
|
---
|
||||||
|
|
||||||
<video
|
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||||
class:list={['w-fit', className]}
|
<video class:list={['w-fit', className]} data-src={src} preload="none" {...rest}>
|
||||||
data-src={src}
|
|
||||||
preload="none"
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<source src="" type={`video/${type}`} />
|
<source src="" type={`video/${type}`} />
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const videos = document.querySelectorAll(
|
const videos = document.querySelectorAll('video[data-src]') as NodeListOf<HTMLVideoElement>
|
||||||
'video[data-src]',
|
|
||||||
) as NodeListOf<HTMLVideoElement>
|
|
||||||
|
|
||||||
const loadVideo = (video: HTMLVideoElement) => {
|
const loadVideo = (video: HTMLVideoElement) => {
|
||||||
const source = video.querySelector('source')
|
const source = video.querySelector('source')
|
||||||
|
|
|
@ -45,7 +45,7 @@ const { label, href, checksum } = Astro.props
|
||||||
Show SHA-256
|
Show SHA-256
|
||||||
</span>
|
</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="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
|
<button
|
||||||
type="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"
|
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>
|
<script>
|
||||||
const checksumButtons = document.querySelectorAll(
|
const checksumButtons = document.querySelectorAll(
|
||||||
'.checksum-icon-btn',
|
'.checksum-icon-btn'
|
||||||
) as NodeListOf<HTMLButtonElement>
|
) as NodeListOf<HTMLButtonElement>
|
||||||
const checksumTooltips = document.querySelectorAll(
|
const checksumTooltips = document.querySelectorAll(
|
||||||
'.checksum-tooltip',
|
'.checksum-tooltip'
|
||||||
) as NodeListOf<HTMLDivElement>
|
) as NodeListOf<HTMLDivElement>
|
||||||
const copyButtons = document.querySelectorAll(
|
const copyButtons = document.querySelectorAll('.copy-btn') as NodeListOf<HTMLButtonElement>
|
||||||
'.copy-btn',
|
|
||||||
) as NodeListOf<HTMLButtonElement>
|
|
||||||
|
|
||||||
function stopEvent(e: Event) {
|
function stopEvent(e: Event) {
|
||||||
e.preventDefault?.()
|
e.preventDefault?.()
|
||||||
|
@ -113,23 +111,20 @@ const { label, href, checksum } = Astro.props
|
||||||
setTimeout(() => (btn.innerText = original), 1200)
|
setTimeout(() => (btn.innerText = original), 1200)
|
||||||
}
|
}
|
||||||
|
|
||||||
checksumButtons.forEach((btn) => {
|
checksumButtons.forEach(btn => {
|
||||||
btn.addEventListener('click', stopEvent)
|
btn.addEventListener('click', stopEvent)
|
||||||
})
|
})
|
||||||
checksumTooltips.forEach((tooltip) => {
|
checksumTooltips.forEach(tooltip => {
|
||||||
tooltip.addEventListener('mousedown', stopEvent)
|
tooltip.addEventListener('mousedown', stopEvent)
|
||||||
tooltip.addEventListener('click', stopEvent)
|
tooltip.addEventListener('click', stopEvent)
|
||||||
})
|
})
|
||||||
copyButtons.forEach((btn) => {
|
copyButtons.forEach(btn => {
|
||||||
btn.addEventListener('click', (e) =>
|
btn.addEventListener('click', e =>
|
||||||
copyChecksum(
|
copyChecksum(
|
||||||
e,
|
e,
|
||||||
(
|
(btn.closest('.checksum-tooltip')?.querySelector('.font-mono') as HTMLSpanElement)
|
||||||
btn
|
?.innerText
|
||||||
.closest('.checksum-tooltip')
|
)
|
||||||
?.querySelector('.font-mono') as HTMLSpanElement
|
|
||||||
)?.innerText,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
btn.addEventListener('mousedown', stopEvent)
|
btn.addEventListener('mousedown', stopEvent)
|
||||||
})
|
})
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
|
|
||||||
// Apply twilight mode to all relevant elements
|
// Apply twilight mode to all relevant elements
|
||||||
const coralElements = document.querySelectorAll(
|
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) {
|
for (const element of coralElements) {
|
||||||
element.setAttribute('data-twilight', 'true')
|
element.setAttribute('data-twilight', 'true')
|
||||||
|
@ -88,10 +88,7 @@
|
||||||
if (!link.id.includes('beta')) {
|
if (!link.id.includes('beta')) {
|
||||||
const href = link.getAttribute('href')
|
const href = link.getAttribute('href')
|
||||||
if (href && href.includes('/latest/download/')) {
|
if (href && href.includes('/latest/download/')) {
|
||||||
const twilightHref = href.replace(
|
const twilightHref = href.replace('/latest/download/', '/download/twilight/')
|
||||||
'/latest/download/',
|
|
||||||
'/download/twilight/',
|
|
||||||
)
|
|
||||||
link.setAttribute('href', twilightHref)
|
link.setAttribute('href', twilightHref)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id={`${platform}-downloads`}
|
id={`${platform}-downloads`}
|
||||||
data-active={platform === "mac"}
|
data-active={platform === 'mac'}
|
||||||
class="platform-section data-[active='false']:hidden"
|
class="platform-section data-[active='false']:hidden"
|
||||||
>
|
>
|
||||||
<div class="items-center gap-8 md:flex">
|
<div class="items-center gap-8 md:flex">
|
||||||
|
@ -56,7 +56,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
||||||
<p class="text-muted-foreground mb-6" set:html={description} />
|
<p class="text-muted-foreground mb-6" set:html={description} />
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
{
|
{
|
||||||
platform === "linux" ? (
|
platform === 'linux' ? (
|
||||||
<>
|
<>
|
||||||
{releases.flathub && releases.flathub.all.label && (
|
{releases.flathub && releases.flathub.all.label && (
|
||||||
<article class="flathub-download data-[twilight='true']:hidden">
|
<article class="flathub-download data-[twilight='true']:hidden">
|
||||||
|
@ -71,24 +71,16 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
||||||
</article>
|
</article>
|
||||||
)}
|
)}
|
||||||
{releases.x86_64 &&
|
{releases.x86_64 &&
|
||||||
typeof releases.x86_64 === "object" &&
|
typeof releases.x86_64 === 'object' &&
|
||||||
"tarball" in releases.x86_64 &&
|
'tarball' in releases.x86_64 &&
|
||||||
(releases.x86_64.tarball) && (
|
releases.x86_64.tarball && (
|
||||||
<article>
|
<article>
|
||||||
<h4 class="mb-3 text-lg font-medium">x86_64</h4>
|
<h4 class="mb-3 text-lg font-medium">x86_64</h4>
|
||||||
<div class="">
|
<div class="">
|
||||||
{releases.x86_64.tarball && (
|
{releases.x86_64.tarball && (
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
label={
|
label={releases.x86_64.tarball.label ? releases.x86_64.tarball.label : ''}
|
||||||
releases.x86_64.tarball.label
|
href={releases.x86_64.tarball.link ? releases.x86_64.tarball.link : ''}
|
||||||
? releases.x86_64.tarball.label
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
href={
|
|
||||||
releases.x86_64.tarball.link
|
|
||||||
? releases.x86_64.tarball.link
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
variant="x86_64"
|
variant="x86_64"
|
||||||
checksum={releases.x86_64.tarball.checksum}
|
checksum={releases.x86_64.tarball.checksum}
|
||||||
/>
|
/>
|
||||||
|
@ -97,24 +89,18 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
||||||
</article>
|
</article>
|
||||||
)}
|
)}
|
||||||
{releases.aarch64 &&
|
{releases.aarch64 &&
|
||||||
typeof releases.aarch64 === "object" &&
|
typeof releases.aarch64 === 'object' &&
|
||||||
"tarball" in releases.aarch64 &&
|
'tarball' in releases.aarch64 &&
|
||||||
(releases.aarch64.tarball) && (
|
releases.aarch64.tarball && (
|
||||||
<article>
|
<article>
|
||||||
<h4 class="mb-3 text-lg font-medium">ARM64</h4>
|
<h4 class="mb-3 text-lg font-medium">ARM64</h4>
|
||||||
<div class="gap-3">
|
<div class="gap-3">
|
||||||
{releases.aarch64.tarball && (
|
{releases.aarch64.tarball && (
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
label={
|
label={
|
||||||
releases.aarch64.tarball.label
|
releases.aarch64.tarball.label ? releases.aarch64.tarball.label : ''
|
||||||
? releases.aarch64.tarball.label
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
href={
|
|
||||||
releases.aarch64.tarball.link
|
|
||||||
? releases.aarch64.tarball.link
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
|
href={releases.aarch64.tarball.link ? releases.aarch64.tarball.link : ''}
|
||||||
variant="aarch64"
|
variant="aarch64"
|
||||||
checksum={releases.aarch64.tarball.checksum}
|
checksum={releases.aarch64.tarball.checksum}
|
||||||
/>
|
/>
|
||||||
|
@ -133,9 +119,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
||||||
checksum={releases.universal.checksum}
|
checksum={releases.universal.checksum}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{releases.x86_64 &&
|
{releases.x86_64 && isFlatReleaseInfo(releases.x86_64) && releases.x86_64.label && (
|
||||||
isFlatReleaseInfo(releases.x86_64) &&
|
|
||||||
releases.x86_64.label && (
|
|
||||||
<DownloadCard
|
<DownloadCard
|
||||||
label={releases.x86_64.label}
|
label={releases.x86_64.label}
|
||||||
href={releases.x86_64.link}
|
href={releases.x86_64.link}
|
||||||
|
@ -158,11 +142,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
||||||
<div
|
<div
|
||||||
class="download-browser-logo flex justify-center text-coral transition-colors data-[twilight='true']:text-zen-blue md:w-1/3"
|
class="download-browser-logo flex justify-center text-coral transition-colors data-[twilight='true']:text-zen-blue md:w-1/3"
|
||||||
>
|
>
|
||||||
<Image
|
<Image src={AppIconDark} alt="Zen Browser" class="w-32 translate-y-6 transform dark:hidden" />
|
||||||
src={AppIconDark}
|
|
||||||
alt="Zen Browser"
|
|
||||||
class="w-32 translate-y-6 transform dark:hidden"
|
|
||||||
/>
|
|
||||||
<Image
|
<Image
|
||||||
src={AppIconLight}
|
src={AppIconLight}
|
||||||
alt="Zen Browser"
|
alt="Zen Browser"
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'react'
|
||||||
import type { ZenTheme } from '../mods'
|
|
||||||
|
import { type ZenTheme } from '../mods'
|
||||||
|
|
||||||
type SortOrder = 'default' | 'asc' | 'desc'
|
type SortOrder = 'default' | 'asc' | 'desc'
|
||||||
|
|
||||||
interface ModsSearchState {
|
type ModsSearchState = {
|
||||||
search: string
|
search: string
|
||||||
createdSort: SortOrder
|
createdSort: SortOrder
|
||||||
updatedSort: SortOrder
|
updatedSort: SortOrder
|
||||||
|
@ -86,11 +87,11 @@ export function useModsSearch(mods: ZenTheme[]) {
|
||||||
const searchTerm = state.search.toLowerCase()
|
const searchTerm = state.search.toLowerCase()
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
filtered = filtered.filter(
|
filtered = filtered.filter(
|
||||||
(mod) =>
|
mod =>
|
||||||
mod.name.toLowerCase().includes(searchTerm) ||
|
mod.name.toLowerCase().includes(searchTerm) ||
|
||||||
mod.description.toLowerCase().includes(searchTerm) ||
|
mod.description.toLowerCase().includes(searchTerm) ||
|
||||||
mod.author.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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,34 +121,36 @@ export function useModsSearch(mods: ZenTheme[]) {
|
||||||
const paginatedMods = filteredMods.slice(startIndex, endIndex)
|
const paginatedMods = filteredMods.slice(startIndex, endIndex)
|
||||||
|
|
||||||
const setSearch = (search: string) => {
|
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 = () => {
|
const toggleCreatedSort = () => {
|
||||||
setState((prev) => ({
|
setState(prev => ({
|
||||||
...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
|
page: 1, // Reset page when sort changes
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleUpdatedSort = () => {
|
const toggleUpdatedSort = () => {
|
||||||
setState((prev) => ({
|
setState(prev => ({
|
||||||
...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
|
page: 1, // Reset page when sort changes
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPage = (page: number) => {
|
const setPage = (page: number) => {
|
||||||
setState((prev) => ({
|
setState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
page: Math.max(1, Math.min(page, totalPages)),
|
page: Math.max(1, Math.min(page, totalPages)),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const setLimit = (limit: number) => {
|
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 {
|
return {
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,18 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,18 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,19 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,18 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,18 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
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
|
||||||
|
>
|
||||||
|
|
|
@ -31,7 +31,6 @@ const locale = getLocale(Astro)
|
||||||
return 'light'
|
return 'light'
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|
||||||
if (theme === 'light') {
|
if (theme === 'light') {
|
||||||
document.documentElement.setAttribute('data-theme', 'light')
|
document.documentElement.setAttribute('data-theme', 'light')
|
||||||
} else {
|
} else {
|
||||||
|
@ -39,27 +38,32 @@ const locale = getLocale(Astro)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html lang={locale}>
|
<html lang={locale}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
{redirect ? <meta http-equiv="refresh" content={`0;url=${redirect}`} /> : null}
|
{redirect ? <meta http-equiv="refresh" content={`0;url=${redirect}`} /> : null}
|
||||||
<meta name="description" content={description ?? defaultDescription} />
|
<meta name="description" content={description ?? defaultDescription} />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="sitemap" href="/sitemap-0.xml" />
|
<link rel="sitemap" href="/sitemap-0.xml" />
|
||||||
|
|
||||||
{isHome && (
|
|
||||||
// @prettier-ignore
|
|
||||||
<!-- Injecting schema to homepage only (for SEO) -->
|
|
||||||
<script is:inline type="application/ld+json">
|
|
||||||
{
|
{
|
||||||
"@context":"https://schema.org",
|
isHome && (
|
||||||
"@type":"WebSite",
|
<>
|
||||||
"name":"Zen Browser",
|
{/* Injecting schema to homepage only (for SEO) */}
|
||||||
"url":"https://zen-browser.app/"
|
<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/',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</script>)}
|
|
||||||
|
|
||||||
<!-- ICO favicon as a fallback for browsers that don't support SVG favicons (Safari) -->
|
<!-- ICO favicon as a fallback for browsers that don't support SVG favicons (Safari) -->
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<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:title" content={title} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:image" content={ogImage ?? defaultOgImage} />
|
<meta property="og:image" content={ogImage ?? defaultOgImage} />
|
||||||
<meta
|
<meta property="og:description" content={description ?? defaultDescription} />
|
||||||
property="og:description"
|
|
||||||
content={description ?? defaultDescription}
|
|
||||||
/>
|
|
||||||
<meta property="og:color" content="#da755b" />
|
<meta property="og:color" content="#da755b" />
|
||||||
<!-- Twitter card -->
|
<!-- Twitter card -->
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
@ -88,17 +89,15 @@ const locale = getLocale(Astro)
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(
|
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;',
|
'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;'
|
'color: #f76f53; font-size: 16px; font-family: "Rubik"; margin-bottom: 20px;'
|
||||||
);
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body class="overflow-x-hidden text-balance bg-paper font-['bricolage-grotesque'] text-dark">
|
||||||
class="overflow-x-hidden bg-paper font-['bricolage-grotesque'] text-dark text-balance"
|
|
||||||
>
|
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<slot />
|
<slot />
|
||||||
<Footer />
|
<Footer />
|
||||||
|
@ -127,13 +126,13 @@ const locale = getLocale(Astro)
|
||||||
--zen-paper: #f2f0e3;
|
--zen-paper: #f2f0e3;
|
||||||
--zen-dark: #2e2e2e;
|
--zen-dark: #2e2e2e;
|
||||||
--zen-muted: rgba(0, 0, 0, 0.05);
|
--zen-muted: rgba(0, 0, 0, 0.05);
|
||||||
--zen-subtle: rgba(0,0,0,0.05);
|
--zen-subtle: rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
&[data-theme='dark'] {
|
&[data-theme='dark'] {
|
||||||
--zen-paper: #1f1f1f;
|
--zen-paper: #1f1f1f;
|
||||||
--zen-dark: #d1cfc0;
|
--zen-dark: #d1cfc0;
|
||||||
--zen-muted: rgba(255, 255, 255, 0.05);
|
--zen-muted: rgba(255, 255, 255, 0.05);
|
||||||
--zen-subtle: rgba(255,255,255,0.1);
|
--zen-subtle: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
|
|
||||||
export interface ZenTheme {
|
export type ZenTheme = {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
image: string
|
image: string
|
||||||
|
@ -24,7 +24,7 @@ export async function getAllMods(): Promise<ZenTheme[]> {
|
||||||
const res = await fetch(THEME_API)
|
const res = await fetch(THEME_API)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
// convert dict to array
|
// convert dict to array
|
||||||
const mods = Object.keys(json).map((key) => json[key])
|
const mods = Object.keys(json).map(key => json[key])
|
||||||
return mods
|
return mods
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
|
@ -17,9 +17,7 @@ const {
|
||||||
<main
|
<main
|
||||||
class="container flex min-h-[70vh] flex-col items-center justify-center gap-6 py-24 text-center"
|
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">
|
<Title class="text-7xl font-bold text-coral md:text-9xl xl:text-9xl"> 404 </Title>
|
||||||
404
|
|
||||||
</Title>
|
|
||||||
<div class="flex flex-col items-center gap-6">
|
<div class="flex flex-col items-center gap-6">
|
||||||
<Description class="text-xl md:text-2xl">
|
<Description class="text-xl md:text-2xl">
|
||||||
{notFound.title}
|
{notFound.title}
|
||||||
|
@ -27,7 +25,7 @@ const {
|
||||||
<p class="max-w-xl text-lg text-gray-500 dark:text-gray-400">
|
<p class="max-w-xl text-lg text-gray-500 dark:text-gray-400">
|
||||||
{notFound.description}
|
{notFound.description}
|
||||||
</p>
|
</p>
|
||||||
<Button href={getLocalePath("/")} isPrimary class="w-fit">
|
<Button href={getLocalePath('/')} isPrimary class="w-fit">
|
||||||
{notFound.button}
|
{notFound.button}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,31 +14,27 @@ const {
|
||||||
} = getUI(locale)
|
} = getUI(locale)
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout title={layout.about.title} description={layout.about.description}>
|
||||||
title={layout.about.title}
|
<main class="container flex min-h-screen w-full flex-col gap-24 py-24">
|
||||||
description={layout.about.description}
|
<div class="flex w-full flex-col gap-6">
|
||||||
>
|
|
||||||
<main
|
|
||||||
class="flex min-h-screen flex-col py-24 container w-full gap-24"
|
|
||||||
>
|
|
||||||
<div class="w-full flex flex-col gap-6">
|
|
||||||
<Description class="text-6xl font-bold leading-none">{about.title}</Description>
|
<Description class="text-6xl font-bold leading-none">{about.title}</Description>
|
||||||
<Description class="max-w-4xl">
|
<Description class="max-w-4xl">
|
||||||
{about.description}
|
{about.description}
|
||||||
</Description>
|
</Description>
|
||||||
<Button href="/donate" class="w-fit" isPrimary
|
<Button href="/donate" class="w-fit" isPrimary>{about.littleHelp}</Button>
|
||||||
>{about.littleHelp}</Button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4 w-full">
|
<div class="flex w-full flex-col gap-4">
|
||||||
<div class="text-4xl lg:text-5xl font-bold leading-none">{about.mainTeam.title}</div>
|
<div class="text-4xl font-bold leading-none lg:text-5xl">{about.mainTeam.title}</div>
|
||||||
<Description>
|
<Description>
|
||||||
{about.mainTeam.description}
|
{about.mainTeam.description}
|
||||||
</Description>
|
</Description>
|
||||||
<div class="flex flex-col gap-6">
|
<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="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">
|
<ul class="flex flex-col gap-2">
|
||||||
{Object.entries(members).map(([_key, member]) => (
|
{Object.entries(members).map(([_key, member]) => (
|
||||||
<li class="text-sm">
|
<li class="text-sm">
|
||||||
|
@ -54,15 +50,19 @@ const {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4 w-full">
|
<div class="flex w-full flex-col gap-4">
|
||||||
<div class="text-4xl lg:text-5xl font-bold leading-none">{about.contributors.title}</div>
|
<div class="text-4xl font-bold leading-none lg:text-5xl">{about.contributors.title}</div>
|
||||||
<Description>
|
<Description>
|
||||||
{about.contributors.description}
|
{about.contributors.description}
|
||||||
</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"
|
<a href="https://github.com/zen-browser/desktop/graphs/contributors"
|
||||||
><Image
|
><Image
|
||||||
src="https://contributors-img.web.app/image?repo=zen-browser/desktop"
|
src="https://contributors-img.web.app/image?repo=zen-browser/desktop"
|
||||||
|
@ -70,8 +70,12 @@ const {
|
||||||
width={500}
|
width={500}
|
||||||
height={500}
|
height={500}
|
||||||
/></a
|
/></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"
|
<a href="https://github.com/zen-browser/www/graphs/contributors"
|
||||||
><Image
|
><Image
|
||||||
src="https://contributors-img.web.app/image?repo=zen-browser/www"
|
src="https://contributors-img.web.app/image?repo=zen-browser/www"
|
||||||
|
@ -79,7 +83,8 @@ const {
|
||||||
width={500}
|
width={500}
|
||||||
height={500}
|
height={500}
|
||||||
/></a
|
/></a
|
||||||
></div></div>
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -14,26 +14,20 @@ const {
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={layout.donate.title} description={layout.donate.description}>
|
<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">
|
<div class="flex flex-col gap-4 lg:text-center">
|
||||||
<Description class="text-6xl font-bold">{donate.title}</Description>
|
<Description class="text-6xl font-bold">{donate.title}</Description>
|
||||||
<Description class="max-w-3xl">
|
<Description class="max-w-3xl">
|
||||||
{donate.description}
|
{donate.description}
|
||||||
</Description>
|
</Description>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="grid max-w-5xl grid-cols-1 gap-12 text-center lg:grid-cols-[1fr_1px_1fr]">
|
||||||
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="flex flex-col items-center gap-4">
|
||||||
<div class="text-6xl font-bold">{donate.patreon.title}</div>
|
<div class="text-6xl font-bold">{donate.patreon.title}</div>
|
||||||
<Description>
|
<Description>
|
||||||
{donate.patreon.description}
|
{donate.patreon.description}
|
||||||
</Description>
|
</Description>
|
||||||
<Button
|
<Button isPrimary href="https://www.patreon.com/zen_browser" class="w-fit">
|
||||||
isPrimary
|
|
||||||
href="https://www.patreon.com/zen_browser"
|
|
||||||
class="w-fit"
|
|
||||||
>
|
|
||||||
{donate.patreon.button}
|
{donate.patreon.button}
|
||||||
<ArrowRightIcon class="size-4" />
|
<ArrowRightIcon class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -39,9 +39,7 @@ const platformDescriptions = download.platformDescriptions
|
||||||
<main class="flex min-h-screen flex-col px-6 data-[os='windows']:bg-zen-blue">
|
<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="container relative mx-auto py-12">
|
||||||
<div class="mb-6 mt-12 flex flex-col gap-4">
|
<div class="mb-6 mt-12 flex flex-col gap-4">
|
||||||
<Description id="download-title" class="text-6xl font-bold"
|
<Description id="download-title" class="text-6xl font-bold">{download.title}</Description>
|
||||||
>{download.title}</Description
|
|
||||||
>
|
|
||||||
<Description class="max-w-xl text-pretty">
|
<Description class="max-w-xl text-pretty">
|
||||||
{download.description}
|
{download.description}
|
||||||
</Description>
|
</Description>
|
||||||
|
@ -191,9 +189,7 @@ const platformDescriptions = download.platformDescriptions
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Security Notice -->
|
<!-- Security Notice -->
|
||||||
<div
|
<div class="bg-opaicty-10 grid grid-cols-[auto,1fr] gap-4 rounded-2xl bg-subtle p-6">
|
||||||
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">
|
<div class="h-fit rounded-xl bg-subtle p-3">
|
||||||
<LockIcon class="h-5 w-5" />
|
<LockIcon class="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -202,10 +198,7 @@ const platformDescriptions = download.platformDescriptions
|
||||||
<h3 class="mb-2 text-lg font-medium">
|
<h3 class="mb-2 text-lg font-medium">
|
||||||
{download.securityNotice.title}
|
{download.securityNotice.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p class="text-muted-foreground" set:html={download.securityNotice.description} />
|
||||||
class="text-muted-foreground"
|
|
||||||
set:html={download.securityNotice.description}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import rss, { type RSSOptions } from '@astrojs/rss'
|
import rss, { type RSSOptions } from '@astrojs/rss'
|
||||||
import { releaseNotes } from '~/release-notes'
|
|
||||||
import type { ReleaseNote } from '~/release-notes'
|
import { releaseNotes, type ReleaseNote } from '~/release-notes'
|
||||||
|
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from '~/utils/i18n'
|
||||||
|
|
||||||
/** The default number of entries to include in the RSS feed. */
|
/** The default number of entries to include in the RSS feed. */
|
||||||
|
@ -12,7 +13,8 @@ const RSS_ENTRY_LIMIT = 20
|
||||||
*/
|
*/
|
||||||
export function GET(context: { url: URL }) {
|
export function GET(context: { url: URL }) {
|
||||||
// Just in case the release notes array is empty for whatever reason.
|
// 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 = {
|
const rssData: RSSOptions = {
|
||||||
title: 'Zen Browser Release Notes',
|
title: 'Zen Browser Release Notes',
|
||||||
|
@ -79,7 +81,10 @@ function formatReleaseNote(releaseNote: ReleaseNote) {
|
||||||
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(
|
||||||
|
'⚠️ Breaking changes',
|
||||||
|
releaseNote.breakingChanges?.map(breakingChangeToReleaseNote)
|
||||||
|
)
|
||||||
content += addReleaseNoteSection('✓ Fixes', releaseNote.fixes?.map(fixToReleaseNote))
|
content += addReleaseNoteSection('✓ Fixes', releaseNote.fixes?.map(fixToReleaseNote))
|
||||||
content += addReleaseNoteSection('🖌 Theme Changes', releaseNote.themeChanges)
|
content += addReleaseNoteSection('🖌 Theme Changes', releaseNote.themeChanges)
|
||||||
content += addReleaseNoteSection('⭐ Features', releaseNote.features)
|
content += addReleaseNoteSection('⭐ Features', releaseNote.features)
|
||||||
|
@ -119,7 +124,9 @@ function fixToReleaseNote(fix?: Exclude<ReleaseNote['fixes'], undefined>[number]
|
||||||
return note
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
function breakingChangeToReleaseNote(breakingChange?: Exclude<ReleaseNote['breakingChanges'], undefined>[number]) {
|
function breakingChangeToReleaseNote(
|
||||||
|
breakingChange?: Exclude<ReleaseNote['breakingChanges'], undefined>[number]
|
||||||
|
) {
|
||||||
if (typeof breakingChange === 'string') {
|
if (typeof breakingChange === 'string') {
|
||||||
return breakingChange
|
return breakingChange
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,7 @@ const locale = getLocale(Astro)
|
||||||
const { layout } = getUI(locale)
|
const { layout } = getUI(locale)
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout title={layout.index.title} description={layout.index.description} isHome>
|
||||||
title={layout.index.title}
|
|
||||||
description={layout.index.description}
|
|
||||||
isHome
|
|
||||||
>
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<Hero />
|
<Hero />
|
||||||
<Features />
|
<Features />
|
||||||
|
|
|
@ -11,8 +11,8 @@ import { getLocale, getOtherLocales } from '~/utils/i18n'
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const mods = await getAllMods()
|
const mods = await getAllMods()
|
||||||
return mods.flatMap((mod) => [
|
return mods.flatMap(mod => [
|
||||||
...getOtherLocales().map((locale) => ({
|
...getOtherLocales().map(locale => ({
|
||||||
params: {
|
params: {
|
||||||
slug: mod.id,
|
slug: mod.id,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
|
@ -97,39 +97,22 @@ const {
|
||||||
.replace('{version}', mod.version)
|
.replace('{version}', mod.version)
|
||||||
.replace('{link}', getAuthorLink(mod.author))}
|
.replace('{link}', getAuthorLink(mod.author))}
|
||||||
/>
|
/>
|
||||||
<p
|
<p set:html={slug.creationDate.replace('{createdAt}', dates.createdAt)} />
|
||||||
set:html={slug.creationDate.replace('{createdAt}', dates.createdAt)}
|
|
||||||
/>
|
|
||||||
{
|
{
|
||||||
dates.createdAt !== dates.updatedAt && (
|
dates.createdAt !== dates.updatedAt && (
|
||||||
<p
|
<p set:html={slug.latestUpdate.replace('{updatedAt}', dates.updatedAt)} />
|
||||||
set:html={slug.latestUpdate.replace(
|
|
||||||
'{updatedAt}',
|
|
||||||
dates.updatedAt,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
mod.homepage && (
|
mod.homepage && (
|
||||||
<a
|
<a href={mod.homepage} target="_blank" rel="noopener noreferrer" class="zen-link">
|
||||||
href={mod.homepage}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="zen-link"
|
|
||||||
>
|
|
||||||
{slug.visitModHomepage}
|
{slug.visitModHomepage}
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col sm:items-end">
|
<div class="flex flex-col sm:items-end">
|
||||||
<Button
|
<Button class="hidden" id="install-theme" extra={{ 'zen-theme-id': mod.id }} isPrimary>
|
||||||
class="hidden"
|
|
||||||
id="install-theme"
|
|
||||||
extra={{ 'zen-theme-id': mod.id }}
|
|
||||||
isPrimary
|
|
||||||
>
|
|
||||||
{slug.installMod}
|
{slug.installMod}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -27,10 +27,6 @@ const allMods = (await getAllMods()) || []
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Importing ModList component -->
|
<!-- Importing ModList component -->
|
||||||
<ModsList
|
<ModsList allMods={allMods} locale={locale ?? CONSTANT.I18N.DEFAULT_LOCALE} client:load />
|
||||||
allMods={allMods}
|
|
||||||
locale={locale ?? CONSTANT.I18N.DEFAULT_LOCALE}
|
|
||||||
client:load
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -12,10 +12,7 @@ const {
|
||||||
} = getUI(locale)
|
} = getUI(locale)
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout title={layout.privacyPolicy.title} description={layout.privacyPolicy.description}>
|
||||||
title={layout.privacyPolicy.title}
|
|
||||||
description={layout.privacyPolicy.description}
|
|
||||||
>
|
|
||||||
<main class="mx-auto mt-52 w-1/2 pb-24">
|
<main class="mx-auto mt-52 w-1/2 pb-24">
|
||||||
<Title id="privacy-policy" class="xl:text-6xl">{privacyPolicy.title}</Title>
|
<Title id="privacy-policy" class="xl:text-6xl">{privacyPolicy.title}</Title>
|
||||||
<div class="ml-4 font-bold">{privacyPolicy.lastUpdated}</div>
|
<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">
|
<div class="mx-12 my-12 flex gap-4 font-bold">
|
||||||
{privacyPolicy.sections.introduction.summary}
|
{privacyPolicy.sections.introduction.summary}
|
||||||
</div>
|
</div>
|
||||||
<Title
|
<Title class="mt-16 text-4xl font-bold" id="1-information-we-do-not-collect">
|
||||||
class="mt-16 text-4xl font-bold"
|
|
||||||
id="1-information-we-do-not-collect"
|
|
||||||
>
|
|
||||||
{privacyPolicy.sections.noCollect.title}
|
{privacyPolicy.sections.noCollect.title}
|
||||||
</Title>
|
</Title>
|
||||||
<p>{privacyPolicy.sections.noCollect.body}</p>
|
<p>{privacyPolicy.sections.noCollect.body}</p>
|
||||||
<h3 class="mt-4 text-xl font-bold" id="-1-1-no-telemetry-">
|
<h3 class="mt-4 text-xl font-bold" id="-1-1-no-telemetry-">
|
||||||
<strong class="font-bold"
|
<strong class="font-bold">{privacyPolicy.sections.noTelemetry.title}</strong>
|
||||||
>{privacyPolicy.sections.noTelemetry.title}</strong
|
|
||||||
>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p>{privacyPolicy.sections.noTelemetry.body}</p>
|
<p>{privacyPolicy.sections.noTelemetry.body}</p>
|
||||||
<p>{privacyPolicy.sections.noTelemetry.body2}</p>
|
<p>{privacyPolicy.sections.noTelemetry.body2}</p>
|
||||||
<h3 class="mt-4 text-xl font-bold" id="-1-2-no-personal-data-collection-">
|
<h3 class="mt-4 text-xl font-bold" id="-1-2-no-personal-data-collection-">
|
||||||
<strong class="font-bold"
|
<strong class="font-bold">{privacyPolicy.sections.noPersonalData.title}</strong>
|
||||||
>{privacyPolicy.sections.noPersonalData.title}</strong
|
|
||||||
>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p>{privacyPolicy.sections.noPersonalData.body}</p>
|
<p>{privacyPolicy.sections.noPersonalData.body}</p>
|
||||||
<h3 class="mt-4 text-xl font-bold" id="-1-4-no-third-party-tracking-">
|
<h3 class="mt-4 text-xl font-bold" id="-1-4-no-third-party-tracking-">
|
||||||
<strong class="font-bold"
|
<strong class="font-bold">{privacyPolicy.sections.noThirdParty.title}</strong>
|
||||||
>{privacyPolicy.sections.noThirdParty.title}</strong
|
|
||||||
>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p>{privacyPolicy.sections.noThirdParty.body}</p>
|
<p>{privacyPolicy.sections.noThirdParty.body}</p>
|
||||||
<h3 class="mt-4 text-xl font-bold" id="-1-3-no-third-party-tracking-">
|
<h3 class="mt-4 text-xl font-bold" id="-1-3-no-third-party-tracking-">
|
||||||
<strong class="font-bold"
|
<strong class="font-bold">{privacyPolicy.sections.externalConnections.title}</strong>
|
||||||
>{privacyPolicy.sections.externalConnections.title}</strong
|
|
||||||
>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p>{privacyPolicy.sections.externalConnections.body}</p>
|
<p>{privacyPolicy.sections.externalConnections.body}</p>
|
||||||
<Title
|
<Title class="mt-16 text-4xl font-bold" id="2-information-stored-locally-on-your-device">
|
||||||
class="mt-16 text-4xl font-bold"
|
|
||||||
id="2-information-stored-locally-on-your-device"
|
|
||||||
>
|
|
||||||
{privacyPolicy.sections.localStorage.title}
|
{privacyPolicy.sections.localStorage.title}
|
||||||
</Title>
|
</Title>
|
||||||
<h3 class="mt-4 text-xl font-bold" id="-2-1-browsing-data-">
|
<h3 class="mt-4 text-xl font-bold" id="-2-1-browsing-data-">
|
||||||
<strong class="font-bold"
|
<strong class="font-bold">{privacyPolicy.sections.browsingData.title}</strong>
|
||||||
>{privacyPolicy.sections.browsingData.title}</strong
|
|
||||||
>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p>{privacyPolicy.sections.browsingData.body}</p>
|
<p>{privacyPolicy.sections.browsingData.body}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<strong class="font-bold">{privacyPolicy.sections.cookies.title}</strong
|
<strong class="font-bold">{privacyPolicy.sections.cookies.title}</strong>: {
|
||||||
>: {privacyPolicy.sections.cookies.body}
|
privacyPolicy.sections.cookies.body
|
||||||
|
}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong class="font-bold">{privacyPolicy.sections.cache.title}</strong>: {
|
<strong class="font-bold">{privacyPolicy.sections.cache.title}</strong>: {
|
||||||
|
@ -91,9 +73,7 @@ const {
|
||||||
<p>{privacyPolicy.sections.sync.body}</p>
|
<p>{privacyPolicy.sections.sync.body}</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="zen-link" href="https://www.mozilla.org/en-US/privacy/mozilla-accounts/"
|
||||||
class="zen-link"
|
|
||||||
href="https://www.mozilla.org/en-US/privacy/mozilla-accounts/"
|
|
||||||
>{privacyPolicy.sections.sync.link1}</a
|
>{privacyPolicy.sections.sync.link1}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
@ -120,9 +100,7 @@ const {
|
||||||
{privacyPolicy.sections.control.title}
|
{privacyPolicy.sections.control.title}
|
||||||
</Title>
|
</Title>
|
||||||
<h3 class="mt-4 text-xl font-bold" id="-6-1-data-deletion-">
|
<h3 class="mt-4 text-xl font-bold" id="-6-1-data-deletion-">
|
||||||
<strong class="font-bold"
|
<strong class="font-bold">{privacyPolicy.sections.control.deletionTitle}</strong>
|
||||||
>{privacyPolicy.sections.control.deletionTitle}</strong
|
|
||||||
>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p>{privacyPolicy.sections.control.deletionBody}</p>
|
<p>{privacyPolicy.sections.control.deletionBody}</p>
|
||||||
<Title class="mt-16 text-4xl font-bold" id="7-our-website-and-services">
|
<Title class="mt-16 text-4xl font-bold" id="7-our-website-and-services">
|
||||||
|
@ -130,24 +108,16 @@ const {
|
||||||
</Title>
|
</Title>
|
||||||
<p>{privacyPolicy.sections.website.body}</p>
|
<p>{privacyPolicy.sections.website.body}</p>
|
||||||
<h3 class="mt-4 text-xl font-bold" id="-7-1-external-links-">
|
<h3 class="mt-4 text-xl font-bold" id="-7-1-external-links-">
|
||||||
<strong class="font-bold"
|
<strong class="font-bold">{privacyPolicy.sections.website.externalLinksTitle}</strong>
|
||||||
>{privacyPolicy.sections.website.externalLinksTitle}</strong
|
|
||||||
>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p>{privacyPolicy.sections.website.externalLinksBody}</p>
|
<p>{privacyPolicy.sections.website.externalLinksBody}</p>
|
||||||
<Title
|
<Title class="mt-16 text-4xl font-bold" id="8-changes-to-this-privacy-policy">
|
||||||
class="mt-16 text-4xl font-bold"
|
|
||||||
id="8-changes-to-this-privacy-policy"
|
|
||||||
>
|
|
||||||
{privacyPolicy.sections.changes.title}
|
{privacyPolicy.sections.changes.title}
|
||||||
</Title>
|
</Title>
|
||||||
<p>
|
<p>
|
||||||
{privacyPolicy.sections.changes.body}
|
{privacyPolicy.sections.changes.body}
|
||||||
</p>
|
</p>
|
||||||
<Title
|
<Title class="mt-16 text-4xl font-bold" id="9-other-telemetry-done-by-mozilla-firefox">
|
||||||
class="mt-16 text-4xl font-bold"
|
|
||||||
id="9-other-telemetry-done-by-mozilla-firefox"
|
|
||||||
>
|
|
||||||
{privacyPolicy.sections.otherTelemetry.title}
|
{privacyPolicy.sections.otherTelemetry.title}
|
||||||
</Title>
|
</Title>
|
||||||
<p>
|
<p>
|
||||||
|
@ -155,9 +125,7 @@ const {
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Please check <a
|
Please check <a class="zen-link" href="https://www.mozilla.org/en-US/privacy/"
|
||||||
class="zen-link"
|
|
||||||
href="https://www.mozilla.org/en-US/privacy/"
|
|
||||||
>{privacyPolicy.sections.otherTelemetry.firefoxPrivacyNotice}</a
|
>{privacyPolicy.sections.otherTelemetry.firefoxPrivacyNotice}</a
|
||||||
>
|
>
|
||||||
{privacyPolicy.sections.otherTelemetry.forMoreInformation}
|
{privacyPolicy.sections.otherTelemetry.forMoreInformation}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export async function getStaticPaths() {
|
||||||
const i18nPaths = getI18nPaths()
|
const i18nPaths = getI18nPaths()
|
||||||
|
|
||||||
return i18nPaths.flatMap(({ params: { locale } }) => [
|
return i18nPaths.flatMap(({ params: { locale } }) => [
|
||||||
...releaseNotes.map((release) => ({
|
...releaseNotes.map(release => ({
|
||||||
params: { slug: release.version, locale },
|
params: { slug: release.version, locale },
|
||||||
props: { ...release },
|
props: { ...release },
|
||||||
})),
|
})),
|
||||||
|
|
|
@ -21,21 +21,16 @@ const {
|
||||||
<main
|
<main
|
||||||
class="container flex h-full min-h-[1000px] flex-1 flex-col items-center justify-center py-4"
|
class="container flex h-full min-h-[1000px] flex-1 flex-col items-center justify-center py-4"
|
||||||
>
|
>
|
||||||
<div
|
<div id="release-notes" class="py-42 flex min-h-screen w-full flex-col justify-center gap-8">
|
||||||
id="release-notes"
|
|
||||||
class="py-42 flex min-h-screen gap-8 w-full flex-col justify-center"
|
|
||||||
>
|
|
||||||
<Description class="mt-48 text-6xl font-bold">Changelog</Description>
|
<Description class="mt-48 text-6xl font-bold">Changelog</Description>
|
||||||
<p
|
<p
|
||||||
class="text-base opacity-55"
|
class="text-base opacity-55"
|
||||||
set:html={releaseNotes.topSection.description.replaceAll(
|
set:html={releaseNotes.topSection.description.replaceAll(
|
||||||
'{latestVersion}',
|
'{latestVersion}',
|
||||||
releaseNotesData[0].version,
|
releaseNotesData[0].version
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div class="mt-8 flex w-fit flex-col gap-4 sm:mr-0 sm:flex-row sm:items-center">
|
||||||
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">
|
<Button class="flex" isPrimary href="/donate">
|
||||||
{releaseNotes.list.support}
|
{releaseNotes.list.support}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -44,12 +39,11 @@ const {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
releaseNotesTwilight.features.length ||
|
releaseNotesTwilight.features.length || releaseNotesTwilight.fixes.length ? (
|
||||||
releaseNotesTwilight.fixes.length ? (
|
|
||||||
<ReleaseNoteItem {...releaseNotesTwilight} isTwilight />
|
<ReleaseNoteItem {...releaseNotesTwilight} isTwilight />
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
{releaseNotesData.map((notes: any) => <ReleaseNoteItem {...notes} />)}
|
{releaseNotesData.map(notes => <ReleaseNoteItem {...notes} />)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Button href="#" id="scroll-top" isPrimary class="fixed bottom-8 right-8">
|
<Button href="#" id="scroll-top" isPrimary class="fixed bottom-8 right-8">
|
||||||
|
@ -67,7 +61,7 @@ const {
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div id="version-list" class="flex flex-col gap-2 text-xl text-dark">
|
<div id="version-list" class="flex flex-col gap-2 text-xl text-dark">
|
||||||
{
|
{
|
||||||
releaseNotesData.map((note) => (
|
releaseNotesData.map(note => (
|
||||||
<button
|
<button
|
||||||
aria-label={`Navigate to version ${note.version}`}
|
aria-label={`Navigate to version ${note.version}`}
|
||||||
class="w-full text-left transition-colors duration-150 hover:text-coral"
|
class="w-full text-left transition-colors duration-150 hover:text-coral"
|
||||||
|
@ -81,7 +75,7 @@ const {
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
<script>
|
<script>
|
||||||
import { openModal, closeModal } from 'free-astro-components'
|
import { closeModal, openModal } from 'free-astro-components'
|
||||||
|
|
||||||
const scrollTopButton = document.getElementById('scroll-top')
|
const scrollTopButton = document.getElementById('scroll-top')
|
||||||
const versionButton = document.getElementById('navigate-to-version')
|
const versionButton = document.getElementById('navigate-to-version')
|
||||||
|
@ -110,11 +104,9 @@ const {
|
||||||
if (!version) return
|
if (!version) return
|
||||||
window.location.hash = version
|
window.location.hash = version
|
||||||
|
|
||||||
const versionDetails = document
|
const versionDetails = document.getElementById(version)?.getElementsByTagName('details')
|
||||||
.getElementById(version)
|
|
||||||
?.getElementsByTagName('details')
|
|
||||||
if (versionDetails && versionDetails.length > 0) {
|
if (versionDetails && versionDetails.length > 0) {
|
||||||
Array.from(versionDetails).forEach((accordion) => {
|
Array.from(versionDetails).forEach(accordion => {
|
||||||
accordion.setAttribute('open', '')
|
accordion.setAttribute('open', '')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -131,7 +123,7 @@ const {
|
||||||
versionButton?.addEventListener('click', openVersionModal)
|
versionButton?.addEventListener('click', openVersionModal)
|
||||||
versionList?.addEventListener('click', navigateToVersion)
|
versionList?.addEventListener('click', navigateToVersion)
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', e => {
|
||||||
if (e.key === 'Escape' && modal?.hasAttribute('open')) {
|
if (e.key === 'Escape' && modal?.hasAttribute('open')) {
|
||||||
closeModal(modal)
|
closeModal(modal)
|
||||||
// Remove scroll lock if present
|
// Remove scroll lock if present
|
||||||
|
|
|
@ -14,10 +14,6 @@ const {
|
||||||
|
|
||||||
<Layout title={layout.welcome.title} description={layout.welcome.description}>
|
<Layout title={layout.welcome.title} description={layout.welcome.description}>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<Features
|
<Features title1={welcome.title[0]} title2={welcome.title[1]} title3={welcome.title[2]} />
|
||||||
title1={welcome.title[0]}
|
|
||||||
title2={welcome.title[1]}
|
|
||||||
title3={welcome.title[2]}
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -27,24 +27,14 @@ if (latestVersion.version.split('.').length > 2 && whatsNewText[1] !== latestVer
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout title={layout.whatsNew.title.replace('{latestVersion.version}', latestVersion.version)}>
|
||||||
title={layout.whatsNew.title.replace(
|
|
||||||
'{latestVersion.version}',
|
|
||||||
latestVersion.version,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<main
|
<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]"
|
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 class="flex flex-col gap-8">
|
||||||
<div>
|
<div>
|
||||||
<Description class="text-5xl font-bold md:text-6xl"
|
<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>
|
<Description>{latestVersion.date}</Description>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,10 +42,7 @@ if (latestVersion.version.split('.').length > 2 && whatsNewText[1] !== latestVer
|
||||||
<Fragment set:html={whatsNewText[0].replace(/\n/g, '<br>')} />
|
<Fragment set:html={whatsNewText[0].replace(/\n/g, '<br>')} />
|
||||||
</div>
|
</div>
|
||||||
<ul class="hidden list-disc flex-col gap-2 xl:container xl:flex">
|
<ul class="hidden list-disc flex-col gap-2 xl:container xl:flex">
|
||||||
<a
|
<a href="https://github.com/zen-browser/desktop/issues/new/choose" target="_blank">
|
||||||
href="https://github.com/zen-browser/desktop/issues/new/choose"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<Description class="text-base font-bold">
|
<Description class="text-base font-bold">
|
||||||
{whatsNew.reportIssue}
|
{whatsNew.reportIssue}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import releaseNotesStable from './release-notes/stable.json'
|
import releaseNotesStable from './release-notes/stable.json'
|
||||||
|
|
||||||
interface FixWithIssue {
|
type FixWithIssue = {
|
||||||
description: string
|
description: string
|
||||||
issue?: number
|
issue?: number
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ type Fix = string | FixWithIssue
|
||||||
|
|
||||||
export type BreakingChange = string | { description: string; link: string }
|
export type BreakingChange = string | { description: string; link: string }
|
||||||
|
|
||||||
export interface ReleaseNote {
|
export type ReleaseNote = {
|
||||||
version: string
|
version: string
|
||||||
date?: string // optional for twilight
|
date?: string // optional for twilight
|
||||||
extra?: string
|
extra?: string
|
||||||
|
|
|
@ -601,7 +601,10 @@
|
||||||
"version": "1.0.0-a.30",
|
"version": "1.0.0-a.30",
|
||||||
"date": "26/08/2024",
|
"date": "26/08/2024",
|
||||||
"extra": "This release is the thirtieth alpha release of the 1.0.0-alpha series.",
|
"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": [
|
"fixes": [
|
||||||
{
|
{
|
||||||
"description": "Letterboxing option is missing",
|
"description": "Letterboxing option is missing",
|
||||||
|
@ -924,7 +927,11 @@
|
||||||
"date": "24/09/2024",
|
"date": "24/09/2024",
|
||||||
"workflowId": 11020784612,
|
"workflowId": 11020784612,
|
||||||
"extra": "This update is a small patch to fix some issues that weren't addressed in the previous release!",
|
"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": [
|
"fixes": [
|
||||||
{
|
{
|
||||||
"description": "Fixed issue with hovering over window control buttons (macOS)"
|
"description": "Fixed issue with hovering over window control buttons (macOS)"
|
||||||
|
@ -949,7 +956,9 @@
|
||||||
"Improved Expand Tabs on Hover layout"
|
"Improved Expand Tabs on Hover layout"
|
||||||
],
|
],
|
||||||
"themeChanges": ["Toggle inputs will not use the themed tertiary color"],
|
"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": [
|
"fixes": [
|
||||||
{
|
{
|
||||||
"description": "Fixed Firefox add-ons not updating",
|
"description": "Fixed Firefox add-ons not updating",
|
||||||
|
@ -1121,7 +1130,10 @@
|
||||||
"description": "Fixed about page linking 'global Community' to a Mozilla page"
|
"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",
|
"version": "1.0.1-a.11",
|
||||||
|
@ -1258,7 +1270,9 @@
|
||||||
"description": "Fixed sidebar webpanels being in a darker contrast"
|
"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",
|
"version": "1.0.1-a.15",
|
||||||
|
@ -2097,7 +2111,10 @@
|
||||||
"date": "30/01/2025",
|
"date": "30/01/2025",
|
||||||
"workflowId": 13062083313,
|
"workflowId": 13062083313,
|
||||||
"extra": "Quick fix for a critical bug that was introduced in the previous release.",
|
"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",
|
"version": "1.7.5b",
|
||||||
|
@ -2154,7 +2171,9 @@
|
||||||
"Fixed opening glance tabs on essentials messing up the sidebar",
|
"Fixed opening glance tabs on essentials messing up the sidebar",
|
||||||
"Fixed pinned tabs appearing on normal container after a restart"
|
"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,
|
"workflowId": 13209591935,
|
||||||
"date": "08/02/2025"
|
"date": "08/02/2025"
|
||||||
},
|
},
|
||||||
|
@ -2684,7 +2703,10 @@
|
||||||
"'All tabs' menu not showing any text when collapsed toolbar is enabled."
|
"'All tabs' menu not showing any text when collapsed toolbar is enabled."
|
||||||
],
|
],
|
||||||
"security": "https://www.mozilla.org/en-US/security/advisories/mfsa2025-36/",
|
"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."],
|
"knownIssues": ["Selecting a tab on private mode doesn't scroll to make the tab visible."],
|
||||||
"themeChanges": [
|
"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."
|
"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."
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
||||||
import { beforeEach, describe, expect, it } from 'vitest'
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import Button from '~/components/Button.astro'
|
import Button from '~/components/Button.astro'
|
||||||
|
|
||||||
describe('<Button />', () => {
|
describe('<Button />', () => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
||||||
import { beforeEach, describe, expect, it } from 'vitest'
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import ButtonCard from '~/components/download/ButtonCard.astro'
|
import ButtonCard from '~/components/download/ButtonCard.astro'
|
||||||
|
|
||||||
describe('<ButtonCard />', () => {
|
describe('<ButtonCard />', () => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
||||||
import { beforeEach, describe, expect, it } from 'vitest'
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import PlatformDownload from '~/components/download/PlatformDownload.astro'
|
import PlatformDownload from '~/components/download/PlatformDownload.astro'
|
||||||
|
|
||||||
const mockIcon = ['<svg></svg>']
|
const mockIcon = ['<svg></svg>']
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { getReleasesWithChecksums } from '~/components/download/release-data'
|
import { getReleasesWithChecksums } from '~/components/download/release-data'
|
||||||
|
|
||||||
describe('getReleasesWithChecksums', () => {
|
describe('getReleasesWithChecksums', () => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect, test } from '@playwright/test'
|
import { expect, test, type BrowserContextOptions, type Page } from '@playwright/test'
|
||||||
import type { BrowserContextOptions, Page } from '@playwright/test'
|
|
||||||
import { getReleasesWithChecksums } from '~/components/download/release-data'
|
import { getReleasesWithChecksums } from '~/components/download/release-data'
|
||||||
import { CONSTANT } from '~/constants'
|
import { CONSTANT } from '~/constants'
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ const getPlatformButton = (page: Page, platform: string) =>
|
||||||
page.locator(`button.platform-selector[data-platform='${platform}']`)
|
page.locator(`button.platform-selector[data-platform='${platform}']`)
|
||||||
|
|
||||||
// Helper to get the platform download link
|
// 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}')`)
|
page.locator(`#${platform}-downloads .download-link:has-text('${label}')`)
|
||||||
|
|
||||||
const platformConfigs: { name: string; userAgent: string; platform: string }[] = [
|
const platformConfigs: { name: string; userAgent: string; platform: string }[] = [
|
||||||
|
@ -30,7 +30,8 @@ const platformConfigs: { name: string; userAgent: string; platform: string }[] =
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'linux',
|
name: 'linux',
|
||||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
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',
|
platform: 'Linux x86_64',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -48,7 +49,7 @@ test.describe('Download page default tab per platform', () => {
|
||||||
await expect(getPlatformSection(page, name)).toBeVisible()
|
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
|
// 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(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')
|
||||||
}
|
}
|
||||||
|
@ -66,9 +67,12 @@ test.describe('Download page platform detection and tab switching', () => {
|
||||||
await expect(getPlatformSection(page, platform)).toBeVisible()
|
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
|
// 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(getPlatformSection(page, otherPlatform)).toBeHidden()
|
||||||
await expect(getPlatformButton(page, otherPlatform)).not.toHaveAttribute('data-active', 'true')
|
await expect(getPlatformButton(page, otherPlatform)).not.toHaveAttribute(
|
||||||
|
'data-active',
|
||||||
|
'true'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -81,7 +85,11 @@ test.describe('Download page download links', () => {
|
||||||
return {
|
return {
|
||||||
mac: [releases.macos.universal],
|
mac: [releases.macos.universal],
|
||||||
windows: [releases.windows.x86_64, releases.windows.arm64],
|
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,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +100,9 @@ test.describe('Download page download links', () => {
|
||||||
await page.waitForLoadState('domcontentloaded')
|
await page.waitForLoadState('domcontentloaded')
|
||||||
for (const platform of platforms) {
|
for (const platform of platforms) {
|
||||||
await getPlatformButton(page, platform).click()
|
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}"]`)
|
const downloadLink = page.locator(`#${platform}-downloads .download-link[href="${link}"]`)
|
||||||
await expect(downloadLink).toContainText(label)
|
await expect(downloadLink).toContainText(label)
|
||||||
await expect(downloadLink).toHaveAttribute('href', link)
|
await expect(downloadLink).toHaveAttribute('href', link)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
import translation from '~/i18n/en/translation.json'
|
import translation from '~/i18n/en/translation.json'
|
||||||
|
|
||||||
vi.mock('~/utils/i18n', () => ({
|
vi.mock('~/utils/i18n', () => ({
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { GetStaticPaths } from 'astro'
|
import { type GetStaticPaths } from 'astro'
|
||||||
|
|
||||||
import { CONSTANT } from '~/constants'
|
import { CONSTANT } from '~/constants'
|
||||||
import UI_EN from '~/i18n/en/translation.json'
|
import UI_EN from '~/i18n/en/translation.json'
|
||||||
|
|
||||||
|
@ -44,7 +45,9 @@ export const locales = CONSTANT.I18N.LOCALES.map(({ value }) => value)
|
||||||
* List of locales excluding the default locale
|
* List of locales excluding the default locale
|
||||||
* @type {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
|
* Retrieves locales other than the default locale
|
||||||
|
@ -94,7 +97,7 @@ export const getUI = (locale?: Locale | string): UI => {
|
||||||
const result = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj }
|
const result = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj }
|
||||||
|
|
||||||
// Merge properties from the default object
|
// 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 defaultValue = defaultObj[key]
|
||||||
const overrideValue = overrideObj[key]
|
const overrideValue = overrideObj[key]
|
||||||
|
|
||||||
|
@ -106,7 +109,10 @@ export const getUI = (locale?: Locale | string): UI => {
|
||||||
typeof overrideValue === 'object'
|
typeof overrideValue === 'object'
|
||||||
) {
|
) {
|
||||||
// Type assertion to handle nested merging
|
// 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) {
|
} else if (overrideValue !== undefined) {
|
||||||
// Override with the new value if it exists
|
// Override with the new value if it exists
|
||||||
;(result as Record<keyof T, unknown>)[key] = overrideValue
|
;(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
|
// 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)) {
|
if (!(key in defaultObj)) {
|
||||||
;(result as Record<keyof T, unknown>)[key] = overrideObj[key]
|
;(result as Record<keyof T, unknown>)[key] = overrideObj[key]
|
||||||
}
|
}
|
||||||
|
@ -137,12 +143,14 @@ export const getStaticPaths = (() => {
|
||||||
params: { locale: undefined },
|
params: { locale: undefined },
|
||||||
props: { locale: CONSTANT.I18N.DEFAULT_LOCALE },
|
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 },
|
params: { locale: value },
|
||||||
props: {
|
props: {
|
||||||
locale: value,
|
locale: value,
|
||||||
},
|
},
|
||||||
})),
|
})
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}) satisfies GetStaticPaths
|
}) satisfies GetStaticPaths
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"extends": "astro/tsconfigs/strict",
|
"extends": "astro/tsconfigs/strict",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "preact",
|
"jsxImportSource": "react",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue