mirror of
https://github.com/zen-browser/www.git
synced 2025-07-07 17:05:32 +02:00
Merge branch 'main' of https://github.com/zen-browser/www
This commit is contained in:
commit
301199b9b2
88 changed files with 9185 additions and 3748 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:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: ci-pipeline-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -23,7 +27,7 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
biome:
|
||||
eslint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
steps:
|
||||
|
@ -39,8 +43,25 @@ jobs:
|
|||
path: |
|
||||
node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
|
||||
- name: Run Biome check
|
||||
run: npx biome check ./src
|
||||
- name: Run Eslint check
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
38
.github/workflows/renovate.yml
vendored
Normal file
38
.github/workflows/renovate.yml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
name: Renovate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every Monday at 8:00 AM UTC
|
||||
- cron: "0 8 * * 1"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
logLevel:
|
||||
description: "Log level"
|
||||
required: false
|
||||
default: "info"
|
||||
type: choice
|
||||
options:
|
||||
- info
|
||||
- debug
|
||||
- trace
|
||||
|
||||
env:
|
||||
LOG_LEVEL: ${{ inputs.logLevel || 'info' }}
|
||||
RENOVATE_REPOSITORIES: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Renovate
|
||||
uses: renovatebot/github-action@v42.0.4
|
||||
with:
|
||||
configurationFile: renovate.json
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
env:
|
||||
LOG_LEVEL: ${{ env.LOG_LEVEL }}
|
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,21 +1,30 @@
|
|||
import tailwind from '@astrojs/tailwind'
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config'
|
||||
|
||||
import preact from '@astrojs/preact'
|
||||
|
||||
import react from '@astrojs/react'
|
||||
import sitemap from '@astrojs/sitemap'
|
||||
import tailwind from '@astrojs/tailwind'
|
||||
import { defineConfig } from 'astro/config'
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [tailwind(), preact({ compat: true }), sitemap()],
|
||||
integrations: [tailwind(), react(), sitemap()],
|
||||
site: 'https://zen-browser.app',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
locales: ['en', 'ja'],
|
||||
routing: {
|
||||
fallbackType: 'rewrite',
|
||||
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;
|
8442
package-lock.json
generated
8442
package-lock.json
generated
File diff suppressed because it is too large
Load diff
112
package.json
112
package.json
|
@ -9,56 +9,86 @@
|
|||
"preview": "astro preview --port 3000",
|
||||
"wrangler": "wrangler",
|
||||
"astro": "astro",
|
||||
"lint": "biome lint ./src",
|
||||
"format": "biome format ./src",
|
||||
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.astro",
|
||||
"lint:fix": "eslint ./src --ext .js,.jsx,.ts,.tsx,.astro --fix",
|
||||
"format": "prettier --write ./src",
|
||||
"format:check": "prettier --check ./src",
|
||||
"prepare": "husky",
|
||||
"test": "npx vitest run",
|
||||
"test:coverage": "npx vitest --coverage",
|
||||
"test:playwright": "npx playwright test"
|
||||
"test:playwright": "npx playwright test --reporter=list"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/cloudflare": "^12.5.3",
|
||||
"@astrojs/preact": "^4.1.0",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.4.0",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"@fontsource/bricolage-grotesque": "^5.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.1",
|
||||
"astro": "^5.8.1",
|
||||
"astro-navbar": "^2.3.7",
|
||||
"@astrojs/check": "0.9.4",
|
||||
"@astrojs/cloudflare": "12.5.2",
|
||||
"@astrojs/react": "^4.3.0",
|
||||
"@astrojs/rss": "4.0.11",
|
||||
"@astrojs/sitemap": "3.3.1",
|
||||
"@astrojs/tailwind": "6.0.2",
|
||||
"@fontsource/bricolage-grotesque": "5.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"astro": "5.7.10",
|
||||
"astro-navbar": "2.3.7",
|
||||
"autoprefixer": "10.4.14",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"free-astro-components": "^1.2.0",
|
||||
"lucide-astro": "^0.460.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"motion": "^11.13.5",
|
||||
"postcss": "^8.5.1",
|
||||
"preact": "^10.26.2",
|
||||
"sharp": "^0.33.5",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.6.3"
|
||||
"clsx": "2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"free-astro-components": "1.2.0",
|
||||
"motion": "^12.15.0",
|
||||
"postcss": "8.5.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"sharp": "0.33.5",
|
||||
"tailwind-merge": "3.3.0",
|
||||
"tailwindcss": "3.4.15",
|
||||
"typescript": "5.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.15.18",
|
||||
"@vitest/coverage-istanbul": "^3.1.3",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.3",
|
||||
"wrangler": "^3.114.8"
|
||||
"@eslint/compat": "1.2.9",
|
||||
"@eslint/eslintrc": "3.3.1",
|
||||
"@eslint/js": "9.27.0",
|
||||
"@playwright/test": "1.52.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/eslint-plugin-jsx-a11y": "6.10.0",
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/node": "22.15.18",
|
||||
"@typescript-eslint/eslint-plugin": "8.33.0",
|
||||
"@typescript-eslint/parser": "8.33.0",
|
||||
"@typescript-eslint/utils": "8.33.0",
|
||||
"@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",
|
||||
"jsdom": "26.1.0",
|
||||
"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",
|
||||
"vitest": "3.1.3",
|
||||
"wrangler": "4.17.0"
|
||||
},
|
||||
"overrides": {
|
||||
"jiti": "2.4.2"
|
||||
},
|
||||
"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',
|
||||
testIgnore: ['**.test.ts'],
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
|
|
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,
|
||||
};
|
135
renovate.json
Normal file
135
renovate.json
Normal file
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":semanticCommits",
|
||||
"config:js-app"
|
||||
],
|
||||
"timezone": "UTC",
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"labels": ["dependencies"],
|
||||
"assignees": ["@me"],
|
||||
"prConcurrentLimit": 5,
|
||||
"prHourlyLimit": 2,
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"automerge": false,
|
||||
"schedule": ["before 9am on Monday"]
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Astro and React ecosystem packages",
|
||||
"matchPackagePatterns": ["^@astrojs/", "^astro", "^react", "^@types/react"],
|
||||
"groupName": "Astro and React core",
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"automerge": false,
|
||||
"reviewersFromCodeOwners": true
|
||||
},
|
||||
{
|
||||
"description": "Development dependencies",
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"groupName": "Dev dependencies",
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"automerge": false,
|
||||
"automergeType": "pr",
|
||||
"minimumReleaseAge": "3 days"
|
||||
},
|
||||
{
|
||||
"description": "Testing packages",
|
||||
"matchPackagePatterns": [
|
||||
"^@testing-library/",
|
||||
"^@playwright/",
|
||||
"^vitest",
|
||||
"^@vitest/",
|
||||
"^jsdom"
|
||||
],
|
||||
"groupName": "Testing packages",
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"automerge": false,
|
||||
"automergeType": "pr"
|
||||
},
|
||||
{
|
||||
"description": "ESLint and TypeScript packages",
|
||||
"matchPackagePatterns": ["^@typescript-eslint/", "^eslint", "^typescript", "^@types/"],
|
||||
"excludePackagePatterns": ["^@types/react"],
|
||||
"groupName": "TypeScript and ESLint",
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"automerge": false,
|
||||
"automergeType": "pr"
|
||||
},
|
||||
{
|
||||
"description": "Formatting and code quality tools",
|
||||
"matchPackageNames": [
|
||||
"prettier",
|
||||
"prettier-plugin-astro",
|
||||
"prettier-plugin-tailwindcss",
|
||||
"husky",
|
||||
"lint-staged"
|
||||
],
|
||||
"groupName": "Code formatting tools",
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"automerge": false,
|
||||
"automergeType": "pr"
|
||||
},
|
||||
{
|
||||
"description": "TailwindCSS ecosystem",
|
||||
"matchPackagePatterns": ["^tailwind", "^@tailwindcss/"],
|
||||
"matchPackageNames": ["autoprefixer", "postcss", "clsx", "tailwind-merge"],
|
||||
"groupName": "TailwindCSS ecosystem",
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "FontAwesome packages",
|
||||
"matchPackagePatterns": ["^@fortawesome/"],
|
||||
"groupName": "FontAwesome packages",
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"automerge": false,
|
||||
"automergeType": "pr"
|
||||
},
|
||||
{
|
||||
"description": "Lucide icon packages",
|
||||
"matchPackagePatterns": ["^lucide-"],
|
||||
"groupName": "Lucide icons",
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"automerge": false,
|
||||
"automergeType": "pr"
|
||||
},
|
||||
{
|
||||
"description": "Major updates require manual review",
|
||||
"matchUpdateTypes": ["major"],
|
||||
"automerge": false,
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"labels": ["dependencies", "major-update"],
|
||||
"reviewersFromCodeOwners": true
|
||||
},
|
||||
{
|
||||
"description": "Pin Node.js to LTS versions",
|
||||
"matchPackageNames": ["node"],
|
||||
"allowedVersions": "/^(18|20|22)\\./",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "Cloudflare Workers and Wrangler",
|
||||
"matchPackageNames": ["wrangler", "@astrojs/cloudflare"],
|
||||
"groupName": "Cloudflare ecosystem",
|
||||
"schedule": ["before 9am on Monday"],
|
||||
"automerge": false,
|
||||
"reviewersFromCodeOwners": true
|
||||
}
|
||||
],
|
||||
"vulnerabilityAlerts": {
|
||||
"enabled": false,
|
||||
"labels": ["security", "dependencies"],
|
||||
"automerge": false,
|
||||
"schedule": ["at any time"]
|
||||
},
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"dependencyDashboard": false,
|
||||
"dependencyDashboardTitle": "Dependency Dashboard",
|
||||
"dependencyDashboardHeader": "This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more.",
|
||||
"dependencyDashboardFooter": "Configure Renovate in `renovate.json`",
|
||||
"configMigration": true,
|
||||
"platformAutomerge": false,
|
||||
"automergeStrategy": "squash"
|
||||
}
|
|
@ -7,7 +7,7 @@ export function getTitleAnimation(delay = 0, duration = 0.3, once = true) {
|
|||
filter: 'blur(0px)',
|
||||
transition: { duration, delay },
|
||||
},
|
||||
viewport: { once: once },
|
||||
viewport: { once },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,12 +9,15 @@ const {
|
|||
mods: { slug },
|
||||
},
|
||||
} = getUI(locale)
|
||||
|
||||
const { href, ...props } = Astro.props
|
||||
|
||||
if (!href) {
|
||||
console.error('BackButton: href is required')
|
||||
}
|
||||
---
|
||||
|
||||
<button
|
||||
onclick="window.history.back()"
|
||||
class="mb-8 flex w-min items-center gap-2"
|
||||
>
|
||||
<a href={href} class="mb-8 flex w-fit items-center gap-2" {...props} data-testid="back-button">
|
||||
<ArrowLeftIcon class="size-4" />
|
||||
{slug.back}
|
||||
</button>
|
||||
</a>
|
||||
|
|
|
@ -3,7 +3,17 @@ import { getLocale, getPath } from '~/utils/i18n'
|
|||
|
||||
const locale = getLocale(Astro)
|
||||
const getLocalePath = getPath(locale)
|
||||
const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = Astro.props
|
||||
const {
|
||||
class: className,
|
||||
isPrimary,
|
||||
isAlert,
|
||||
isBordered,
|
||||
href,
|
||||
id,
|
||||
extra,
|
||||
localePath = true,
|
||||
...props
|
||||
} = Astro.props
|
||||
---
|
||||
|
||||
{
|
||||
|
@ -11,7 +21,7 @@ const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = As
|
|||
<a
|
||||
id={id}
|
||||
{...extra}
|
||||
href={getLocalePath(href)}
|
||||
href={localePath ? getLocalePath(href) : href}
|
||||
class:list={[
|
||||
'transition-bg flex items-center justify-center gap-2 rounded-xl px-6 py-4 transition-transform duration-150 hover:scale-[1.02] active:scale-[0.98]',
|
||||
className,
|
||||
|
@ -23,6 +33,7 @@ const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = As
|
|||
? 'bg-subtle'
|
||||
: '!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm',
|
||||
]}
|
||||
{...props}
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
|
@ -41,6 +52,7 @@ const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = As
|
|||
? ''
|
||||
: '!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm',
|
||||
]}
|
||||
{...props}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
|
|
@ -4,10 +4,7 @@ const sizes = [216, 396, 576, 756]
|
|||
const borderWidths = [20, 30, 40, 50]
|
||||
---
|
||||
|
||||
<div
|
||||
id="circles"
|
||||
class:list={['pointer-events-none inset-0 overflow-hidden', classList]}
|
||||
>
|
||||
<div id="circles" class:list={['pointer-events-none inset-0 overflow-hidden', classList]}>
|
||||
<div class="mx-auto opacity-10 lg:opacity-100">
|
||||
{
|
||||
[...Array(4)].map((_, i) => (
|
||||
|
|
|
@ -22,22 +22,20 @@ const {
|
|||
id="Community"
|
||||
class="relative flex w-full flex-col items-center gap-6 py-12 text-start md:text-center lg:py-36"
|
||||
>
|
||||
<Description class="mb-2 text-6xl font-bold">
|
||||
<motion.span client:load {...getTitleAnimation(0.2)}>
|
||||
{community.title[0]}
|
||||
</motion.span>
|
||||
<motion.span client:load {...getTitleAnimation(0.4)}>
|
||||
{community.title[1]}
|
||||
</motion.span>
|
||||
<motion.span client:load {...getTitleAnimation(0.6)}>
|
||||
{community.title[2]}
|
||||
</motion.span>
|
||||
<Description class="mb-2 text-4xl font-bold sm:text-6xl">
|
||||
{
|
||||
community.title.map((title, index) =>
|
||||
title !== '\n' ? (
|
||||
<motion.span client:load {...getTitleAnimation(0.2 + index * 0.2)}>
|
||||
{title}
|
||||
</motion.span>
|
||||
) : (
|
||||
<br class="hidden md:block" />
|
||||
)
|
||||
)
|
||||
}
|
||||
</Description>
|
||||
<motion.p
|
||||
client:load
|
||||
{...getTitleAnimation(0.6)}
|
||||
className="lg:w-1/2 lg:px-0"
|
||||
>
|
||||
<motion.p client:load {...getTitleAnimation(0.6)} className="lg:w-1/2 lg:px-0">
|
||||
{community.description}
|
||||
</motion.p>
|
||||
<div class="flex w-full flex-wrap gap-3 sm:gap-10 md:justify-center">
|
||||
|
@ -47,19 +45,11 @@ const {
|
|||
<span>{community.lists.freeAndOpenSource.title}</span>
|
||||
</Button>
|
||||
</motion.span>
|
||||
<motion.div
|
||||
client:load
|
||||
{...getTitleAnimation(1)}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<motion.div client:load {...getTitleAnimation(1)} className="flex items-center gap-4">
|
||||
<CheckIcon class="size-4" />
|
||||
<span>{community.lists.simpleYetPowerful.title}</span>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
client:load
|
||||
{...getTitleAnimation(1.2)}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<motion.div client:load {...getTitleAnimation(1.2)} className="flex items-center gap-4">
|
||||
<CheckIcon class="size-4" />
|
||||
<span>{community.lists.privateAndAlwaysUpToDate.title}</span>
|
||||
</motion.div>
|
||||
|
|
|
@ -19,25 +19,28 @@ const {
|
|||
},
|
||||
} = getUI(locale)
|
||||
|
||||
const { title1 = features.title1, title2 = features.title2, title3 = features.title3 } = Astro.props
|
||||
interface Props {
|
||||
titles?: string[]
|
||||
}
|
||||
|
||||
const descriptions = Object.values(features.featureTabs).map((tab) => tab.description)
|
||||
const { titles } = Astro.props
|
||||
|
||||
const descriptions = Object.values(features.featureTabs).map(tab => tab.description)
|
||||
---
|
||||
|
||||
<section
|
||||
id="Features"
|
||||
class="relative flex w-full flex-col py-12 text-start lg:py-36"
|
||||
>
|
||||
<Description class="mb-2 text-4xl sm:text-6xl font-bold">
|
||||
<motion.span client:load {...getTitleAnimation(0.2)}>
|
||||
{title1}
|
||||
</motion.span>
|
||||
<motion.span client:load {...getTitleAnimation(0.4)}>
|
||||
{title2}
|
||||
</motion.span>
|
||||
<motion.span client:load {...getTitleAnimation(0.6)}>
|
||||
{title3}
|
||||
</motion.span>
|
||||
<section id="Features" class="relative flex w-full flex-col py-12 text-start lg:py-36">
|
||||
<Description class="mb-2 text-4xl font-bold sm:text-6xl">
|
||||
{
|
||||
(titles || features.titles).map((title, index) =>
|
||||
title !== '\n' ? (
|
||||
<motion.span client:load {...getTitleAnimation(0.2 + index * 0.2)}>
|
||||
{title}
|
||||
</motion.span>
|
||||
) : (
|
||||
<br class="hidden md:block" />
|
||||
)
|
||||
)
|
||||
}
|
||||
</Description>
|
||||
<motion.p client:load {...getTitleAnimation(0.6)} className="lg:w-1/2">
|
||||
{features.description}
|
||||
|
@ -49,7 +52,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
<motion.button
|
||||
client:load
|
||||
{...getTitleAnimation()}
|
||||
class="feature-tab whitespace-nowrap"
|
||||
className="feature-tab whitespace-nowrap"
|
||||
data-active="true"
|
||||
>
|
||||
{features.featureTabs.workspaces.title}
|
||||
|
@ -57,21 +60,21 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
<motion.button
|
||||
client:load
|
||||
{...getTitleAnimation(0.2)}
|
||||
class="feature-tab whitespace-nowrap"
|
||||
className="feature-tab whitespace-nowrap"
|
||||
>
|
||||
{features.featureTabs.compactMode.title}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
client:load
|
||||
{...getTitleAnimation(0.4)}
|
||||
class="feature-tab whitespace-nowrap"
|
||||
className="feature-tab whitespace-nowrap"
|
||||
>
|
||||
{features.featureTabs.glance.title}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
client:load
|
||||
{...getTitleAnimation(0.6)}
|
||||
class="feature-tab whitespace-nowrap"
|
||||
className="feature-tab whitespace-nowrap"
|
||||
>
|
||||
{features.featureTabs.splitView.title}
|
||||
</motion.button>
|
||||
|
@ -79,12 +82,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
|
||||
<!-- Desktop features list -->
|
||||
<div id="features-list" class="hidden lg:flex lg:flex-col lg:gap-3">
|
||||
<motion.div
|
||||
client:load
|
||||
{...getTitleAnimation(0.8)}
|
||||
className="feature"
|
||||
data-active="true"
|
||||
>
|
||||
<motion.div client:load {...getTitleAnimation(0.8)} className="feature" data-active="true">
|
||||
<Description class="text-2xl font-bold">
|
||||
{features.featureTabs.workspaces.title}
|
||||
</Description>
|
||||
|
@ -119,14 +117,10 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
</div>
|
||||
|
||||
<!-- Mobile description -->
|
||||
<div
|
||||
class="feature-description mt-4 lg:hidden"
|
||||
data-descriptions={descriptions}
|
||||
>
|
||||
</div>
|
||||
<div class="feature-description mt-4 lg:hidden" data-descriptions={descriptions}></div>
|
||||
</div>
|
||||
|
||||
<div class="sticky top-6 w-full lg:w-3/5 h-fit">
|
||||
<div class="sticky top-6 h-fit w-full lg:w-3/5">
|
||||
<div class="relative w-full">
|
||||
<div class="video-stack relative h-full w-full">
|
||||
<Video
|
||||
|
@ -173,24 +167,16 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
</section>
|
||||
|
||||
<script>
|
||||
const features = document.querySelectorAll(
|
||||
'.feature, .feature-tab',
|
||||
) as NodeListOf<HTMLElement>
|
||||
const features = document.querySelectorAll('.feature, .feature-tab') as NodeListOf<HTMLElement>
|
||||
|
||||
// Set initial description
|
||||
const descriptionEl = document.querySelector(
|
||||
'.feature-description',
|
||||
) as HTMLDivElement
|
||||
const descriptionEl = document.querySelector('.feature-description') as HTMLDivElement
|
||||
const descriptions = descriptionEl?.dataset.descriptions?.split(',')
|
||||
if (descriptionEl && descriptions) {
|
||||
descriptionEl.textContent = descriptions[0]
|
||||
}
|
||||
|
||||
function changeToFeature({
|
||||
target,
|
||||
}: {
|
||||
target: HTMLElement | undefined | null
|
||||
}) {
|
||||
function changeToFeature({ target }: { target: HTMLElement | undefined | null }) {
|
||||
target = target?.closest('.feature, .feature-tab')
|
||||
if (!target) return
|
||||
|
||||
|
@ -212,9 +198,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
descriptionEl.textContent = descriptions[index]
|
||||
}
|
||||
|
||||
const videos = document.querySelectorAll(
|
||||
'.feature-video',
|
||||
) as NodeListOf<HTMLVideoElement>
|
||||
const videos = document.querySelectorAll('.feature-video') as NodeListOf<HTMLVideoElement>
|
||||
videos.forEach((vid, i) => {
|
||||
const yOffset = (i - index) * 20
|
||||
const zOffset = i === index ? 0 : -100 - Math.abs(i - index) * 50
|
||||
|
@ -240,9 +224,11 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
}
|
||||
|
||||
for (const feature of features) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
feature.addEventListener('click', changeToFeature as any)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
changeToFeature({ target: features[0] as any })
|
||||
</script>
|
||||
|
||||
|
@ -266,7 +252,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
}
|
||||
|
||||
.feature-description {
|
||||
@apply px-4 text-sm text-gray-600 dark:text-gray-300;
|
||||
@apply px-4 text-sm;
|
||||
}
|
||||
|
||||
.video-stack {
|
||||
|
|
|
@ -19,11 +19,9 @@ const {
|
|||
role="contentinfo"
|
||||
aria-label="Site footer"
|
||||
>
|
||||
<div
|
||||
class="container flex w-full flex-col items-start justify-between gap-12"
|
||||
>
|
||||
<div class="container flex w-full flex-col items-start justify-between gap-12">
|
||||
<section
|
||||
class="w-full text-center lg:w-1/2 lg:text-left"
|
||||
class="flex w-full flex-col gap-4 text-center lg:w-1/2 lg:text-left"
|
||||
aria-labelledby="footer-title"
|
||||
>
|
||||
<Description id="footer-title" class="text-6xl font-bold !text-paper"
|
||||
|
@ -48,12 +46,8 @@ const {
|
|||
class="grid w-full max-w-5xl place-items-center gap-12 text-center sm:text-left"
|
||||
aria-label="Footer navigation and links"
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3 lg:w-full"
|
||||
>
|
||||
<div
|
||||
class="grid gap-8 sm:col-span-2 sm:grid-cols-2 md:col-span-1 md:grid-cols-1"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3 lg:w-full">
|
||||
<div class="grid gap-8 sm:col-span-2 sm:grid-cols-2 md:col-span-1 md:grid-cols-1">
|
||||
<section
|
||||
class="flex flex-col items-center gap-2 sm:items-start"
|
||||
aria-labelledby="follow-us-heading"
|
||||
|
@ -63,10 +57,7 @@ const {
|
|||
</h2>
|
||||
<SocialMediaStrip />
|
||||
</section>
|
||||
<section
|
||||
class="flex flex-col gap-2"
|
||||
aria-labelledby="about-us-heading"
|
||||
>
|
||||
<section class="flex flex-col gap-2" aria-labelledby="about-us-heading">
|
||||
<h2 id="about-us-heading" class="text-base !font-semibold">
|
||||
{footer.aboutUs}
|
||||
</h2>
|
||||
|
@ -92,23 +83,17 @@ const {
|
|||
</h2>
|
||||
<ul class="grid gap-2 opacity-80">
|
||||
<li>
|
||||
<a href="https://docs.zen-browser.app/" class="font-normal"
|
||||
>{footer.documentation}</a
|
||||
<a href="https://docs.zen-browser.app/" class="font-normal">{footer.documentation}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getLocalePath('/mods')} class="font-normal">{footer.zenMods}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getLocalePath('/release-notes')} class="font-normal">{footer.releaseNotes}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getLocalePath('/mods')} class="font-normal"
|
||||
>{footer.zenMods}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getLocalePath('/release-notes')} class="font-normal"
|
||||
>{footer.releaseNotes}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getLocalePath('/download?twilight')} class="font-normal"
|
||||
>{footer.twilight}</a
|
||||
<a href={getLocalePath('/download?twilight')} class="font-normal">{footer.twilight}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -119,19 +104,15 @@ const {
|
|||
</h2>
|
||||
<ul class="grid gap-2 opacity-80">
|
||||
<li>
|
||||
<a href="https://discord.gg/zen-browser" class="font-normal"
|
||||
>{footer.discord}</a
|
||||
<a href="https://discord.gg/zen-browser" class="font-normal">{footer.discord}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://uptime.zen-browser.app/" class="font-normal">{footer.uptimeStatus}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://uptime.zen-browser.app/" class="font-normal"
|
||||
>{footer.uptimeStatus}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/zen-browser/desktop/issues/new/choose"
|
||||
class="font-normal">{footer.reportAnIssue}</a
|
||||
<a href="https://github.com/zen-browser/desktop/issues/new/choose" class="font-normal"
|
||||
>{footer.reportAnIssue}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -148,11 +129,7 @@ const {
|
|||
/>
|
||||
</section>
|
||||
<section class="absolute bottom-0 right-0">
|
||||
<Circles
|
||||
white
|
||||
multiplier={0.7}
|
||||
class="mb-[-100px] ml-auto mr-[-80px] hidden lg:block"
|
||||
/>
|
||||
<Circles white multiplier={0.7} class="mb-[-100px] ml-auto mr-[-80px] hidden lg:block" />
|
||||
</section>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
@ -36,52 +36,45 @@ const {
|
|||
class="flex w-full flex-col items-center gap-[20%] py-32 text-center lg:gap-[25%]"
|
||||
>
|
||||
<div class="flex h-full flex-col items-center justify-center">
|
||||
<Title
|
||||
class="relative px-12 text-center font-normal leading-8 md:text-7xl lg:px-0 lg:text-9xl"
|
||||
>
|
||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||
{hero.title[0]}
|
||||
</motion.span>
|
||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||
{hero.title[1]}
|
||||
</motion.span>
|
||||
<br class="hidden md:block" />
|
||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||
{hero.title[2]}
|
||||
</motion.span>
|
||||
<motion.span
|
||||
client:load
|
||||
{...getHeroTitleAnimation()}
|
||||
className="italic text-coral"
|
||||
>
|
||||
{hero.title[3]}
|
||||
</motion.span>
|
||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||
{hero.title[4]}
|
||||
</motion.span>
|
||||
<Title class="relative px-12 text-center font-normal md:text-7xl lg:px-0 lg:text-9xl">
|
||||
{
|
||||
hero.title.map(title =>
|
||||
title.text !== '\n' ? (
|
||||
<motion.span
|
||||
client:load
|
||||
{...getHeroTitleAnimation()}
|
||||
className={title.highlight ? 'italic text-coral' : ''}
|
||||
>
|
||||
{title.text}
|
||||
</motion.span>
|
||||
) : (
|
||||
<br class="hidden md:block" />
|
||||
)
|
||||
)
|
||||
}
|
||||
</Title>
|
||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||
<Description class="px-12 text-center lg:px-0">
|
||||
{hero.description[0]}.
|
||||
{hero.description[0]}
|
||||
<br class="hidden sm:inline" />
|
||||
{hero.description[1]}</Description
|
||||
>
|
||||
</motion.span>
|
||||
<div class="mt-6 flex w-2/3 flex-col gap-3 sm:gap-6 md:w-fit md:flex-row">
|
||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||
<Button class="w-full" href={getLocalePath("/download")} isPrimary>
|
||||
<Button class="w-full" href={getLocalePath('/download')} isPrimary>
|
||||
{hero.buttons.beta}
|
||||
<ArrowRightIcon class="size-4" />
|
||||
</Button>
|
||||
</motion.span>
|
||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||
<Button href={getLocalePath("/donate")}>{hero.buttons.support}</Button>
|
||||
<Button href={getLocalePath('/donate')}>{hero.buttons.support}</Button>
|
||||
</motion.span>
|
||||
</div>
|
||||
<motion.span
|
||||
client:load
|
||||
{...getHeroTitleAnimation()}
|
||||
class="mx-auto translate-y-16 !transform"
|
||||
className="mx-auto translate-y-16 !transform"
|
||||
>
|
||||
<SocialMediaStrip />
|
||||
</motion.span>
|
||||
|
|
|
@ -11,12 +11,7 @@ const {
|
|||
---
|
||||
|
||||
<!-- Hidden checkbox for menu toggle -->
|
||||
<input
|
||||
type="checkbox"
|
||||
id="mobile-menu-toggle"
|
||||
class="peer sr-only lg:hidden"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input type="checkbox" id="mobile-menu-toggle" class="peer sr-only lg:hidden" aria-hidden="true" />
|
||||
|
||||
<!-- Mobile Slide Menu -->
|
||||
<div
|
||||
|
@ -25,17 +20,16 @@ const {
|
|||
>
|
||||
<div class="flex items-center justify-between border-b border-dark px-4 py-2">
|
||||
<div class="text-lg font-bold">{menu.menu}</div>
|
||||
<label
|
||||
for="mobile-menu-toggle"
|
||||
class="cursor-pointer p-2 text-dark"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label for="mobile-menu-toggle" class="cursor-pointer p-2 text-dark">
|
||||
<span class="sr-only">Close menu</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
|
@ -52,21 +46,18 @@ const {
|
|||
<div class="mb-2 font-bold">{menu.gettingStarted}</div>
|
||||
<ul class="ml-4 space-y-2">
|
||||
<li>
|
||||
<a
|
||||
href={getLocalePath('/mods')}
|
||||
class="block text-dark hover:text-coral">{menu.zenMods}</a
|
||||
<a href={getLocalePath('/mods')} class="block text-dark hover:text-coral"
|
||||
>{menu.zenMods}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getLocalePath('/release-notes')}
|
||||
class="block text-dark hover:text-coral">{menu.releaseNotes}</a
|
||||
<a href={getLocalePath('/release-notes')} class="block text-dark hover:text-coral"
|
||||
>{menu.releaseNotes}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://discord.gg/zen-browser"
|
||||
class="block text-dark hover:text-coral">{menu.discord}</a
|
||||
<a href="https://discord.gg/zen-browser" class="block text-dark hover:text-coral"
|
||||
>{menu.discord}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -76,21 +67,18 @@ const {
|
|||
<div class="mb-2 font-bold">{menu.usefulLinks}</div>
|
||||
<ul class="ml-4 space-y-2">
|
||||
<li>
|
||||
<a
|
||||
href={getLocalePath('/donate')}
|
||||
class="block text-dark hover:text-coral">{menu.donate}</a
|
||||
<a href={getLocalePath('/donate')} class="block text-dark hover:text-coral"
|
||||
>{menu.donate}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getLocalePath('/about')}
|
||||
class="block text-dark hover:text-coral">{menu.aboutUs}</a
|
||||
<a href={getLocalePath('/about')} class="block text-dark hover:text-coral"
|
||||
>{menu.aboutUs}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://docs.zen-browser.app"
|
||||
class="block text-dark hover:text-coral">{menu.documentation}</a
|
||||
<a href="https://docs.zen-browser.app" class="block text-dark hover:text-coral"
|
||||
>{menu.documentation}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -104,15 +92,13 @@ const {
|
|||
</li>
|
||||
<!-- Extra Links -->
|
||||
<li>
|
||||
<a
|
||||
href={getLocalePath('/mods')}
|
||||
class="block font-bold text-dark hover:text-coral">{menu.mods}</a
|
||||
<a href={getLocalePath('/mods')} class="block font-bold text-dark hover:text-coral"
|
||||
>{menu.mods}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getLocalePath('/download')}
|
||||
class="block font-bold text-dark hover:text-coral">{menu.download}</a
|
||||
<a href={getLocalePath('/download')} class="block font-bold text-dark hover:text-coral"
|
||||
>{menu.download}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -120,6 +106,7 @@ const {
|
|||
</div>
|
||||
|
||||
<!-- Overlay for Mobile Menu -->
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label
|
||||
for="mobile-menu-toggle"
|
||||
class="pointer-events-none fixed inset-0 z-30 bg-black opacity-0 transition-opacity duration-300 peer-checked:pointer-events-auto peer-checked:opacity-50"
|
||||
|
|
679
src/components/ModsList.astro
Normal file
679
src/components/ModsList.astro
Normal file
|
@ -0,0 +1,679 @@
|
|||
---
|
||||
import { icon, library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import { type ZenTheme } from '~/mods'
|
||||
import { getPath, type Locale } from '~/utils/i18n'
|
||||
|
||||
// Add icons to the library
|
||||
library.add(faSort, faSortUp, faSortDown)
|
||||
|
||||
// Create icon objects
|
||||
const defaultSortIcon = icon({ prefix: 'fas', iconName: 'sort' })
|
||||
const ascSortIcon = icon({ prefix: 'fas', iconName: 'sort-up' })
|
||||
const descSortIcon = icon({ prefix: 'fas', iconName: 'sort-down' })
|
||||
|
||||
interface Props {
|
||||
allMods: ZenTheme[]
|
||||
locale: Locale
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
translations: any
|
||||
}
|
||||
|
||||
const { allMods, locale, translations } = Astro.props
|
||||
|
||||
const getLocalePath = getPath(locale)
|
||||
|
||||
// Server-side rendering setup
|
||||
const defaultLimit = 12
|
||||
const initialMods = allMods.slice(0, defaultLimit)
|
||||
const totalPages = Math.ceil(allMods.length / defaultLimit)
|
||||
---
|
||||
|
||||
<div id="mods-list-container" class="flex flex-1 flex-col">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="flex w-full flex-col items-center justify-center gap-6">
|
||||
<input
|
||||
class="w-full rounded-full border-2 border-dark bg-transparent px-6 py-2 text-lg outline-none"
|
||||
id="search"
|
||||
placeholder={translations.search}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid w-full grid-cols-2 place-items-center gap-4 sm:grid-cols-3">
|
||||
<div class="flex flex-col items-start gap-2">
|
||||
<button
|
||||
class="text-md flex items-center gap-2 px-4 py-2 font-semibold"
|
||||
id="created-sort"
|
||||
type="button"
|
||||
>
|
||||
{translations.sort.lastCreated}
|
||||
<span class="relative">
|
||||
<span id="created-sort-default" class="" set:html={defaultSortIcon.html[0]} />
|
||||
<span id="created-sort-asc" class="hidden" set:html={ascSortIcon.html[0]} />
|
||||
<span id="created-sort-desc" class="hidden" set:html={descSortIcon.html[0]} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<button
|
||||
class="text-md flex items-center gap-2 px-4 py-2 font-semibold"
|
||||
id="updated-sort"
|
||||
type="button"
|
||||
>
|
||||
{translations.sort.lastUpdated}
|
||||
<span class="relative">
|
||||
<span id="updated-sort-default" class="" set:html={defaultSortIcon.html[0]} />
|
||||
<span id="updated-sort-asc" class="hidden" set:html={ascSortIcon.html[0]} />
|
||||
<span id="updated-sort-desc" class="hidden" set:html={descSortIcon.html[0]} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 flex items-center gap-2 px-4 py-2 sm:col-span-1">
|
||||
<label class="text-md font-semibold" for="limit">
|
||||
{translations.sort.perPage}
|
||||
</label>
|
||||
<select class="rounded border border-dark px-2 py-1 text-sm dark:bg-paper" id="limit">
|
||||
<option value="12">12</option>
|
||||
<option value="24">24</option>
|
||||
<option value="48">48</option>
|
||||
<option value="96">96</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="mods-grid"
|
||||
class="grid w-full grid-cols-1 place-items-start gap-12 py-6 md:grid-cols-2 xl:grid-cols-3"
|
||||
>
|
||||
{
|
||||
initialMods.map(mod => (
|
||||
<a
|
||||
class="mod-card flex w-full flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
|
||||
href={getLocalePath(`/mods/${mod.id}`)}
|
||||
>
|
||||
<div class="relative mb-0 block aspect-[1.85/1] h-48 overflow-hidden rounded-md border-2 border-dark object-cover shadow-md">
|
||||
<img
|
||||
alt={mod.name}
|
||||
class="h-full w-full object-cover transition-transform duration-100 hover:scale-105"
|
||||
loading="lazy"
|
||||
src={mod.image}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-bold">
|
||||
{mod.name}{' '}
|
||||
<span class="ml-1 text-sm font-normal">
|
||||
{translations.by} @{mod.author}
|
||||
</span>
|
||||
</h2>
|
||||
<p class="line-clamp-2 text-sm font-thin">{mod.description}</p>
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
id="no-results"
|
||||
class="hidden h-full min-h-96 flex-col justify-center gap-4 place-self-center text-center"
|
||||
>
|
||||
<h2 class="text-lg font-bold">{translations.noResults}</h2>
|
||||
<p class="text-sm font-thin">{translations.noResultsDescription}</p>
|
||||
</div>
|
||||
<div id="pagination" class="mx-auto mb-12 hidden items-center justify-center gap-4 px-8">
|
||||
{
|
||||
totalPages > 1 && (
|
||||
<>
|
||||
<span class="pointer-events-none px-3 py-2 text-gray-400"><</span>
|
||||
<form class="flex items-center gap-2" id="page-form">
|
||||
<input
|
||||
id="page-input"
|
||||
aria-label="Page number"
|
||||
class="w-16 rounded border border-dark bg-transparent px-2 py-1 text-center text-sm"
|
||||
type="text"
|
||||
value="1"
|
||||
/>
|
||||
<span class="text-sm">
|
||||
{translations.pagination.pagination
|
||||
.replace('{totalPages}', totalPages.toString())
|
||||
.replace('{totalItems}', allMods.length.toString())
|
||||
.replace('{input}', '')}
|
||||
</span>
|
||||
</form>
|
||||
<a
|
||||
class="px-3 py-2 text-dark hover:text-gray-600"
|
||||
href={getLocalePath('/mods?page=2')}
|
||||
data-page="2"
|
||||
>
|
||||
>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script define:vars={{ allMods, translations, locale }} is:inline>
|
||||
class ModsSearch {
|
||||
constructor() {
|
||||
this.allMods = allMods
|
||||
this.translations = translations
|
||||
this.locale = locale // State
|
||||
this.state = {
|
||||
search: '',
|
||||
createdSort: 'default',
|
||||
updatedSort: 'default',
|
||||
page: 1,
|
||||
limit: 12,
|
||||
}
|
||||
|
||||
// Track if content has been dynamically modified
|
||||
this.hasBeenModified = false
|
||||
|
||||
// Performance optimizations
|
||||
this.searchTimeout = null
|
||||
this.isRendering = false
|
||||
this.lastFilteredMods = null
|
||||
this.lastRenderState = null
|
||||
|
||||
// Cache DOM elements for better performance and intellisense
|
||||
this.elements = this.cacheElements()
|
||||
|
||||
this.initializeFromURL()
|
||||
this.bindEvents()
|
||||
this.renderMods()
|
||||
}
|
||||
|
||||
cacheElements() {
|
||||
const searchInput = document.getElementById('search')
|
||||
const limitSelect = document.getElementById('limit')
|
||||
const createdSortButton = document.getElementById('created-sort')
|
||||
const updatedSortButton = document.getElementById('updated-sort')
|
||||
const modsGrid = document.getElementById('mods-grid')
|
||||
const noResults = document.getElementById('no-results')
|
||||
const pagination = document.getElementById('pagination')
|
||||
|
||||
// Sort icon elements
|
||||
const createdSortDefault = document.getElementById('created-sort-default')
|
||||
const createdSortAsc = document.getElementById('created-sort-asc')
|
||||
const createdSortDesc = document.getElementById('created-sort-desc')
|
||||
const updatedSortDefault = document.getElementById('updated-sort-default')
|
||||
const updatedSortAsc = document.getElementById('updated-sort-asc')
|
||||
const updatedSortDesc = document.getElementById('updated-sort-desc')
|
||||
|
||||
return {
|
||||
searchInput: searchInput instanceof HTMLInputElement ? searchInput : null,
|
||||
limitSelect: limitSelect instanceof HTMLSelectElement ? limitSelect : null,
|
||||
createdSortButton:
|
||||
createdSortButton instanceof HTMLButtonElement ? createdSortButton : null,
|
||||
updatedSortButton:
|
||||
updatedSortButton instanceof HTMLButtonElement ? updatedSortButton : null,
|
||||
modsGrid: modsGrid instanceof HTMLDivElement ? modsGrid : null,
|
||||
noResults: noResults instanceof HTMLDivElement ? noResults : null,
|
||||
pagination: pagination instanceof HTMLDivElement ? pagination : null,
|
||||
sortIcons: {
|
||||
createdDefault: createdSortDefault instanceof HTMLSpanElement ? createdSortDefault : null,
|
||||
createdAsc: createdSortAsc instanceof HTMLSpanElement ? createdSortAsc : null,
|
||||
createdDesc: createdSortDesc instanceof HTMLSpanElement ? createdSortDesc : null,
|
||||
updatedDefault: updatedSortDefault instanceof HTMLSpanElement ? updatedSortDefault : null,
|
||||
updatedAsc: updatedSortAsc instanceof HTMLSpanElement ? updatedSortAsc : null,
|
||||
updatedDesc: updatedSortDesc instanceof HTMLSpanElement ? updatedSortDesc : null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
getLocalePath(path) {
|
||||
if (this.locale && this.locale !== 'en' && !path.startsWith(`/${this.locale}`)) {
|
||||
return `/${this.locale}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
}
|
||||
return path
|
||||
}
|
||||
validateSortOrder(value, defaultValue = 'default') {
|
||||
const validSortOrders = ['default', 'asc', 'desc']
|
||||
return validSortOrders.includes(value) ? value : defaultValue
|
||||
}
|
||||
|
||||
validateLimit(value, defaultValue = 12) {
|
||||
const validLimits = [12, 24, 48, 96]
|
||||
const parsed = parseInt(value, 10)
|
||||
return validLimits.includes(parsed) ? parsed : defaultValue
|
||||
}
|
||||
|
||||
validatePage(value, defaultValue = 1) {
|
||||
const parsed = parseInt(value, 10)
|
||||
return !isNaN(parsed) && parsed >= 1 ? parsed : defaultValue
|
||||
}
|
||||
initializeFromURL() {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
||||
const rawCreatedSort = params.get('created')
|
||||
const rawUpdatedSort = params.get('updated')
|
||||
const rawPage = params.get('page')
|
||||
const rawLimit = params.get('limit')
|
||||
|
||||
this.state = {
|
||||
search: params.get('q') || '',
|
||||
createdSort: this.validateSortOrder(rawCreatedSort, 'default'),
|
||||
updatedSort: this.validateSortOrder(rawUpdatedSort, 'default'),
|
||||
page: this.validatePage(rawPage, 1),
|
||||
limit: this.validateLimit(rawLimit, 12),
|
||||
}
|
||||
|
||||
// Set form values using cached elements
|
||||
if (this.elements.searchInput) {
|
||||
this.elements.searchInput.value = this.state.search
|
||||
}
|
||||
if (this.elements.limitSelect) {
|
||||
this.elements.limitSelect.value = this.state.limit.toString()
|
||||
}
|
||||
}
|
||||
bindEvents() {
|
||||
// Search input with debouncing
|
||||
if (this.elements.searchInput) {
|
||||
this.elements.searchInput.addEventListener('input', e => {
|
||||
// Clear existing timeout
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout)
|
||||
}
|
||||
|
||||
// Set new timeout for debounced search
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
this.setState({ search: e.target.value, page: 1 })
|
||||
}
|
||||
}, 300) // 300ms debounce
|
||||
})
|
||||
}
|
||||
|
||||
// Sort buttons
|
||||
if (this.elements.createdSortButton) {
|
||||
this.elements.createdSortButton.addEventListener('click', () => {
|
||||
this.toggleCreatedSort()
|
||||
})
|
||||
}
|
||||
|
||||
if (this.elements.updatedSortButton) {
|
||||
this.elements.updatedSortButton.addEventListener('click', () => {
|
||||
this.toggleUpdatedSort()
|
||||
})
|
||||
}
|
||||
|
||||
// Limit select
|
||||
if (this.elements.limitSelect) {
|
||||
this.elements.limitSelect.addEventListener('change', e => {
|
||||
if (e.target instanceof HTMLSelectElement) {
|
||||
const newLimit = this.validateLimit(e.target.value, this.state.limit)
|
||||
this.setState({ limit: newLimit, page: 1 })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
setState(newState) {
|
||||
// Prevent multiple renders
|
||||
if (this.isRendering) return
|
||||
|
||||
// Validate new state values before setting
|
||||
const validatedState = { ...newState }
|
||||
|
||||
if ('createdSort' in validatedState) {
|
||||
validatedState.createdSort = this.validateSortOrder(
|
||||
validatedState.createdSort,
|
||||
this.state.createdSort
|
||||
)
|
||||
}
|
||||
if ('updatedSort' in validatedState) {
|
||||
validatedState.updatedSort = this.validateSortOrder(
|
||||
validatedState.updatedSort,
|
||||
this.state.updatedSort
|
||||
)
|
||||
}
|
||||
|
||||
if ('page' in validatedState) {
|
||||
validatedState.page = this.validatePage(validatedState.page, this.state.page)
|
||||
}
|
||||
|
||||
if ('limit' in validatedState) {
|
||||
validatedState.limit = this.validateLimit(validatedState.limit, this.state.limit)
|
||||
}
|
||||
|
||||
// Determine if this is a search/filter operation (should replace URL)
|
||||
// vs pagination navigation (should push to history)
|
||||
const isSearchOrFilter =
|
||||
'search' in validatedState ||
|
||||
'createdSort' in validatedState ||
|
||||
'updatedSort' in validatedState ||
|
||||
'limit' in validatedState ||
|
||||
('page' in validatedState && validatedState.page === 1)
|
||||
|
||||
this.state = { ...this.state, ...validatedState }
|
||||
this.updateURL(isSearchOrFilter)
|
||||
this.updateSortIcons()
|
||||
this.renderMods()
|
||||
}
|
||||
toggleCreatedSort() {
|
||||
const newSort =
|
||||
this.state.createdSort === 'default'
|
||||
? 'asc'
|
||||
: this.state.createdSort === 'asc'
|
||||
? 'desc'
|
||||
: 'default'
|
||||
this.setState({ createdSort: newSort, updatedSort: 'default', page: 1 })
|
||||
}
|
||||
|
||||
toggleUpdatedSort() {
|
||||
const newSort =
|
||||
this.state.updatedSort === 'default'
|
||||
? 'asc'
|
||||
: this.state.updatedSort === 'asc'
|
||||
? 'desc'
|
||||
: 'default'
|
||||
this.setState({ updatedSort: newSort, createdSort: 'default', page: 1 })
|
||||
}
|
||||
updateSortIcons() {
|
||||
// Update created sort icons using cached elements
|
||||
if (this.elements.sortIcons.createdDefault) {
|
||||
this.elements.sortIcons.createdDefault.classList.toggle(
|
||||
'hidden',
|
||||
this.state.createdSort !== 'default'
|
||||
)
|
||||
}
|
||||
if (this.elements.sortIcons.createdAsc) {
|
||||
this.elements.sortIcons.createdAsc.classList.toggle(
|
||||
'hidden',
|
||||
this.state.createdSort !== 'asc'
|
||||
)
|
||||
}
|
||||
if (this.elements.sortIcons.createdDesc) {
|
||||
this.elements.sortIcons.createdDesc.classList.toggle(
|
||||
'hidden',
|
||||
this.state.createdSort !== 'desc'
|
||||
)
|
||||
}
|
||||
|
||||
// Update updated sort icons using cached elements
|
||||
if (this.elements.sortIcons.updatedDefault) {
|
||||
this.elements.sortIcons.updatedDefault.classList.toggle(
|
||||
'hidden',
|
||||
this.state.updatedSort !== 'default'
|
||||
)
|
||||
}
|
||||
if (this.elements.sortIcons.updatedAsc) {
|
||||
this.elements.sortIcons.updatedAsc.classList.toggle(
|
||||
'hidden',
|
||||
this.state.updatedSort !== 'asc'
|
||||
)
|
||||
}
|
||||
if (this.elements.sortIcons.updatedDesc) {
|
||||
this.elements.sortIcons.updatedDesc.classList.toggle(
|
||||
'hidden',
|
||||
this.state.updatedSort !== 'desc'
|
||||
)
|
||||
}
|
||||
}
|
||||
updateURL(isSearchOrFilter = false) {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (this.state.search) params.set('q', this.state.search)
|
||||
if (this.state.createdSort !== 'default') params.set('created', this.state.createdSort)
|
||||
if (this.state.updatedSort !== 'default') params.set('updated', this.state.updatedSort)
|
||||
if (this.state.page > 1) params.set('page', this.state.page.toString())
|
||||
if (this.state.limit !== 12) params.set('limit', this.state.limit.toString())
|
||||
|
||||
const newUrl = `${window.location.pathname}${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
// Only push to history for pagination navigation, replace for search/filtering
|
||||
if (!isSearchOrFilter && this.state.page > 1) {
|
||||
window.history.pushState({}, '', newUrl)
|
||||
} else {
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
}
|
||||
}
|
||||
|
||||
getFilteredMods() {
|
||||
let filtered = [...this.allMods]
|
||||
|
||||
// Filter by search
|
||||
const searchTerm = this.state.search.toLowerCase()
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(
|
||||
mod =>
|
||||
mod.name.toLowerCase().includes(searchTerm) ||
|
||||
mod.description.toLowerCase().includes(searchTerm) ||
|
||||
mod.author.toLowerCase().includes(searchTerm) ||
|
||||
(mod.tags?.some(tag => tag.toLowerCase().includes(searchTerm)) ?? false)
|
||||
)
|
||||
}
|
||||
|
||||
// Sort by createdAt if chosen
|
||||
if (this.state.createdSort !== 'default') {
|
||||
filtered.sort((a, b) => {
|
||||
const diff = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
return this.state.createdSort === 'asc' ? diff : -diff
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by updatedAt if chosen
|
||||
if (this.state.updatedSort !== 'default') {
|
||||
filtered.sort((a, b) => {
|
||||
const diff = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
||||
return this.state.updatedSort === 'asc' ? diff : -diff
|
||||
})
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
renderMods() {
|
||||
// Prevent concurrent renders
|
||||
if (this.isRendering) return
|
||||
this.isRendering = true
|
||||
|
||||
// Use requestAnimationFrame for smoother rendering
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
const filteredMods = this.getFilteredMods()
|
||||
const totalPages = Math.ceil(filteredMods.length / this.state.limit)
|
||||
const startIndex = (this.state.page - 1) * this.state.limit
|
||||
const endIndex = startIndex + this.state.limit
|
||||
const paginatedMods = filteredMods.slice(startIndex, endIndex)
|
||||
|
||||
if (paginatedMods.length > 0) {
|
||||
if (this.elements.noResults) {
|
||||
this.elements.noResults.classList.add('hidden')
|
||||
}
|
||||
if (this.elements.modsGrid) {
|
||||
this.elements.modsGrid.classList.remove('hidden')
|
||||
}
|
||||
|
||||
// Check if we're in the default state
|
||||
const isDefaultState =
|
||||
this.state.search === '' &&
|
||||
this.state.createdSort === 'default' &&
|
||||
this.state.updatedSort === 'default' &&
|
||||
this.state.page === 1 &&
|
||||
this.state.limit === 12
|
||||
|
||||
// Re-render if: not in default state, OR we've been modified and need to restore default
|
||||
if (!isDefaultState || this.hasBeenModified) {
|
||||
// Create document fragment for better performance
|
||||
const fragment = document.createDocumentFragment()
|
||||
const tempDiv = document.createElement('div')
|
||||
|
||||
tempDiv.innerHTML = paginatedMods
|
||||
.map(
|
||||
mod => `
|
||||
<a
|
||||
class="mod-card flex w-full flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
|
||||
href="${this.getLocalePath(`/mods/${mod.id}${this.buildSearchParams()}`)}"
|
||||
>
|
||||
<div class="relative mb-0 block aspect-[1.85/1] h-48 overflow-hidden rounded-md border-2 border-dark object-cover shadow-md">
|
||||
<img
|
||||
alt="${mod.name}"
|
||||
class="h-full w-full object-cover transition-transform duration-100 hover:scale-105"
|
||||
loading="lazy"
|
||||
src="${mod.image}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-bold">
|
||||
${mod.name} <span class="ml-1 text-sm font-normal">${this.translations.by} @${mod.author}</span>
|
||||
</h2>
|
||||
<p class="text-sm font-thin line-clamp-2">${mod.description}</p>
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
)
|
||||
.join('')
|
||||
|
||||
// Move all children to fragment
|
||||
while (tempDiv.firstChild) {
|
||||
fragment.appendChild(tempDiv.firstChild)
|
||||
}
|
||||
|
||||
// Clear and append fragment (single reflow)
|
||||
if (this.elements.modsGrid) {
|
||||
this.elements.modsGrid.innerHTML = ''
|
||||
this.elements.modsGrid.appendChild(fragment)
|
||||
}
|
||||
|
||||
// Track that we've modified the content
|
||||
this.hasBeenModified = !isDefaultState
|
||||
}
|
||||
} else {
|
||||
if (this.elements.modsGrid) {
|
||||
this.elements.modsGrid.classList.add('hidden')
|
||||
}
|
||||
if (this.elements.noResults) {
|
||||
this.elements.noResults.classList.replace('hidden', 'flex')
|
||||
}
|
||||
this.hasBeenModified = true
|
||||
}
|
||||
|
||||
this.renderPagination(totalPages, filteredMods.length)
|
||||
} finally {
|
||||
this.isRendering = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
buildSearchParams() {
|
||||
const params = new URLSearchParams()
|
||||
if (this.state.search) params.set('q', this.state.search)
|
||||
if (this.state.createdSort !== 'default') params.set('created', this.state.createdSort)
|
||||
if (this.state.updatedSort !== 'default') params.set('updated', this.state.updatedSort)
|
||||
if (this.state.page > 1) params.set('page', this.state.page.toString())
|
||||
if (this.state.limit !== 12) params.set('limit', this.state.limit.toString())
|
||||
return params.toString() ? `?${params.toString()}` : ''
|
||||
}
|
||||
|
||||
buildPaginationUrl(targetPage) {
|
||||
const params = new URLSearchParams(this.buildSearchParams())
|
||||
if (targetPage > 1) {
|
||||
params.set('page', targetPage.toString())
|
||||
} else {
|
||||
params.delete('page')
|
||||
}
|
||||
return `/mods?${params.toString()}`
|
||||
}
|
||||
navigatePage(pageNum) {
|
||||
// Validate page number
|
||||
const validatedPage = this.validatePage(pageNum, this.state.page)
|
||||
|
||||
// Update state without going through setState to handle URL properly
|
||||
this.state = { ...this.state, page: validatedPage }
|
||||
|
||||
// For pagination navigation, push to history if page > 1, otherwise replace
|
||||
this.updateURL(false) // false = this is navigation, not search/filter
|
||||
this.updateSortIcons()
|
||||
this.renderMods()
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
renderPagination(totalPages, totalItems) {
|
||||
const paginationElement = this.elements.pagination
|
||||
|
||||
if (!paginationElement) return
|
||||
|
||||
if (totalPages <= 1) {
|
||||
paginationElement.innerHTML = ''
|
||||
paginationElement.classList.add('hidden')
|
||||
return
|
||||
}
|
||||
|
||||
const prevButton =
|
||||
this.state.page > 1
|
||||
? `<a
|
||||
class="px-3 py-2 text-dark hover:text-gray-600"
|
||||
href="${this.getLocalePath(this.buildPaginationUrl(this.state.page - 1))}"
|
||||
data-page="${this.state.page - 1}"
|
||||
>
|
||||
<
|
||||
</a>`
|
||||
: `<span class="pointer-events-none px-3 py-2 text-gray-400"><</span>`
|
||||
|
||||
const nextButton =
|
||||
this.state.page < totalPages
|
||||
? `<a
|
||||
class="px-3 py-2 text-dark hover:text-gray-600"
|
||||
href="${this.getLocalePath(this.buildPaginationUrl(this.state.page + 1))}"
|
||||
data-page="${this.state.page + 1}"
|
||||
>
|
||||
>
|
||||
</a>`
|
||||
: `<span class="pointer-events-none px-3 py-2 text-gray-400">></span>`
|
||||
|
||||
const paginationText = this.translations.pagination.pagination
|
||||
.replace('{totalPages}', totalPages.toString())
|
||||
.replace('{totalItems}', totalItems.toString())
|
||||
|
||||
paginationElement.innerHTML = `
|
||||
${prevButton}
|
||||
<form class="flex items-center gap-2" id="page-form">
|
||||
<input
|
||||
id="page-input"
|
||||
aria-label="Page number"
|
||||
class="w-16 rounded border border-dark bg-transparent px-2 py-1 text-center text-sm"
|
||||
type="text"
|
||||
value="${this.state.page}"
|
||||
/>
|
||||
<span class="text-sm">${paginationText.replace('{input}', '')}</span>
|
||||
</form>
|
||||
${nextButton}
|
||||
`
|
||||
|
||||
// Bind pagination events
|
||||
paginationElement.classList.replace('hidden', 'flex')
|
||||
|
||||
const pageForm = document.getElementById('page-form')
|
||||
const pageInput = document.getElementById('page-input')
|
||||
|
||||
if (pageForm && pageInput instanceof HTMLInputElement) {
|
||||
pageForm.addEventListener('submit', e => {
|
||||
e.preventDefault()
|
||||
const inputValue = pageInput.value
|
||||
const newPage = this.validatePage(inputValue, this.state.page)
|
||||
|
||||
// Additional validation for page range
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
this.navigatePage(newPage)
|
||||
} else {
|
||||
// Reset to current page if out of range
|
||||
pageInput.value = this.state.page.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Bind navigation links
|
||||
paginationElement.addEventListener('click', e => {
|
||||
if (e.target instanceof HTMLAnchorElement && e.target.dataset.page) {
|
||||
e.preventDefault()
|
||||
this.navigatePage(parseInt(e.target.dataset.page, 10))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the mods search
|
||||
new ModsSearch()
|
||||
</script>
|
|
@ -1,230 +0,0 @@
|
|||
import { icon, library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faSort, faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { useModsSearch } from '~/hooks/useModsSearch'
|
||||
import type { ZenTheme } from '~/mods'
|
||||
import { type Locale, getUI } from '~/utils/i18n'
|
||||
|
||||
// Add icons to the library
|
||||
library.add(faSort, faSortUp, faSortDown)
|
||||
|
||||
// Create icon objects
|
||||
const defaultSortIcon = icon({ prefix: 'fas', iconName: 'sort' })
|
||||
const ascSortIcon = icon({ prefix: 'fas', iconName: 'sort-up' })
|
||||
const descSortIcon = icon({ prefix: 'fas', iconName: 'sort-down' })
|
||||
|
||||
interface ModsListProps {
|
||||
allMods: ZenTheme[]
|
||||
locale: Locale
|
||||
}
|
||||
|
||||
export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||
const {
|
||||
search,
|
||||
createdSort,
|
||||
updatedSort,
|
||||
page,
|
||||
limit,
|
||||
totalPages,
|
||||
totalItems,
|
||||
setSearch,
|
||||
toggleCreatedSort,
|
||||
toggleUpdatedSort,
|
||||
setPage,
|
||||
setLimit,
|
||||
mods: paginatedMods,
|
||||
// searchParams,
|
||||
} = useModsSearch(allMods)
|
||||
|
||||
const [pageInput, setPageInput] = useState(page.toString())
|
||||
|
||||
// Keep page input in sync with actual page
|
||||
useEffect(() => {
|
||||
setPageInput(page.toString())
|
||||
}, [page])
|
||||
|
||||
function getSortIcon(state: 'default' | 'asc' | 'desc') {
|
||||
if (state === 'asc') return ascSortIcon
|
||||
if (state === 'desc') return descSortIcon
|
||||
return defaultSortIcon
|
||||
}
|
||||
|
||||
function handleSearch(e: Event) {
|
||||
const target = e.target as HTMLInputElement
|
||||
setSearch(target.value)
|
||||
}
|
||||
|
||||
function handleLimitChange(e: Event) {
|
||||
const target = e.target as HTMLSelectElement
|
||||
setLimit(Number.parseInt(target.value, 10))
|
||||
}
|
||||
|
||||
function handlePageSubmit(e: Event) {
|
||||
e.preventDefault()
|
||||
const newPage = Number.parseInt(pageInput, 10)
|
||||
if (!Number.isNaN(newPage) && newPage >= 1 && newPage <= totalPages) {
|
||||
setPage(newPage)
|
||||
window.scrollTo(0, 0)
|
||||
} else {
|
||||
setPageInput(page.toString())
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageInputChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement
|
||||
setPageInput(target.value)
|
||||
}
|
||||
|
||||
function navigatePage(pageNum: number) {
|
||||
setPage(pageNum)
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
const {
|
||||
routes: { mods },
|
||||
} = getUI(locale)
|
||||
|
||||
function renderPagination() {
|
||||
if (totalPages <= 1) return null
|
||||
return (
|
||||
<div className="mx-auto mb-12 flex items-center justify-center gap-4 px-8">
|
||||
<button
|
||||
className={`px-3 py-2 ${page === 1 ? 'pointer-events-none text-gray-400' : 'text-dark hover:text-gray-600'}`}
|
||||
onClick={() => navigatePage(page - 1)}
|
||||
type="button"
|
||||
>
|
||||
<
|
||||
</button>
|
||||
<form className="flex items-center gap-2" onSubmit={handlePageSubmit}>
|
||||
{mods.pagination.pagination.split('{input}').map((value, index) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<input
|
||||
aria-label="Page number"
|
||||
className="w-16 rounded border border-dark bg-transparent px-2 py-1 text-center text-sm"
|
||||
onInput={handlePageInputChange}
|
||||
type="text"
|
||||
value={pageInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span className="text-sm" key={value}>
|
||||
{value.replace('{totalPages}', totalPages.toString()).replace('{totalItems}', totalItems.toString())}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</form>
|
||||
<button
|
||||
className={`px-3 py-2 ${page === totalPages ? 'pointer-events-none text-gray-400' : 'text-dark hover:text-gray-600'}`}
|
||||
onClick={() => navigatePage(page + 1)}
|
||||
type="button"
|
||||
>
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex w-full flex-col items-center justify-center gap-6">
|
||||
<input
|
||||
className="w-full rounded-full border-2 border-dark bg-transparent px-6 py-2 text-lg outline-none"
|
||||
id="search"
|
||||
onInput={handleSearch}
|
||||
placeholder={mods.search}
|
||||
type="text"
|
||||
value={search}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full grid-cols-2 place-items-center gap-4 sm:grid-cols-3">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-2 font-semibold text-md"
|
||||
onClick={toggleCreatedSort}
|
||||
type="button"
|
||||
>
|
||||
{mods.sort.lastCreated}
|
||||
<span
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: Icons are safe
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getSortIcon(createdSort).html[0],
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-2 font-semibold text-md"
|
||||
onClick={toggleUpdatedSort}
|
||||
type="button"
|
||||
>
|
||||
{mods.sort.lastUpdated}
|
||||
<span
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: Icons are safe
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getSortIcon(updatedSort).html[0],
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 px-4 py-2">
|
||||
<label className="font-semibold text-md" htmlFor="limit">
|
||||
{mods.sort.perPage}
|
||||
</label>
|
||||
<select
|
||||
className="rounded border border-dark bg-transparent px-2 py-1 text-sm [&>option]:text-black"
|
||||
id="limit"
|
||||
onInput={handleLimitChange}
|
||||
value={limit}
|
||||
>
|
||||
<option value="12">12</option>
|
||||
<option value="24">24</option>
|
||||
<option value="48">48</option>
|
||||
<option value="96">96</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full grid-cols-1 place-items-start gap-12 py-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{paginatedMods.length > 0 ? (
|
||||
paginatedMods.map((mod) => (
|
||||
<a
|
||||
className="mod-card flex w-full flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
|
||||
href={`/mods/${mod.id}`}
|
||||
key={mod.id}
|
||||
>
|
||||
<div className="relative mb-0 block aspect-[1.85/1] h-48 overflow-hidden rounded-md border-2 border-dark object-cover shadow-md">
|
||||
<img
|
||||
alt={mod.name}
|
||||
className="h-full w-full object-cover transition-transform duration-100 hover:scale-105"
|
||||
loading="lazy"
|
||||
src={mod.image}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-bold text-lg">
|
||||
{mod.name} <span className="ml-1 font-normal text-sm">by @{mod.author}</span>
|
||||
</h2>
|
||||
<p className="font-thin text-sm">{mod.description}</p>
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-4 grid place-items-center gap-4 place-self-center px-8 text-center">
|
||||
<h2 className="font-bold text-lg">{mods.noResults}</h2>
|
||||
<p className="font-thin text-sm">{mods.noResultsDescription}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderPagination()}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
---
|
||||
import { Astronav, Dropdown, DropdownItems, MenuItems } from 'astro-navbar'
|
||||
import { motion } from 'motion/react'
|
||||
import Button from '~/components/Button.astro'
|
||||
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
|
||||
import ChevronDownIcon from '~/icons/ChevronDownIcon.astro'
|
||||
import DownloadIcon from '~/icons/DownloadIcon.astro'
|
||||
import MenuIcon from '~/icons/MenuIcon.astro'
|
||||
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
||||
import { getTitleAnimation } from '../animations.ts'
|
||||
import Logo from './Logo.astro'
|
||||
import MobileMenu from './MobileMenu.astro'
|
||||
import ThemeSwitch from './ThemeSwitch.astro'
|
||||
|
@ -26,10 +24,7 @@ const {
|
|||
<MenuItems
|
||||
class="container relative z-20 grid w-full grid-cols-2 items-center gap-2 bg-paper py-3 lg:grid lg:grid-cols-[auto_1fr_auto] lg:py-6"
|
||||
>
|
||||
<a
|
||||
class="flex items-center gap-2 text-lg font-bold"
|
||||
href={getLocalePath('/')}
|
||||
>
|
||||
<a class="flex items-center gap-2 text-lg font-bold" href={getLocalePath('/')}>
|
||||
<Logo class="text-coral" />
|
||||
<span>{brand}</span>
|
||||
</a>
|
||||
|
@ -44,15 +39,8 @@ const {
|
|||
/>
|
||||
</button>
|
||||
<DropdownItems>
|
||||
<motion.div
|
||||
className="navbar-dropdown"
|
||||
{...getTitleAnimation(0, 0.3, false)}
|
||||
client:load
|
||||
>
|
||||
<a
|
||||
class="dropdown-item bg-dark/5 row-span-2"
|
||||
href={getLocalePath('/mods')}
|
||||
>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="dropdown-item bg-dark/5 row-span-2" href={getLocalePath('/mods')}>
|
||||
<div class="dropdown-title">{menu.zenMods}</div>
|
||||
<div class="dropdown-description">
|
||||
{menu.zenModsDesc}
|
||||
|
@ -74,7 +62,7 @@ const {
|
|||
{menu.discordDesc}
|
||||
</div>
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</DropdownItems>
|
||||
</Dropdown>
|
||||
<Dropdown class="group">
|
||||
|
@ -85,11 +73,7 @@ const {
|
|||
/>
|
||||
</button>
|
||||
<DropdownItems>
|
||||
<motion.div
|
||||
className="navbar-dropdown !grid-cols-1 gap-1"
|
||||
{...getTitleAnimation(0, 0.3, false)}
|
||||
client:load
|
||||
>
|
||||
<div class="navbar-dropdown !grid-cols-1 gap-1">
|
||||
<a class="dropdown-item" href={getLocalePath('/donate')}>
|
||||
<div class="dropdown-title">{menu.donate}</div>
|
||||
<div class="dropdown-description">
|
||||
|
@ -108,17 +92,13 @@ const {
|
|||
{menu.documentationDesc}
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="https://github.com/zen-browser"
|
||||
target="_blank"
|
||||
>
|
||||
<a class="dropdown-item" href="https://github.com/zen-browser" target="_blank">
|
||||
<div class="dropdown-title">{menu.github}</div>
|
||||
<div class="dropdown-description">
|
||||
{menu.githubDesc}
|
||||
</div>
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</DropdownItems>
|
||||
</Dropdown>
|
||||
<a class="hidden items-center lg:block" href={getLocalePath('/mods')}>
|
||||
|
@ -138,6 +118,7 @@ const {
|
|||
<DownloadIcon class="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label
|
||||
for="mobile-menu-toggle"
|
||||
class="cursor-pointer p-2 text-dark lg:hidden"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
import InfoIcon from '~/icons/InfoIcon.astro'
|
||||
|
||||
import { getIntlLocale } from '~/constants/i18n'
|
||||
import { releaseNotes as releaseNotesData } from '~/release-notes'
|
||||
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
||||
import { type ReleaseNote, getReleaseNoteFirefoxVersion } from '../release-notes'
|
||||
|
@ -25,7 +26,9 @@ if (props.date) {
|
|||
}
|
||||
|
||||
const ffVersion = getReleaseNoteFirefoxVersion(props)
|
||||
const currentReleaseIndex = releaseNotesData.findIndex((releaseNote: ReleaseNote) => releaseNote.version === props.version)
|
||||
const currentReleaseIndex = releaseNotesData.findIndex(
|
||||
(releaseNote: ReleaseNote) => releaseNote.version === props.version
|
||||
)
|
||||
const prevReleaseNote = releaseNotesData[currentReleaseIndex + 1]
|
||||
let compareLink = ''
|
||||
if (prevReleaseNote && !isTwilight) {
|
||||
|
@ -34,13 +37,15 @@ if (prevReleaseNote && !isTwilight) {
|
|||
|
||||
const isLatest = currentReleaseIndex === 0
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const listItems = {} as any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const generateItems = (items: any, type: string) => {
|
||||
if (!items) return
|
||||
if (!listItems[type]) {
|
||||
listItems[type] = []
|
||||
}
|
||||
// biome-ignore lint/complexity/noForEach: We dont need to use a for loop here
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
items.forEach((item: any) => {
|
||||
switch (type) {
|
||||
case 'feature':
|
||||
|
@ -106,7 +111,7 @@ generateItems(props.knownIssues, 'known')
|
|||
class="release-note-item relative mt-12 flex flex-col pt-24 lg:flex-row"
|
||||
id={props.version}
|
||||
>
|
||||
<div class="px-5 md:px-10 md:pr-32 w-full gap-2 flex flex-col">
|
||||
<div class="flex w-full flex-col gap-2 px-5 md:px-10 md:pr-32">
|
||||
{
|
||||
isTwilight ? (
|
||||
<a
|
||||
|
@ -117,75 +122,78 @@ generateItems(props.knownIssues, 'known')
|
|||
</a>
|
||||
) : null
|
||||
}
|
||||
<div class="w-full sm:flex justify-between">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-0 text-sm font-bold opacity-80">
|
||||
{
|
||||
isTwilight ? (
|
||||
<>
|
||||
{releaseNoteItem.twilightChanges} {props.version.replaceAll(
|
||||
'{version}',
|
||||
props.version,
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{releaseNoteItem.releaseChanges.replaceAll(
|
||||
'{version}',
|
||||
props.version,
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
ffVersion ? (
|
||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||
<a rel="noopener noreferrer"class="text-xs underline decoration-wavy text-coral opacity-80" href={`https://www.mozilla.org/en-US/firefox/${ffVersion}/releasenotes/`} target="_blank" rel="noopener noreferrer">
|
||||
{releaseNoteItem.firefoxVersion.replace('{version}', ffVersion)}
|
||||
</a>
|
||||
) : null
|
||||
}
|
||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
class="zen-link whitespace-nowrap !no-underline text-xs opacity-80"
|
||||
target="_blank"
|
||||
href={`https://github.com/zen-browser/desktop/releases/tag/${isTwilight ? 'twilight' : props.version}`}
|
||||
>{releaseNoteItem.githubRelease}</a
|
||||
<div class="w-full justify-between sm:flex">
|
||||
<div
|
||||
class="flex flex-col gap-1 text-sm font-bold opacity-80 sm:flex-row sm:items-center sm:gap-0"
|
||||
>
|
||||
{
|
||||
!isTwilight ? (
|
||||
<>
|
||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
class="zen-link whitespace-nowrap !no-underline text-xs opacity-80"
|
||||
target="_blank"
|
||||
href={`https://github.com/zen-browser/desktop/actions/runs/${props.workflowId}`}
|
||||
>
|
||||
{releaseNoteItem.workflowRun}
|
||||
</a>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
compareLink !== '' ? (
|
||||
<>
|
||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
class="zen-link whitespace-nowrap !no-underline text-xs opacity-80"
|
||||
target="_blank"
|
||||
href={compareLink}
|
||||
>
|
||||
{releaseNoteItem.compareChanges}
|
||||
</a>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
<div class="text-xs opacity-80 font-bold">
|
||||
{date && date.toLocaleDateString('en-US', { dateStyle: 'long' })}
|
||||
</div>
|
||||
{
|
||||
isTwilight ? (
|
||||
<>
|
||||
{releaseNoteItem.twilightChanges}{' '}
|
||||
{props.version.replaceAll('{version}', props.version)}
|
||||
</>
|
||||
) : (
|
||||
<>{releaseNoteItem.releaseChanges.replaceAll('{version}', props.version)}</>
|
||||
)
|
||||
}
|
||||
{
|
||||
ffVersion ? (
|
||||
<>
|
||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
class="text-xs text-coral underline decoration-wavy opacity-80"
|
||||
href={`https://www.mozilla.org/en-US/firefox/${ffVersion}/releasenotes/`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{releaseNoteItem.firefoxVersion.replace('{version}', ffVersion)}
|
||||
</a>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
class="zen-link whitespace-nowrap text-xs !no-underline opacity-80"
|
||||
target="_blank"
|
||||
href={`https://github.com/zen-browser/desktop/releases/tag/${isTwilight ? 'twilight' : props.version}`}
|
||||
>{releaseNoteItem.githubRelease}</a
|
||||
>
|
||||
{
|
||||
!isTwilight ? (
|
||||
<>
|
||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
class="zen-link whitespace-nowrap text-xs !no-underline opacity-80"
|
||||
target="_blank"
|
||||
href={`https://github.com/zen-browser/desktop/actions/runs/${props.workflowId}`}
|
||||
>
|
||||
{releaseNoteItem.workflowRun}
|
||||
</a>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
compareLink !== '' ? (
|
||||
<>
|
||||
<span class="text-muted-foreground mx-3 hidden sm:block">•</span>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
class="zen-link whitespace-nowrap text-xs !no-underline opacity-80"
|
||||
target="_blank"
|
||||
href={compareLink}
|
||||
>
|
||||
{releaseNoteItem.compareChanges}
|
||||
</a>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
<div class="text-xs font-bold opacity-80">
|
||||
{date && date.toLocaleDateString(getIntlLocale(locale), { dateStyle: 'long' })}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
props.extra?.length ? (
|
||||
|
@ -194,35 +202,33 @@ generateItems(props.knownIssues, 'known')
|
|||
</p>
|
||||
) : null
|
||||
}
|
||||
{isTwilight || isLatest ? (
|
||||
|
||||
<div class="text-muted-forground flex text-sm opacity-70">
|
||||
{isTwilight ? <InfoIcon class="mx-4 my-0 size-6 text-yellow-500" /> : null}
|
||||
<p class="m-0">
|
||||
{isTwilight ? <>{releaseNoteItem.twilightWarning}</> : null}
|
||||
<span set:html={releaseNoteItem.reportIssues} />
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div class="gap-8 flex flex-col mt-4">
|
||||
{
|
||||
Object.keys(listItems).map((type) => {
|
||||
const items = listItems[type];
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<ul class="gap-1 flex flex-col">
|
||||
{items.map((item: any) => (
|
||||
<ReleaseNoteListItem
|
||||
type={item.type}
|
||||
content={item.content}
|
||||
link={item.link}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
isTwilight || isLatest ? (
|
||||
<div class="text-muted-forground flex text-sm opacity-70">
|
||||
{isTwilight ? <InfoIcon class="mx-4 my-0 size-6 text-yellow-500" /> : null}
|
||||
<p class="m-0">
|
||||
{isTwilight ? <>{releaseNoteItem.twilightWarning}</> : null}
|
||||
<span set:html={releaseNoteItem.reportIssues} />
|
||||
</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
<div class="mt-4 flex flex-col gap-8">
|
||||
{
|
||||
Object.keys(listItems).map(type => {
|
||||
const items = listItems[type]
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ul class="flex flex-col gap-1">
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
{items.map((item: any) => (
|
||||
<ReleaseNoteListItem type={item.type} content={item.content} link={item.link} />
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<style is:global>
|
||||
.ac-accordion-item-title {
|
||||
|
@ -248,10 +254,7 @@ generateItems(props.knownIssues, 'known')
|
|||
.ac-accordion {
|
||||
&.ac-accordion--light {
|
||||
> * + * {
|
||||
border-color: light-dark(
|
||||
rgba(0, 0, 0, 0.1),
|
||||
rgba(255, 255, 255, 0.1)
|
||||
) !important;
|
||||
border-color: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1)) !important;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
||||
import { getLocale, getUI } from '~/utils/i18n'
|
||||
|
||||
const { type, content, link } = Astro.props as {
|
||||
interface Props {
|
||||
type: 'security' | 'feature' | 'fix' | 'theme' | 'break' | 'known' | 'change'
|
||||
content: string
|
||||
link?: {
|
||||
|
@ -10,6 +10,8 @@ const { type, content, link } = Astro.props as {
|
|||
} | null
|
||||
}
|
||||
|
||||
const { type, content, link } = Astro.props
|
||||
|
||||
const locale = getLocale(Astro)
|
||||
|
||||
const {
|
||||
|
@ -22,30 +24,25 @@ const {
|
|||
<li class="flex gap-2">
|
||||
<div
|
||||
class:list={[
|
||||
(type === 'security' && 'text-[#e3401f]') ||
|
||||
(type === 'feature' && 'text-[#bf3316] dark:text-[#ffb1a1]') ||
|
||||
(type === 'fix' && 'text-[#fe846b]') ||
|
||||
(type === 'theme' && 'text-[#f76f53]') ||
|
||||
(type === 'change' && 'text-[#f7a74b]') ||
|
||||
(type === 'break' && 'text-[#471308] dark:text-[#D02908]') || ''
|
||||
, 'opacity-80 font-bold min-w-16']}
|
||||
>
|
||||
(type === 'security' && 'text-[#e3401f]') ||
|
||||
(type === 'feature' && 'text-[#bf3316] dark:text-[#ffb1a1]') ||
|
||||
(type === 'fix' && 'text-[#fe846b]') ||
|
||||
(type === 'theme' && 'text-[#f76f53]') ||
|
||||
(type === 'break' && 'text-[#471308] dark:text-[#D02908]') ||
|
||||
'',
|
||||
'min-w-16 font-bold opacity-80',
|
||||
]}
|
||||
>
|
||||
{itemType[type]}
|
||||
</div>
|
||||
<div>
|
||||
{content && (
|
||||
<span
|
||||
class="text-base opacity-80"
|
||||
set:html={content}
|
||||
/>
|
||||
)}
|
||||
{link && (
|
||||
<a
|
||||
href={link.href}
|
||||
class="text-base text-blue inline-block underline"
|
||||
>
|
||||
{link.text}
|
||||
</a>
|
||||
)}
|
||||
{content && <span class="text-base opacity-80" set:html={content} />}
|
||||
{
|
||||
link && (
|
||||
<a href={link.href} class="text-blue inline-block text-base underline">
|
||||
{link.text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -2,7 +2,13 @@
|
|||
const { gap = 4 } = Astro.props
|
||||
|
||||
import { icon, library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faBluesky, faGithub, faMastodon, faReddit, faXTwitter } from '@fortawesome/free-brands-svg-icons'
|
||||
import {
|
||||
faBluesky,
|
||||
faGithub,
|
||||
faMastodon,
|
||||
faReddit,
|
||||
faXTwitter,
|
||||
} from '@fortawesome/free-brands-svg-icons'
|
||||
|
||||
library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit)
|
||||
const Mastodon = icon({ prefix: 'fab', iconName: 'mastodon' })
|
||||
|
|
|
@ -21,7 +21,7 @@ const {
|
|||
<section id="sponsors" class:list={['py-12', !showSponsors && 'hidden']}>
|
||||
<div class="mx-auto flex flex-col text-center">
|
||||
<motion.span client:load {...getTitleAnimation(0.2)}>
|
||||
<Description class="mb-2 text-6xl font-bold">Our Sponsors</Description>
|
||||
<Description class="mb-2 text-4xl font-bold sm:text-6xl">{sponsors.title}</Description>
|
||||
</motion.span>
|
||||
<motion.span client:load {...getTitleAnimation(0.4)}>
|
||||
<Description set:html={sponsors.description} />
|
||||
|
|
|
@ -9,10 +9,7 @@ const { label, className = '' } = Astro.props
|
|||
|
||||
<button
|
||||
type="button"
|
||||
class:list={[
|
||||
'inline-flex h-8 w-8 cursor-pointer items-center justify-center',
|
||||
className,
|
||||
]}
|
||||
class:list={['inline-flex h-8 w-8 cursor-pointer items-center justify-center', className]}
|
||||
id="theme-switcher"
|
||||
aria-label={label || 'Toggle theme'}
|
||||
>
|
||||
|
@ -29,9 +26,7 @@ const { label, className = '' } = Astro.props
|
|||
</button>
|
||||
|
||||
<script>
|
||||
const themeSwitch = document.getElementById(
|
||||
'theme-switcher',
|
||||
) as HTMLButtonElement
|
||||
const themeSwitch = document.getElementById('theme-switcher') as HTMLButtonElement
|
||||
|
||||
const resolveTheme = () => {
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||
|
|
|
@ -4,10 +4,7 @@ const { class: className } = Astro.props
|
|||
---
|
||||
|
||||
<h1
|
||||
class={cn(
|
||||
"text-dark leading-[0.9] mb-[0.4rem] font-junicode font-semibold text-5xl",
|
||||
className,
|
||||
)}
|
||||
class={cn('mb-[0.4rem] font-junicode text-5xl font-semibold leading-[0.9] text-dark', className)}
|
||||
>
|
||||
<slot />
|
||||
</h1>
|
||||
|
|
|
@ -3,19 +3,13 @@ const { src, class: className, ...rest } = Astro.props
|
|||
const type = src.split('.').pop() || 'webm'
|
||||
---
|
||||
|
||||
<video
|
||||
class:list={['w-fit', className]}
|
||||
data-src={src}
|
||||
preload="none"
|
||||
{...rest}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video class:list={['w-fit', className]} data-src={src} preload="none" {...rest}>
|
||||
<source src="" type={`video/${type}`} />
|
||||
</video>
|
||||
|
||||
<script>
|
||||
const videos = document.querySelectorAll(
|
||||
'video[data-src]',
|
||||
) as NodeListOf<HTMLVideoElement>
|
||||
const videos = document.querySelectorAll('video[data-src]') as NodeListOf<HTMLVideoElement>
|
||||
|
||||
const loadVideo = (video: HTMLVideoElement) => {
|
||||
const source = video.querySelector('source')
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
---
|
||||
import { getLocale, getUI } from '~/utils/i18n'
|
||||
|
||||
const locale = getLocale(Astro)
|
||||
const {
|
||||
routes: {
|
||||
download: { buttonCard },
|
||||
},
|
||||
} = getUI(locale)
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
href: string
|
||||
|
@ -42,15 +51,15 @@ const { label, href, checksum } = Astro.props
|
|||
</svg>
|
||||
</button>
|
||||
<span class="absolute -top-10 left-1/2 z-50 hidden min-w-[120px] -translate-x-1/2 select-none 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:hidden group-hover/checksum:flex group-focus-within/checksum:group-hover/checksum:hidden dark:bg-[rgba(24,24,27,0.98)] dark:text-gray-100">
|
||||
Show SHA-256
|
||||
{buttonCard.showChecksum}
|
||||
</span>
|
||||
<span class="checksum-tooltip popover absolute -left-14 -top-12 z-50 hidden min-w-[220px] items-center gap-2 whitespace-nowrap rounded-md border border-subtle bg-[rgba(255,255,255,0.98)] px-3 py-2 text-xs text-gray-700 opacity-100 shadow transition-opacity duration-150 group-focus-within/checksum:flex dark:bg-[rgba(24,24,27,0.98)] dark:text-gray-100">
|
||||
<span class="flex-1 truncate font-mono text-xs">{checksum}</span>
|
||||
<span class="font-mono flex-1 truncate text-xs">{checksum}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="copy-btn rounded bg-coral px-2 py-1 text-xs text-white hover:bg-coral/80 data-[twilight='true']:bg-zen-blue data-[twilight='true']:hover:bg-zen-blue/80"
|
||||
>
|
||||
Copy
|
||||
{buttonCard.copy}
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
|
@ -62,7 +71,7 @@ const { label, href, checksum } = Astro.props
|
|||
<span
|
||||
class="release-type-tag rounded-full bg-coral/10 px-2 py-1 text-xs font-medium text-coral transition-colors duration-200 group-hover:bg-coral/20 data-[twilight='true']:bg-zen-blue/10 data-[twilight='true']:text-zen-blue data-[twilight='true']:group-hover:bg-zen-blue/20"
|
||||
>
|
||||
Beta
|
||||
{buttonCard.beta}
|
||||
</span>
|
||||
<div
|
||||
class="download-arrow-icon text-muted-foreground rounded-xl border border-subtle p-2 transition-colors duration-200 group-hover:border-coral group-hover:text-coral data-[twilight='true']:group-hover:border-zen-blue data-[twilight='true']:group-hover:text-zen-blue"
|
||||
|
@ -89,14 +98,12 @@ const { label, href, checksum } = Astro.props
|
|||
|
||||
<script>
|
||||
const checksumButtons = document.querySelectorAll(
|
||||
'.checksum-icon-btn',
|
||||
'.checksum-icon-btn'
|
||||
) as NodeListOf<HTMLButtonElement>
|
||||
const checksumTooltips = document.querySelectorAll(
|
||||
'.checksum-tooltip',
|
||||
'.checksum-tooltip'
|
||||
) as NodeListOf<HTMLDivElement>
|
||||
const copyButtons = document.querySelectorAll(
|
||||
'.copy-btn',
|
||||
) as NodeListOf<HTMLButtonElement>
|
||||
const copyButtons = document.querySelectorAll('.copy-btn') as NodeListOf<HTMLButtonElement>
|
||||
|
||||
function stopEvent(e: Event) {
|
||||
e.preventDefault?.()
|
||||
|
@ -113,23 +120,20 @@ const { label, href, checksum } = Astro.props
|
|||
setTimeout(() => (btn.innerText = original), 1200)
|
||||
}
|
||||
|
||||
checksumButtons.forEach((btn) => {
|
||||
checksumButtons.forEach(btn => {
|
||||
btn.addEventListener('click', stopEvent)
|
||||
})
|
||||
checksumTooltips.forEach((tooltip) => {
|
||||
checksumTooltips.forEach(tooltip => {
|
||||
tooltip.addEventListener('mousedown', stopEvent)
|
||||
tooltip.addEventListener('click', stopEvent)
|
||||
})
|
||||
copyButtons.forEach((btn) => {
|
||||
btn.addEventListener('click', (e) =>
|
||||
copyButtons.forEach(btn => {
|
||||
btn.addEventListener('click', e =>
|
||||
copyChecksum(
|
||||
e,
|
||||
(
|
||||
btn
|
||||
.closest('.checksum-tooltip')
|
||||
?.querySelector('.font-mono') as HTMLSpanElement
|
||||
)?.innerText,
|
||||
),
|
||||
(btn.closest('.checksum-tooltip')?.querySelector('.font-mono') as HTMLSpanElement)
|
||||
?.innerText
|
||||
)
|
||||
)
|
||||
btn.addEventListener('mousedown', stopEvent)
|
||||
})
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
|
||||
// Apply twilight mode to all relevant elements
|
||||
const coralElements = document.querySelectorAll(
|
||||
'.download-browser-logo, .release-type-tag, .decorative-gradient, .download-link, .download-arrow-icon, .download-card__icon, .checksum-icon-btn, .copy-btn, .flathub-download',
|
||||
'.download-browser-logo, .release-type-tag, .decorative-gradient, .download-link, .download-arrow-icon, .download-card__icon, .checksum-icon-btn, .copy-btn, .flathub-download'
|
||||
)
|
||||
for (const element of coralElements) {
|
||||
element.setAttribute('data-twilight', 'true')
|
||||
|
@ -88,10 +88,7 @@
|
|||
if (!link.id.includes('beta')) {
|
||||
const href = link.getAttribute('href')
|
||||
if (href && href.includes('/latest/download/')) {
|
||||
const twilightHref = href.replace(
|
||||
'/latest/download/',
|
||||
'/download/twilight/',
|
||||
)
|
||||
const twilightHref = href.replace('/latest/download/', '/download/twilight/')
|
||||
link.setAttribute('href', twilightHref)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
|||
|
||||
<div
|
||||
id={`${platform}-downloads`}
|
||||
data-active={platform === "mac"}
|
||||
data-active={platform === 'mac'}
|
||||
class="platform-section data-[active='false']:hidden"
|
||||
>
|
||||
<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} />
|
||||
<div class="space-y-6">
|
||||
{
|
||||
platform === "linux" ? (
|
||||
platform === 'linux' ? (
|
||||
<>
|
||||
{releases.flathub && releases.flathub.all.label && (
|
||||
<article class="flathub-download data-[twilight='true']:hidden">
|
||||
|
@ -71,24 +71,16 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
|||
</article>
|
||||
)}
|
||||
{releases.x86_64 &&
|
||||
typeof releases.x86_64 === "object" &&
|
||||
"tarball" in releases.x86_64 &&
|
||||
(releases.x86_64.tarball) && (
|
||||
typeof releases.x86_64 === 'object' &&
|
||||
'tarball' in releases.x86_64 &&
|
||||
releases.x86_64.tarball && (
|
||||
<article>
|
||||
<h4 class="mb-3 text-lg font-medium">x86_64</h4>
|
||||
<div class="">
|
||||
{releases.x86_64.tarball && (
|
||||
<DownloadCard
|
||||
label={
|
||||
releases.x86_64.tarball.label
|
||||
? releases.x86_64.tarball.label
|
||||
: ""
|
||||
}
|
||||
href={
|
||||
releases.x86_64.tarball.link
|
||||
? releases.x86_64.tarball.link
|
||||
: ""
|
||||
}
|
||||
label={releases.x86_64.tarball.label ? releases.x86_64.tarball.label : ''}
|
||||
href={releases.x86_64.tarball.link ? releases.x86_64.tarball.link : ''}
|
||||
variant="x86_64"
|
||||
checksum={releases.x86_64.tarball.checksum}
|
||||
/>
|
||||
|
@ -97,24 +89,18 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
|||
</article>
|
||||
)}
|
||||
{releases.aarch64 &&
|
||||
typeof releases.aarch64 === "object" &&
|
||||
"tarball" in releases.aarch64 &&
|
||||
(releases.aarch64.tarball) && (
|
||||
typeof releases.aarch64 === 'object' &&
|
||||
'tarball' in releases.aarch64 &&
|
||||
releases.aarch64.tarball && (
|
||||
<article>
|
||||
<h4 class="mb-3 text-lg font-medium">ARM64</h4>
|
||||
<div class="gap-3">
|
||||
{releases.aarch64.tarball && (
|
||||
<DownloadCard
|
||||
label={
|
||||
releases.aarch64.tarball.label
|
||||
? releases.aarch64.tarball.label
|
||||
: ""
|
||||
}
|
||||
href={
|
||||
releases.aarch64.tarball.link
|
||||
? releases.aarch64.tarball.link
|
||||
: ""
|
||||
releases.aarch64.tarball.label ? releases.aarch64.tarball.label : ''
|
||||
}
|
||||
href={releases.aarch64.tarball.link ? releases.aarch64.tarball.link : ''}
|
||||
variant="aarch64"
|
||||
checksum={releases.aarch64.tarball.checksum}
|
||||
/>
|
||||
|
@ -133,15 +119,13 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
|||
checksum={releases.universal.checksum}
|
||||
/>
|
||||
)}
|
||||
{releases.x86_64 &&
|
||||
isFlatReleaseInfo(releases.x86_64) &&
|
||||
releases.x86_64.label && (
|
||||
<DownloadCard
|
||||
label={releases.x86_64.label}
|
||||
href={releases.x86_64.link}
|
||||
checksum={releases.x86_64.checksum}
|
||||
/>
|
||||
)}
|
||||
{releases.x86_64 && isFlatReleaseInfo(releases.x86_64) && releases.x86_64.label && (
|
||||
<DownloadCard
|
||||
label={releases.x86_64.label}
|
||||
href={releases.x86_64.link}
|
||||
checksum={releases.x86_64.checksum}
|
||||
/>
|
||||
)}
|
||||
{releases.arm64 && releases.arm64.label && (
|
||||
<DownloadCard
|
||||
label={releases.arm64.label}
|
||||
|
@ -158,11 +142,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
|||
<div
|
||||
class="download-browser-logo flex justify-center text-coral transition-colors data-[twilight='true']:text-zen-blue md:w-1/3"
|
||||
>
|
||||
<Image
|
||||
src={AppIconDark}
|
||||
alt="Zen Browser"
|
||||
class="w-32 translate-y-6 transform dark:hidden"
|
||||
/>
|
||||
<Image src={AppIconDark} alt="Zen Browser" class="w-32 translate-y-6 transform dark:hidden" />
|
||||
<Image
|
||||
src={AppIconLight}
|
||||
alt="Zen Browser"
|
||||
|
|
|
@ -1,49 +1,61 @@
|
|||
import { getUI } from '~/utils/i18n'
|
||||
|
||||
/**
|
||||
* Returns the releases object, injecting checksums dynamically.
|
||||
* @param locale The locale to use for labels
|
||||
* @param checksums Record<string, string> mapping filenames to SHA-256 hashes
|
||||
*/
|
||||
export function getReleasesWithChecksums(checksums: Record<string, string>) {
|
||||
return {
|
||||
macos: {
|
||||
universal: {
|
||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg',
|
||||
label: 'Universal',
|
||||
checksum: checksums['zen.macos-universal.dmg'],
|
||||
export function getReleasesWithChecksums(locale: string) {
|
||||
const {
|
||||
routes: {
|
||||
download: {
|
||||
links: { macos, windows, linux },
|
||||
},
|
||||
},
|
||||
windows: {
|
||||
x86_64: {
|
||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe',
|
||||
label: '64-bit (Recommended)',
|
||||
checksum: checksums['zen.installer.exe'],
|
||||
},
|
||||
arm64: {
|
||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe',
|
||||
label: 'ARM64',
|
||||
checksum: checksums['zen.installer-arm64.exe'],
|
||||
},
|
||||
},
|
||||
linux: {
|
||||
x86_64: {
|
||||
tarball: {
|
||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
|
||||
label: 'Tarball',
|
||||
checksum: checksums['zen.linux-x86_64.tar.xz'],
|
||||
} = getUI(locale)
|
||||
return (checksums: Record<string, string>) => {
|
||||
return {
|
||||
macos: {
|
||||
universal: {
|
||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg',
|
||||
label: macos.universal,
|
||||
checksum: checksums['zen.macos-universal.dmg'],
|
||||
},
|
||||
},
|
||||
aarch64: {
|
||||
tarball: {
|
||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
|
||||
label: 'Tarball',
|
||||
checksum: checksums['zen.linux-aarch64.tar.xz'],
|
||||
windows: {
|
||||
x86_64: {
|
||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe',
|
||||
label: windows['64bit'],
|
||||
checksum: checksums['zen.installer.exe'],
|
||||
},
|
||||
arm64: {
|
||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe',
|
||||
label: windows.ARM64,
|
||||
checksum: checksums['zen.installer-arm64.exe'],
|
||||
},
|
||||
},
|
||||
flathub: {
|
||||
all: {
|
||||
link: 'https://flathub.org/apps/app.zen_browser.zen',
|
||||
label: 'Flathub',
|
||||
linux: {
|
||||
x86_64: {
|
||||
tarball: {
|
||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
|
||||
label: linux.x86_64,
|
||||
checksum: checksums['zen.linux-x86_64.tar.xz'],
|
||||
},
|
||||
},
|
||||
aarch64: {
|
||||
tarball: {
|
||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
|
||||
label: linux.aarch64,
|
||||
checksum: checksums['zen.linux-aarch64.tar.xz'],
|
||||
},
|
||||
},
|
||||
flathub: {
|
||||
all: {
|
||||
link: 'https://flathub.org/apps/app.zen_browser.zen',
|
||||
label: linux.flathub,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,19 @@
|
|||
export const I18N = {
|
||||
const UI_EN = (await import('~/i18n/en/translation.json', { with: { type: 'json' } })).default
|
||||
const UI_JA = (await import('~/i18n/ja/translation.json', { with: { type: 'json' } })).default
|
||||
|
||||
export const i18n = {
|
||||
DEFAULT_LOCALE: 'en',
|
||||
LOCALES: [{ label: 'English', value: 'en' }],
|
||||
} as const
|
||||
LOCALES: [
|
||||
{ label: 'English', value: 'en', ui: UI_EN, intl: 'en-US' },
|
||||
{ label: '日本語', value: 'ja', ui: UI_JA, intl: 'ja-JP' },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Type definition for UI translations based on the English translation
|
||||
*/
|
||||
export type UIProps = typeof UI_EN | typeof UI_JA
|
||||
|
||||
export const getIntlLocale = (locale: string) => {
|
||||
return i18n.LOCALES.find(l => l.value === locale)?.intl
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CHECKSUMS } from './checksum'
|
||||
import { I18N } from './i18n'
|
||||
import { i18n as I18N } from './i18n'
|
||||
|
||||
export const CONSTANT = {
|
||||
I18N,
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
import { useEffect, useState } from 'preact/hooks'
|
||||
import type { ZenTheme } from '../mods'
|
||||
|
||||
type SortOrder = 'default' | 'asc' | 'desc'
|
||||
|
||||
interface ModsSearchState {
|
||||
search: string
|
||||
createdSort: SortOrder
|
||||
updatedSort: SortOrder
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
const DEFAULT_LIMIT = 12
|
||||
|
||||
export function useModsSearch(mods: ZenTheme[]) {
|
||||
const [searchParams, setSearchParams] = useState<URLSearchParams>()
|
||||
const [state, setState] = useState<ModsSearchState>({
|
||||
search: '',
|
||||
createdSort: 'desc',
|
||||
updatedSort: 'default',
|
||||
page: 1,
|
||||
limit: DEFAULT_LIMIT,
|
||||
})
|
||||
|
||||
// Initialize search params
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
setSearchParams(params)
|
||||
setState({
|
||||
search: params.get('q') || '',
|
||||
createdSort: (params.get('created') as SortOrder) || 'desc',
|
||||
updatedSort: (params.get('updated') as SortOrder) || 'default',
|
||||
page: Number.parseInt(params.get('page') || '1', 10),
|
||||
limit: Number.parseInt(params.get('limit') || String(DEFAULT_LIMIT), 10),
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Update URL when state changes
|
||||
useEffect(() => {
|
||||
if (!searchParams) return
|
||||
|
||||
if (state.search) {
|
||||
searchParams.set('q', state.search)
|
||||
} else {
|
||||
searchParams.delete('q')
|
||||
}
|
||||
|
||||
if (state.createdSort !== 'default') {
|
||||
searchParams.set('created', state.createdSort)
|
||||
} else {
|
||||
searchParams.delete('created')
|
||||
}
|
||||
|
||||
if (state.updatedSort !== 'default') {
|
||||
searchParams.set('updated', state.updatedSort)
|
||||
} else {
|
||||
searchParams.delete('updated')
|
||||
}
|
||||
|
||||
if (state.page > 1) {
|
||||
searchParams.set('page', state.page.toString())
|
||||
} else {
|
||||
searchParams.delete('page')
|
||||
}
|
||||
|
||||
if (state.limit !== DEFAULT_LIMIT) {
|
||||
searchParams.set('limit', state.limit.toString())
|
||||
} else {
|
||||
searchParams.delete('limit')
|
||||
}
|
||||
|
||||
const newUrl = `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`
|
||||
|
||||
if (state.page > 1) {
|
||||
window.history.pushState({}, '', newUrl)
|
||||
} else {
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
}
|
||||
}, [state, searchParams])
|
||||
|
||||
const filteredMods = (() => {
|
||||
let filtered = [...mods]
|
||||
|
||||
// Filter by search
|
||||
const searchTerm = state.search.toLowerCase()
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
mod.name.toLowerCase().includes(searchTerm) ||
|
||||
mod.description.toLowerCase().includes(searchTerm) ||
|
||||
mod.author.toLowerCase().includes(searchTerm) ||
|
||||
(mod.tags?.some((tag) => tag.toLowerCase().includes(searchTerm)) ?? false),
|
||||
)
|
||||
}
|
||||
|
||||
// Sort by createdAt if chosen
|
||||
if (state.createdSort !== 'default') {
|
||||
filtered.sort((a, b) => {
|
||||
const diff = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
return state.createdSort === 'asc' ? diff : -diff
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by updatedAt if chosen
|
||||
if (state.updatedSort !== 'default') {
|
||||
filtered.sort((a, b) => {
|
||||
const diff = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
||||
return state.updatedSort === 'asc' ? diff : -diff
|
||||
})
|
||||
}
|
||||
|
||||
return filtered
|
||||
})()
|
||||
|
||||
// Calculate pagination
|
||||
const totalPages = Math.ceil(filteredMods.length / state.limit)
|
||||
const startIndex = (state.page - 1) * state.limit
|
||||
const endIndex = startIndex + state.limit
|
||||
const paginatedMods = filteredMods.slice(startIndex, endIndex)
|
||||
|
||||
const setSearch = (search: string) => {
|
||||
setState((prev) => ({ ...prev, search, page: 1 })) // Reset page when search changes
|
||||
}
|
||||
|
||||
const toggleCreatedSort = () => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
createdSort: prev.createdSort === 'default' ? 'asc' : prev.createdSort === 'asc' ? 'desc' : 'default',
|
||||
page: 1, // Reset page when sort changes
|
||||
}))
|
||||
}
|
||||
|
||||
const toggleUpdatedSort = () => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
updatedSort: prev.updatedSort === 'default' ? 'asc' : prev.updatedSort === 'asc' ? 'desc' : 'default',
|
||||
page: 1, // Reset page when sort changes
|
||||
}))
|
||||
}
|
||||
|
||||
const setPage = (page: number) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
page: Math.max(1, Math.min(page, totalPages)),
|
||||
}))
|
||||
}
|
||||
|
||||
const setLimit = (limit: number) => {
|
||||
setState((prev) => ({ ...prev, limit, page: 1 })) // Reset page when limit changes
|
||||
}
|
||||
|
||||
return {
|
||||
search: state.search,
|
||||
createdSort: state.createdSort,
|
||||
updatedSort: state.updatedSort,
|
||||
page: state.page,
|
||||
limit: state.limit,
|
||||
totalPages,
|
||||
totalItems: filteredMods.length,
|
||||
setSearch,
|
||||
toggleCreatedSort,
|
||||
toggleUpdatedSort,
|
||||
setPage,
|
||||
setLimit,
|
||||
mods: paginatedMods,
|
||||
searchParams,
|
||||
}
|
||||
}
|
|
@ -3,7 +3,14 @@
|
|||
"index": {
|
||||
"title": "Zen Browser",
|
||||
"hero": {
|
||||
"title": ["welcome", "to", "a", "calmer", "internet"],
|
||||
"title": [
|
||||
{ "text": "welcome ", "highlight": false },
|
||||
{ "text": "to ", "highlight": false },
|
||||
{ "text": "\n", "highlight": false },
|
||||
{ "text": "a ", "highlight": false },
|
||||
{ "text": "calmer ", "highlight": true },
|
||||
{ "text": "internet", "highlight": false }
|
||||
],
|
||||
"description": [
|
||||
"Beautifully designed, privacy-focused, and packed with features.",
|
||||
"We care about your experience, not your data."
|
||||
|
@ -14,10 +21,8 @@
|
|||
}
|
||||
},
|
||||
"features": {
|
||||
"title1": "Productivity",
|
||||
"title2": "at",
|
||||
"title3": "its best",
|
||||
"description": "Zen Browser is packed with features that help you stay productive and focused. Browsers should be tools that help you get things done, not distractions that keep you from your work.",
|
||||
"titles": ["Productivity ", "at ", "its best"],
|
||||
"description": "Zen is packed with features that help you stay productive and focused. Browsers should be tools that help you get things done, not distractions that keep you from your work.",
|
||||
"featureTabs": {
|
||||
"workspaces": {
|
||||
"title": "Workspaces",
|
||||
|
@ -48,7 +53,7 @@
|
|||
}
|
||||
},
|
||||
"community": {
|
||||
"title": ["Our", "Core", "Values"],
|
||||
"title": ["Our ", "Core ", "Values"],
|
||||
"description": "We make it not only a priority, but a necessity to ensure that Zen always strikes the right balance between beauty, performance, and privacy. We are committed to making Zen the most beautiful, productive, and privacy-respecting browser out there — without compromising on your experience.",
|
||||
"lists": {
|
||||
"freeAndOpenSource": {
|
||||
|
@ -78,6 +83,7 @@
|
|||
"pagination": "{input} of {totalPages} ({totalItems} items)"
|
||||
},
|
||||
"search": "Type to search...",
|
||||
"by": "by",
|
||||
"sort": {
|
||||
"lastCreated": "Last created",
|
||||
"lastUpdated": "Last updated",
|
||||
|
@ -92,9 +98,9 @@
|
|||
"description": "You need to have Zen Browser installed to install this theme.",
|
||||
"button": "Download now!"
|
||||
},
|
||||
"createdBy": "Created by <a href={link} class=\"zen-link font-bold\">{author}</a> • <span class=\"font-bold\">v{version}</span>",
|
||||
"creationDate": "Creation date • <b>{createdAt}</b>",
|
||||
"latestUpdate": "Latest update • <b>{updatedAt}</b>",
|
||||
"createdBy": "Created by {author} • <b>v{version}</b>",
|
||||
"creationDate": "Creation date: <b>{createdAt}</b>",
|
||||
"latestUpdate": "Latest update: <b>{updatedAt}</b>",
|
||||
"visitModHomepage": "Visit mod homepage",
|
||||
"installMod": "Install Mod 🎉",
|
||||
"uninstallMod": "Uninstall Mod",
|
||||
|
@ -102,9 +108,8 @@
|
|||
}
|
||||
},
|
||||
"releaseNotes": {
|
||||
"title": "Release notes - Zen Browser",
|
||||
"topSection": {
|
||||
"title": "Release Notes",
|
||||
"title": "Changelog",
|
||||
"description": "Stay up to date with the latest changes to Zen! Since the <a class=\"zen-link\" href=\"#1.0.0-a.1\">first release</a> till <a class=\"zen-link\" href=\"#{latestVersion}\">{latestVersion}</a>, we've been working hard to make Zen Browser the best it can be. Thanks everyone for your feedback! ❤️"
|
||||
},
|
||||
"list": {
|
||||
|
@ -257,25 +262,25 @@
|
|||
}
|
||||
},
|
||||
"download": {
|
||||
"title": "Download - Zen Browser",
|
||||
"description": "Download Zen Browser for your platform and experience a more mindful internet browsing experience. All downloads include SHA256 checksums for verification.",
|
||||
"title": "Download Zen",
|
||||
"description": "Download Zen for your platform and experience a more mindful internet browsing experience. All downloads include SHA256 checksums for verification.",
|
||||
"twilightInfo": "You're currently in Twilight mode, this means you're downloading the latest experimental features and updates.",
|
||||
"alertInfo": {
|
||||
"description": "<strong class='font-medium text-zen-blue'>Twilight Mode:</strong> You're currently in Twilight mode, this means you're downloading the latest experimental features and updates."
|
||||
},
|
||||
"platformSelector": {
|
||||
"title": "Platform Selector",
|
||||
"description": "Select your platform to download Zen Browser."
|
||||
"description": "Select your platform to download Zen."
|
||||
},
|
||||
"additionalResources": {
|
||||
"title": "Additional Resources",
|
||||
"sourceCode": {
|
||||
"title": "Source Code",
|
||||
"description": "Explore Zen Browser's source code on GitHub. Contribute to the project or build your own version."
|
||||
"description": "Explore Zen's source code on GitHub. Contribute to the project or build your own version."
|
||||
},
|
||||
"documentation": {
|
||||
"title": "Documentation",
|
||||
"description": "Access comprehensive documentation, guides, and tutorials for Zen Browser."
|
||||
"description": "Access comprehensive documentation, guides, and tutorials for Zen."
|
||||
}
|
||||
},
|
||||
"securityNotice": {
|
||||
|
@ -294,6 +299,20 @@
|
|||
"mac": "Works on both new Apple (M-Series) and older Intel Macs.<br />Requires macOS 11.0 or later.",
|
||||
"windows": "Works on Windows 10 and Windows 11.<br />Not sure which version to get? Most people should choose the 64-bit installer.",
|
||||
"linux": "Works with many Linux versions.<br />Pick the download that matches your system."
|
||||
},
|
||||
"links": {
|
||||
"macos": { "universal": "Universal" },
|
||||
"windows": { "64bit": "64-bit (Recommended)", "ARM64": "ARM64" },
|
||||
"linux": {
|
||||
"flathub": "Flathub",
|
||||
"x86_64": "Tarball",
|
||||
"aarch64": "Tarball"
|
||||
}
|
||||
},
|
||||
"buttonCard": {
|
||||
"copy": "Copy",
|
||||
"showChecksum": "Show SHA-256",
|
||||
"beta": "Beta"
|
||||
}
|
||||
},
|
||||
"privacyPolicy": {
|
||||
|
@ -302,97 +321,97 @@
|
|||
"sections": {
|
||||
"introduction": {
|
||||
"title": "Introduction",
|
||||
"body": "Welcome to Zen Browser! Your privacy is our priority. This Privacy Policy outlines the types of personal information we collect, how we use it, and the steps we take to protect your data when you use Zen Browser.",
|
||||
"body": "Welcome to Zen! Your privacy is our priority. This Privacy Policy outlines the types of personal information we collect, how we use it, and the steps we take to protect your data when you use Zen.",
|
||||
"summary": "We don't sell data - We don't collect data - We don't track you"
|
||||
},
|
||||
"noCollect": {
|
||||
"title": "1. Information We Do Not Collect",
|
||||
"body": "Zen Browser is designed with privacy in mind. We do not collect, store, or share any of your personal data. Here's what that means:"
|
||||
"body": "Zen is designed with privacy in mind. We do not collect, store, or share any of your personal data. Here's what that means:"
|
||||
},
|
||||
"noTelemetry": {
|
||||
"title": "1.1. No Telemetry",
|
||||
"body": "We do not collect any telemetry data or crash reports.",
|
||||
"body2": "Zen Browser has stripped out telemetry built into Mozilla Firefox. We have removed all telemetry data collection and crash reports."
|
||||
"body2": "Zen has stripped out telemetry built into Mozilla Firefox. We have removed all telemetry data collection and crash reports."
|
||||
},
|
||||
"noPersonalData": {
|
||||
"title": "1.2. No Personal Data Collection",
|
||||
"body": "Zen Browser does not collect any personal information such as your IP address, browsing history, search queries, or form data."
|
||||
"body": "Zen does not collect any personal information such as your IP address, browsing history, search queries, or form data."
|
||||
},
|
||||
"noThirdParty": {
|
||||
"title": "1.3. No Third-Party Tracking",
|
||||
"body": "We do not allow third-party trackers or analytics tools to operate within Zen Browser. Your browsing activity remains entirely private and is not shared with any third party. Mozilla is not considered a third party as it is the base of Zen Browser."
|
||||
"body": "We do not allow third-party trackers or analytics tools to operate within Zen. Your browsing activity remains entirely private and is not shared with any third party. Mozilla is not considered a third party as it is the base of Zen."
|
||||
},
|
||||
"externalConnections": {
|
||||
"title": "1.4. External connections made at startup",
|
||||
"body": "Zen Browser may make external connections at startup to check for updates and ensure the browser is up to date on plugins, addons, check for connectivity and Geolocation/push notifications services in order to comply with web standards. We, at Zen, do not collect any data from these connections, but they may be logged by third-party services or websites you visit. These connections are necessary for the proper functioning of the browser and are not used for tracking or profiling purposes. They can be disabled through the browser flags (about:config)."
|
||||
"body": "Zen may make external connections at startup to check for updates and ensure the browser is up to date on plugins, addons, check for connectivity and Geolocation/push notifications services in order to comply with web standards. We, at Zen, do not collect any data from these connections, but they may be logged by third-party services or websites you visit. These connections are necessary for the proper functioning of the browser and are not used for tracking or profiling purposes. They can be disabled through the browser flags (about:config)."
|
||||
},
|
||||
"localStorage": {
|
||||
"title": "2. Information Stored Locally on Your Device"
|
||||
},
|
||||
"browsingData": {
|
||||
"title": "2.1. Browsing Data",
|
||||
"body": "Zen Browser stores certain data locally on your device to enhance your browsing experience. This includes:"
|
||||
"body": "Zen stores certain data locally on your device to enhance your browsing experience. This includes:"
|
||||
},
|
||||
"cookies": {
|
||||
"title": "Cookies",
|
||||
"body": "Cookies are stored locally on your device and are not shared with Zen Browser or any third party. You have full control over the management of cookies through the browser's settings."
|
||||
"body": "Cookies are stored locally on your device and are not shared with Zen or any third party. You have full control over the management of cookies through the browser's settings."
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache and Temporary Files",
|
||||
"body": "Zen Browser may store cache files and other temporary data locally to improve performance. These files can be cleared at any time through the browser's settings."
|
||||
"body": "Zen may store cache files and other temporary data locally to improve performance. These files can be cleared at any time through the browser's settings."
|
||||
},
|
||||
"settings": {
|
||||
"title": "2.2. Settings and Preferences",
|
||||
"body": "Any customizations, settings, and preferences you make within Zen Browser are stored locally on your device. We do not have access to or control over this data."
|
||||
"body": "Any customizations, settings, and preferences you make within Zen are stored locally on your device. We do not have access to or control over this data."
|
||||
},
|
||||
"sync": {
|
||||
"title": "3. Sync Feature",
|
||||
"body": "Zen Browser offers a \"Sync\" feature, which is implemented using Mozilla Firefox's Sync feature. This feature allows you to synchronize your bookmarks, history, passwords, and other data across multiple devices. For this feature to work, your data is encrypted and stored on Mozilla's servers and is treated in accordance with their Privacy Policy. We, at Zen, cannot view any of this data.",
|
||||
"body": "Zen offers a \"Sync\" feature, which is implemented using Mozilla Firefox's Sync feature. This feature allows you to synchronize your bookmarks, history, passwords, and other data across multiple devices. For this feature to work, your data is encrypted and stored on Mozilla's servers and is treated in accordance with their Privacy Policy. We, at Zen, cannot view any of this data.",
|
||||
"link1": "Mozilla Firefox Sync",
|
||||
"link2": "This is how we store your passwords"
|
||||
},
|
||||
"addons": {
|
||||
"title": "4. Add-ons and \"Mods\"",
|
||||
"body": "You can install Add-ons from addons.mozilla.org. Zen Browser periodically checks for updates to these Add-ons.\nYou can also install \"Mods\" from zen-browser.app/mods. These Mods are hosted by our services and follow the same privacy policy our website. We do not collect any data from these Mods, they are purely static content that is downloaded to your device."
|
||||
"body": "You can install Add-ons from addons.mozilla.org. Zen periodically checks for updates to these Add-ons.\nYou can also install \"Mods\" from zen-browser.app/mods. These Mods are hosted by our services and follow the same privacy policy our website. We do not collect any data from these Mods, they are purely static content that is downloaded to your device."
|
||||
},
|
||||
"security": {
|
||||
"title": "5. Data Security",
|
||||
"body": "Although Zen Browser does not collect your data, we are committed to protecting the information that is stored locally on your device and, if you use the Sync feature, the encrypted data stored on Mozilla's servers. We recommend that you use secure passwords, enable device encryption, and regularly update your software to ensure your data remains safe.",
|
||||
"body": "Although Zen does not collect your data, we are committed to protecting the information that is stored locally on your device and, if you use the Sync feature, the encrypted data stored on Mozilla's servers. We recommend that you use secure passwords, enable device encryption, and regularly update your software to ensure your data remains safe.",
|
||||
"note": "Note that most of the security measures are taken care by Mozilla Firefox."
|
||||
},
|
||||
"control": {
|
||||
"title": "6. Your Control",
|
||||
"deletionTitle": "6.1. Data Deletion",
|
||||
"deletionBody": "You have full control over all data stored locally on your device by Zen Browser. You can clear your browsing data, cookies, and cache at any time using the browser's settings."
|
||||
"deletionBody": "You have full control over all data stored locally on your device by Zen. You can clear your browsing data, cookies, and cache at any time using the browser's settings."
|
||||
},
|
||||
"website": {
|
||||
"title": "7. Our Website and Services",
|
||||
"body": "Zen Browser's website and services do not use any third-party analytics, tracking, or CDN services. We do not collect any personal information from users visiting our website. The website is hosted on Cloudflare but with analytics and tracking disabled, Cloudflare may collect some analytics data from HTTP requests in order to provide security and performance improvements. However, this data is not linked to any personal information and is not used for tracking purposes.",
|
||||
"body": "Zen's website and services do not use any third-party analytics, tracking, or CDN services. We do not collect any personal information from users visiting our website. The website is hosted on Cloudflare but with analytics and tracking disabled, Cloudflare may collect some analytics data from HTTP requests in order to provide security and performance improvements. However, this data is not linked to any personal information and is not used for tracking purposes.",
|
||||
"externalLinksTitle": "7.1. External links",
|
||||
"externalLinksBody": "Zen Browser may contain links to external websites or services that are not owned or operated by us. We are not responsible for the content or privacy practices of these sites. We recommend that you review the privacy policies of these sites before providing them with any personal information."
|
||||
"externalLinksBody": "Zen may contain links to external websites or services that are not owned or operated by us. We are not responsible for the content or privacy practices of these sites. We recommend that you review the privacy policies of these sites before providing them with any personal information."
|
||||
},
|
||||
"changes": {
|
||||
"title": "8. Changes to This Privacy Policy",
|
||||
"body": "We may update this Privacy Policy from time to time to reflect changes in our practices or legal requirements. We will notify you of any significant changes by updating the effective date at the top of this policy. Continued use of Zen Browser after such changes constitutes your acceptance of the new terms."
|
||||
"body": "We may update this Privacy Policy from time to time to reflect changes in our practices or legal requirements. We will notify you of any significant changes by updating the effective date at the top of this policy. Continued use of Zen after such changes constitutes your acceptance of the new terms."
|
||||
},
|
||||
"otherTelemetry": {
|
||||
"title": "9. Other telemetry done by Mozilla Firefox",
|
||||
"body": "We try to disable all telemetry data collection in Zen Browser. But, we may have missed some. Check the below links for more information.",
|
||||
"body": "We try to disable all telemetry data collection in Zen. But, we may have missed some. Check the below links for more information.",
|
||||
"firefoxPrivacyNotice": "Firefox Privacy Notice",
|
||||
"forMoreInformation": "for more information."
|
||||
},
|
||||
"contact": {
|
||||
"title": "10. Contact Us",
|
||||
"body": "If you have any questions or concerns about this Privacy Policy or Zen Browser, please contact us at:",
|
||||
"body": "If you have any questions or concerns about this Privacy Policy or Zen, please contact us at:",
|
||||
"discord": "Discord: ",
|
||||
"discordLink": "Zen Browser's Discord",
|
||||
"discordLink": "Zen's Discord",
|
||||
"github": "GitHub: ",
|
||||
"githubLink": "Organization"
|
||||
}
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"title": ["Welcome", "to", "Zen!"]
|
||||
"title": ["Welcome ", "to ", "Zen!"]
|
||||
},
|
||||
"whatsNew": {
|
||||
"title": "What's New in {latestVersion.version}!",
|
||||
|
@ -413,11 +432,11 @@
|
|||
},
|
||||
"mods": {
|
||||
"title": "Zen Mods",
|
||||
"description": "Browse our diverse collection of Zen Mods, community-made plugins and themes for Zen Browser. Discover a theme to match every mood, and a plugin to fulfill every requirement. Start customizing your browser experience today!"
|
||||
"description": "Browse our diverse collection of Zen Mods, community-made plugins and themes for Zen. Discover a theme to match every mood, and a plugin to fulfill every requirement. Start customizing your browser experience today!"
|
||||
},
|
||||
"releaseNotes": {
|
||||
"title": "Release notes - Zen",
|
||||
"description": "Stay up to date with the latest changes to Zen Browser! Since the first release till {latestVersion}, we've been working hard to make Zen Browser the best it can be. Thanks everyone for your feedback! ❤️"
|
||||
"description": "Stay up to date with the latest changes to Zen! Since the first release till {latestVersion}, we've been working hard to make Zen the best it can be. Thanks everyone for your feedback! ❤️"
|
||||
},
|
||||
"about": {
|
||||
"title": "About Zen",
|
||||
|
@ -429,11 +448,11 @@
|
|||
},
|
||||
"download": {
|
||||
"title": "Download - Zen",
|
||||
"description": "Download Zen Browser for your platform and experience a more mindful internet browsing experience. All downloads include SHA256 checksums for verification."
|
||||
"description": "Download Zen for your platform and experience a more mindful internet browsing experience. All downloads include SHA256 checksums for verification."
|
||||
},
|
||||
"privacyPolicy": {
|
||||
"title": "Privacy Policy - Zen",
|
||||
"description": "Your privacy is our priority. This Privacy Policy outlines the types of personal information we collect, how we use it, and the steps we take to protect your data when you use Zen Browser."
|
||||
"description": "Your privacy is our priority. This Privacy Policy outlines the types of personal information we collect, how we use it, and the steps we take to protect your data when you use Zen."
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Welcome!",
|
||||
|
@ -478,13 +497,13 @@
|
|||
"releaseNotesDesc": "Stay up to date with the latest features and improvements.",
|
||||
"discordDesc": "Join our community on Discord to chat with other Zen users!",
|
||||
"donate": "Donate ❤️",
|
||||
"donateDesc": "Support the development of Zen Browser with a donation.",
|
||||
"donateDesc": "Support the development of Zen with a donation.",
|
||||
"aboutUs": "About Us 🌟",
|
||||
"aboutUsDesc": "Learn more about the team behind Zen Browser.",
|
||||
"aboutUsDesc": "Learn more about the team behind Zen.",
|
||||
"documentation": "Documentation",
|
||||
"documentationDesc": "Learn how to use Zen Browser with our documentation.",
|
||||
"documentationDesc": "Learn how to use Zen with our documentation.",
|
||||
"github": "GitHub",
|
||||
"githubDesc": "Contribute to the development of Zen Browser on GitHub.",
|
||||
"githubDesc": "Contribute to the development of Zen on GitHub.",
|
||||
"menu": "Menu"
|
||||
}
|
||||
}
|
||||
|
|
515
src/i18n/ja/translation.json
Normal file
515
src/i18n/ja/translation.json
Normal file
|
@ -0,0 +1,515 @@
|
|||
{
|
||||
"routes": {
|
||||
"index": {
|
||||
"title": "Zenブラウザー",
|
||||
"hero": {
|
||||
"title": [
|
||||
{ "text": "ようこそ", "highlight": false },
|
||||
{ "text": "\n", "highlight": false },
|
||||
{ "text": "静かな", "highlight": true },
|
||||
{ "text": "\n", "highlight": false },
|
||||
{ "text": "インターネット", "highlight": false },
|
||||
{ "text": "へ", "highlight": false }
|
||||
],
|
||||
"description": [
|
||||
"美しいデザイン、プライバシー重視、機能満載。",
|
||||
"私たちはあなたの体験を大切にし、データには関心がありません。"
|
||||
],
|
||||
"buttons": {
|
||||
"beta": "ベータ版が利用可能です!",
|
||||
"support": "サポートする ❤️"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"titles": ["生産性", "の", "極み"],
|
||||
"description": "Zenは、生産性と集中力を高める機能が満載です。ブラウザーは作業の妨げではなく、作業を助けるツールであるべきです。",
|
||||
"featureTabs": {
|
||||
"workspaces": {
|
||||
"title": "ワークスペース",
|
||||
"description": "タブをワークスペースごとに整理し、プロジェクトごとに分けて管理。簡単に切り替え可能です。"
|
||||
},
|
||||
"compactMode": {
|
||||
"title": "コンパクトモード",
|
||||
"description": "Zenのコンパクトモードは、必要ないときにタブバーを隠し、必要なときに表示して画面を広く使えます。"
|
||||
},
|
||||
"glance": {
|
||||
"title": "覗き見",
|
||||
"description": "覗き見機能で、よく使うタブを素早く切り替え。履歴をスクロールする必要はありません。"
|
||||
},
|
||||
"splitView": {
|
||||
"title": "画面分割",
|
||||
"description": "画面分割機能で、2つのタブを並べて表示。比較や切り替えが簡単です。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sponsors": {
|
||||
"title": "スポンサー",
|
||||
"description": "ご支援いただいているスポンサーの皆様に感謝します。<br />あなたも<a href=\"/donate\" class=\"zen-link\">直接寄付</a>でこの旅に参加できます!",
|
||||
"sponsors": {
|
||||
"tuta": {
|
||||
"name": "Tuta",
|
||||
"url": "https://tuta.com/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community": {
|
||||
"title": ["私たちの", "コア", "バリュー"],
|
||||
"description": "Zenは美しさ、パフォーマンス、プライバシーのバランスを最優先にしています。最高の体験を妥協せずに提供します。",
|
||||
"lists": {
|
||||
"freeAndOpenSource": {
|
||||
"title": "無料・オープンソース",
|
||||
"description": "Zenは無料でオープンソース。誰でも自由に使え、カスタマイズできます。"
|
||||
},
|
||||
"simpleYetPowerful": {
|
||||
"title": "シンプルでパワフル",
|
||||
"description": "Zenは使いやすく、日常の作業も十分にこなせます。"
|
||||
},
|
||||
"privateAndAlwaysUpToDate": {
|
||||
"title": "プライバシー重視・常に最新",
|
||||
"description": "Zenはプライバシーを守り、常に最新。無料で使え、カスタマイズも可能です。"
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"community": {
|
||||
"alt": "コミュニティ"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mods": {
|
||||
"title": "Zen Mods",
|
||||
"description": "Zen Browser用の多彩なMod(プラグイン・テーマ)を探そう。気分やニーズに合ったテーマやプラグインで、ブラウザー体験をカスタマイズ!",
|
||||
"pagination": {
|
||||
"pagination": "{input} / {totalPages}(全{totalItems}件)"
|
||||
},
|
||||
"search": "検索ワードを入力...",
|
||||
"by": "作者",
|
||||
"sort": {
|
||||
"lastCreated": "新着順",
|
||||
"lastUpdated": "更新順",
|
||||
"perPage": "表示件数"
|
||||
},
|
||||
"noResults": "結果が見つかりません",
|
||||
"noResultsDescription": "別のキーワードで検索するか、後でもう一度お試しください。",
|
||||
"slug": {
|
||||
"title": "{name} - Zen Mods",
|
||||
"description": "{name} Modの詳細(Zen用)",
|
||||
"alert": {
|
||||
"description": "このテーマをインストールするにはZenが必要です。",
|
||||
"button": "今すぐダウンロード!"
|
||||
},
|
||||
"createdBy": "作者:{author} • <b>v{version}</b>",
|
||||
"creationDate": "作成日:<b>{createdAt}</b>",
|
||||
"latestUpdate": "最終更新:<b>{updatedAt}</b>",
|
||||
"visitModHomepage": "Modのホームページへ",
|
||||
"installMod": "Modをインストール 🎉",
|
||||
"uninstallMod": "Modをアンインストール",
|
||||
"back": "戻る"
|
||||
}
|
||||
},
|
||||
"releaseNotes": {
|
||||
"topSection": {
|
||||
"title": "変更履歴",
|
||||
"description": "Zenの最新情報はこちら!<a class=\"zen-link\" href=\"#1.0.0-a.1\">最初のリリース</a>から<a class=\"zen-link\" href=\"#{latestVersion}\">{latestVersion}</a>まで、最高のブラウザーを目指して努力しています。ご意見ありがとうございます!❤️"
|
||||
},
|
||||
"list": {
|
||||
"support": "応援してください!",
|
||||
"navigateToVersion": "バージョンへ移動..."
|
||||
},
|
||||
"itemType": {
|
||||
"fix": "修正",
|
||||
"feature": "追加",
|
||||
"known": "既知",
|
||||
"break": "重大",
|
||||
"theme": "テーマ",
|
||||
"security": "セキュリティ"
|
||||
},
|
||||
"backToTop": "トップへ戻る",
|
||||
"chooseVersion": "バージョンを選択",
|
||||
"components": {
|
||||
"releaseNoteItem": {
|
||||
"twilight": "Twilight",
|
||||
"twilightChanges": "Twilightの変更点",
|
||||
"releaseChanges": "v{version}",
|
||||
"firefoxVersion": "Firefox {version}",
|
||||
"githubRelease": "GitHubリリース",
|
||||
"workflowRun": "ワークフロー実行",
|
||||
"compareChanges": "変更を比較",
|
||||
"twilightWarning": "TwilightはZen Browserのプレリリース版です。不具合や未完成の機能が含まれる場合があります。",
|
||||
"reportIssues": " 問題が発生した場合は、<a rel=\"noopener noreferrer\" target=\"_blank\" href=\"https://github.com/zen-browser/desktop/issues/\" class=\"zen-link\">issueページ</a>でご報告ください。",
|
||||
"learnMore": "詳細はこちら",
|
||||
"viewIssue": "GitHubのIssue番号{issue}を見る"
|
||||
}
|
||||
},
|
||||
"slug": {
|
||||
"title": "リリースノート",
|
||||
"redirect": "バージョン{version}のリリースノートにリダイレクト中..."
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "Zenについて",
|
||||
"description": "私たちは、ウェブ体験を大切にする開発者とデザイナーの集まりです。インターネットは、データ収集を心配せずに探索・学習・交流できる場所であるべきだと信じています。",
|
||||
"littleHelp": "応援しませんか?",
|
||||
"mainTeam": {
|
||||
"title": "メインチーム",
|
||||
"description": "最高のブラウジング体験を提供するために努力しているメンバーです。",
|
||||
"subTitle": {
|
||||
"browser": "ブラウザー",
|
||||
"website": "ウェブサイト・ブランディング"
|
||||
},
|
||||
"members": {
|
||||
"browser": {
|
||||
"mauro": {
|
||||
"name": "Mauro B.",
|
||||
"description": "クリエイター・メイン開発者",
|
||||
"link": "https://cheff.dev/"
|
||||
},
|
||||
"jan": {
|
||||
"name": "Jan Heres",
|
||||
"description": "MacOSビルド担当・貢献者",
|
||||
"link": "https://janheres.eu/"
|
||||
},
|
||||
"bryan": {
|
||||
"name": "Bryan Galdámez",
|
||||
"description": "テーマ機能の大貢献者",
|
||||
"link": "https://josuegalre.netlify.app/"
|
||||
},
|
||||
"oscar": {
|
||||
"name": "Oscar Gonzalez",
|
||||
"description": "SRE・コード署名担当",
|
||||
"link": false
|
||||
},
|
||||
"daniel": {
|
||||
"name": "Daniel García",
|
||||
"description": "MacOS証明書・公証管理",
|
||||
"link": false
|
||||
},
|
||||
"brhm": {
|
||||
"name": "BrhmDev",
|
||||
"description": "大きな貢献をしている開発者",
|
||||
"link": "https://github.com/BrhmDev"
|
||||
},
|
||||
"kristijanribaric": {
|
||||
"name": "Kristijan Ribaric",
|
||||
"description": "スプリットビュー・ワークスペース担当",
|
||||
"link": "https://github.com/kristijanribaric"
|
||||
},
|
||||
"larvey": {
|
||||
"name": "Larvey",
|
||||
"description": "AUR管理者",
|
||||
"link": "https://github.com/LarveyOfficial/"
|
||||
},
|
||||
"studio": {
|
||||
"name": "Studio Movie Girl",
|
||||
"description": "グラデーションジェネレーターの貢献者",
|
||||
"link": "https://github.com/neurokitti"
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"taroj1205": {
|
||||
"name": "Shintaro Jokagi",
|
||||
"description": "コアウェブサイトアーキテクト、リファクタリング・技術強化担当",
|
||||
"link": "https://github.com/taroj1205"
|
||||
},
|
||||
"jace": {
|
||||
"name": "Jace",
|
||||
"description": "ウェブサイトデザイン・ブランディング担当",
|
||||
"link": "https://x.com/JaceThings"
|
||||
},
|
||||
"canoa": {
|
||||
"name": "Canoa",
|
||||
"description": "活発な貢献者・ウェブサイト管理",
|
||||
"link": "https://thatcanoa.org/"
|
||||
},
|
||||
"adam": {
|
||||
"name": "Adam",
|
||||
"description": "ブランディング・デザイン",
|
||||
"link": "https://cybrneon.xyz/"
|
||||
},
|
||||
"n7itro": {
|
||||
"name": "n7itro",
|
||||
"description": "リリースノート執筆・貢献者",
|
||||
"link": "https://github.com/n7itro"
|
||||
},
|
||||
"jafeth": {
|
||||
"name": "Jafeth Garro",
|
||||
"description": "ドキュメント執筆",
|
||||
"link": "https://iamjafeth.com/"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contributors": {
|
||||
"title": "コントリビューター",
|
||||
"description": "Zenの発展に貢献してくださった皆様です。",
|
||||
"browser": "ブラウザー",
|
||||
"website": "ウェブサイト"
|
||||
}
|
||||
},
|
||||
"donate": {
|
||||
"title": "寄付",
|
||||
"description": "私たちは少人数の開発チームです。ご支援いただけると幸いです。",
|
||||
"patreon": {
|
||||
"title": "Patreon",
|
||||
"description": "Patreonで毎月のご支援が可能です。ご自身に合った支援レベルをお選びください。",
|
||||
"button": "Patreonへ"
|
||||
},
|
||||
"koFi": {
|
||||
"title": "Ko-fi",
|
||||
"description": "Ko-fiで一度きり、または毎月のご支援が可能です。ご希望の金額をお選びください。",
|
||||
"button": "Ko-fiへ"
|
||||
}
|
||||
},
|
||||
"download": {
|
||||
"title": "Zenをダウンロードする",
|
||||
"description": "お使いのプラットフォーム向けにZenをダウンロード。すべてのダウンロードにはSHA256チェックサムが付属しています。",
|
||||
"twilightInfo": "現在Twilightモードです。最新の実験的機能とアップデートをダウンロードしています。",
|
||||
"alertInfo": {
|
||||
"description": "<strong class='font-medium text-zen-blue'>Twilightモード:</strong> 現在Twilightモードで、最新の実験的機能とアップデートをダウンロードしています。"
|
||||
},
|
||||
"platformSelector": {
|
||||
"title": "プラットフォーム選択",
|
||||
"description": "お使いのプラットフォームを選択してZenをダウンロード。"
|
||||
},
|
||||
"additionalResources": {
|
||||
"title": "追加リソース",
|
||||
"sourceCode": {
|
||||
"title": "ソースコード",
|
||||
"description": "GitHubでZenのソースコードを閲覧・貢献・ビルドできます。"
|
||||
},
|
||||
"documentation": {
|
||||
"title": "ドキュメント",
|
||||
"description": "Zenの包括的なドキュメント・ガイド・チュートリアル。"
|
||||
}
|
||||
},
|
||||
"securityNotice": {
|
||||
"title": "検証済み・安全なダウンロード",
|
||||
"description": "すべてのZenダウンロードは署名・検証済みです。公式サイトまたはGitHubからのダウンロードを推奨します。ダウンロードに問題がある場合やウイルス対策で警告が出た場合は、<a href='https://github.com/zen-browser/desktop/issues/new/choose' class='zen-link ml-1'>ご報告ください</a>。"
|
||||
},
|
||||
"platformNames": {
|
||||
"mac": "macOS",
|
||||
"windows": "Windows",
|
||||
"linux": "Linux",
|
||||
"macDownload": "MacOSダウンロード",
|
||||
"windowsDownload": "Windowsダウンロード",
|
||||
"linuxDownload": "Linuxダウンロード"
|
||||
},
|
||||
"platformDescriptions": {
|
||||
"mac": "Apple(Mシリーズ)・Intel両対応。<br />macOS 11.0以降が必要です。",
|
||||
"windows": "Windows 10・11対応。<br />どちらを選ぶか迷った場合は64ビット版を推奨します。",
|
||||
"linux": "多くのLinuxディストリビューションで動作。<br />お使いのシステムに合ったものを選択してください。"
|
||||
},
|
||||
"links": {
|
||||
"macos": {
|
||||
"universal": "ユニバーサル"
|
||||
},
|
||||
"windows": {
|
||||
"64bit": "64-ビット(推奨)",
|
||||
"ARM64": "ARM64"
|
||||
},
|
||||
"linux": {
|
||||
"flathub": "Flathub",
|
||||
"x86_64": "Tarball",
|
||||
"aarch64": "Tarball"
|
||||
}
|
||||
},
|
||||
"buttonCard": {
|
||||
"copy": "コピー",
|
||||
"showChecksum": "SHA-256を表示",
|
||||
"beta": "ベータ"
|
||||
}
|
||||
},
|
||||
"privacyPolicy": {
|
||||
"title": "プライバシーポリシー",
|
||||
"lastUpdated": "最終更新: 2025-02-5",
|
||||
"sections": {
|
||||
"introduction": {
|
||||
"title": "はじめに",
|
||||
"body": "Zenへようこそ!あなたのプライバシーは最優先です。本ポリシーでは、収集する情報の種類、利用方法、保護手段について説明します。",
|
||||
"summary": "データ販売なし - データ収集なし - トラッキングなし"
|
||||
},
|
||||
"noCollect": {
|
||||
"title": "1. 収集しない情報",
|
||||
"body": "Zenはプライバシー重視で設計されています。個人データを収集・保存・共有しません。"
|
||||
},
|
||||
"noTelemetry": {
|
||||
"title": "1.1. テレメトリーなし",
|
||||
"body": "テレメトリーデータやクラッシュレポートは収集しません。",
|
||||
"body2": "Mozilla Firefoxに組み込まれているテレメトリーも削除しています。"
|
||||
},
|
||||
"noPersonalData": {
|
||||
"title": "1.2. 個人データの収集なし",
|
||||
"body": "IPアドレス、閲覧履歴、検索クエリ、フォームデータなどの個人情報は一切収集しません。"
|
||||
},
|
||||
"noThirdParty": {
|
||||
"title": "1.3. サードパーティトラッキングなし",
|
||||
"body": "サードパーティのトラッカーや解析ツールは一切許可していません。Mozillaはベースであり、サードパーティではありません。"
|
||||
},
|
||||
"externalConnections": {
|
||||
"title": "1.4. 起動時の外部接続",
|
||||
"body": "Zenは起動時にアップデート確認や接続性・ジオロケーション/プッシュ通知サービスのため外部接続を行う場合があります。これらの接続は機能上必要で、トラッキングやプロファイリング目的ではありません。about:configで無効化可能です。"
|
||||
},
|
||||
"localStorage": {
|
||||
"title": "2. デバイスに保存される情報"
|
||||
},
|
||||
"browsingData": {
|
||||
"title": "2.1. 閲覧データ",
|
||||
"body": "Zenは体験向上のため、いくつかのデータをローカルに保存します。"
|
||||
},
|
||||
"cookies": {
|
||||
"title": "クッキー",
|
||||
"body": "クッキーはローカルに保存され、Zenや第三者と共有されません。管理はブラウザー設定から可能です。"
|
||||
},
|
||||
"cache": {
|
||||
"title": "キャッシュ・一時ファイル",
|
||||
"body": "パフォーマンス向上のためキャッシュや一時データを保存します。設定からいつでも削除可能です。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "2.2. 設定・プリファレンス",
|
||||
"body": "カスタマイズや設定はすべてローカルに保存され、私たちがアクセスすることはありません。"
|
||||
},
|
||||
"sync": {
|
||||
"title": "3. 同期機能",
|
||||
"body": "ZenはMozilla FirefoxのSync機能を利用しています。データは暗号化されMozillaのサーバーに保存されます。私たちは内容を閲覧できません。",
|
||||
"link1": "Mozilla Firefox Sync",
|
||||
"link2": "パスワードの保存方法"
|
||||
},
|
||||
"addons": {
|
||||
"title": "4. アドオン・Mod",
|
||||
"body": "MozillaのアドオンやZen Modsをインストール可能です。Zen Modsは当社サービスでホストされ、データ収集はありません。"
|
||||
},
|
||||
"security": {
|
||||
"title": "5. データセキュリティ",
|
||||
"body": "Zenはデータを収集しませんが、ローカルやMozillaサーバー上のデータ保護に努めています。安全なパスワードやデバイス暗号化、ソフトウェアの定期更新を推奨します。",
|
||||
"note": "多くのセキュリティ対策はMozilla Firefoxによって提供されています。"
|
||||
},
|
||||
"control": {
|
||||
"title": "6. コントロール",
|
||||
"deletionTitle": "6.1. データ削除",
|
||||
"deletionBody": "Zenが保存するすべてのローカルデータは、設定からいつでも削除できます。"
|
||||
},
|
||||
"website": {
|
||||
"title": "7. ウェブサイト・サービス",
|
||||
"body": "Zenのウェブサイト・サービスはサードパーティの解析やCDNを使用しません。Cloudflareでホストされていますが、解析・トラッキングは無効化されています。",
|
||||
"externalLinksTitle": "7.1. 外部リンク",
|
||||
"externalLinksBody": "Zenには外部サイトへのリンクが含まれる場合があります。内容やプライバシーについては各サイトのポリシーをご確認ください。"
|
||||
},
|
||||
"changes": {
|
||||
"title": "8. ポリシーの変更",
|
||||
"body": "本ポリシーは必要に応じて更新されます。重要な変更時は日付を更新し、継続利用で同意したものとみなします。"
|
||||
},
|
||||
"otherTelemetry": {
|
||||
"title": "9. Mozilla Firefoxによるその他のテレメトリー",
|
||||
"body": "すべてのテレメトリー無効化に努めていますが、見落としがある場合もあります。詳細は下記リンクをご参照ください。",
|
||||
"firefoxPrivacyNotice": "Firefoxプライバシー通知",
|
||||
"forMoreInformation": "詳細はこちら。"
|
||||
},
|
||||
"contact": {
|
||||
"title": "10. お問い合わせ",
|
||||
"body": "本ポリシーやZenに関するご質問は下記までご連絡ください:",
|
||||
"discord": "Discord: ",
|
||||
"discordLink": "ZenのDiscord",
|
||||
"github": "GitHub: ",
|
||||
"githubLink": "Organization"
|
||||
}
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"title": ["ようこそ", "Zenへ", "!"]
|
||||
},
|
||||
"whatsNew": {
|
||||
"title": "{latestVersion.version}の新機能!",
|
||||
"reportIssue": "問題を報告する",
|
||||
"joinDiscord": "Discordに参加",
|
||||
"readFullReleaseNotes": "リリースノート全文を読む"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "ページが見つかりません",
|
||||
"description": "お探しのページは存在しないか、移動されました。",
|
||||
"button": "ホームへ戻る"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"index": {
|
||||
"title": "Zenブラウザー",
|
||||
"description": "美しいデザイン、プライバシー重視、機能満載。"
|
||||
},
|
||||
"mods": {
|
||||
"title": "Zen Mods",
|
||||
"description": "Zen用の多彩なMod(プラグイン・テーマ)を探そう。気分やニーズに合ったテーマやプラグインで、ブラウザ体験をカスタマイズ!"
|
||||
},
|
||||
"releaseNotes": {
|
||||
"title": "リリースノート - Zen",
|
||||
"description": "Zenの最新情報はこちら!最初のリリースから{latestVersion}まで、最高のブラウザを目指して努力しています。ご意見ありがとうございます!❤️"
|
||||
},
|
||||
"about": {
|
||||
"title": "Zenについて",
|
||||
"description": "私たちは、ウェブ体験を大切にする開発者とデザイナーの集まりです。インターネットは、データ収集を心配せずに探索・学習・交流できる場所であるべきだと信じています。"
|
||||
},
|
||||
"donate": {
|
||||
"title": "寄付 - Zen",
|
||||
"description": "私たちは少人数の開発チームです。ご支援いただけると幸いです。"
|
||||
},
|
||||
"download": {
|
||||
"title": "Zenをダウンロードする",
|
||||
"description": "お使いのプラットフォーム向けにZenをダウンロード。すべてのダウンロードにはSHA256チェックサムが付属しています。"
|
||||
},
|
||||
"privacyPolicy": {
|
||||
"title": "プライバシーポリシー - Zen",
|
||||
"description": "あなたのプライバシーは最優先です。本ポリシーでは、収集する情報の種類、利用方法、保護手段について説明します。"
|
||||
},
|
||||
"welcome": {
|
||||
"title": "ようこそ!",
|
||||
"description": "Zenへようこそ!"
|
||||
},
|
||||
"whatsNew": {
|
||||
"title": "{latestVersion.version}の新機能!"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"footer": {
|
||||
"title": "Zenブラウザー",
|
||||
"description": "美しいデザイン、プライバシー重視、機能満載。私たちはあなたのデータではなく、体験を大切にします。",
|
||||
"download": "ダウンロード",
|
||||
"followUs": "フォローする",
|
||||
"aboutUs": "私たちについて",
|
||||
"teamAndContributors": "チーム・コントリビューター",
|
||||
"privacyPolicy": "プライバシーポリシー",
|
||||
"getStarted": "はじめに",
|
||||
"documentation": "ドキュメント",
|
||||
"zenMods": "Zen Mods",
|
||||
"releaseNotes": "リリースノート",
|
||||
"getHelp": "ヘルプ",
|
||||
"discord": "Discord",
|
||||
"uptimeStatus": "稼働状況",
|
||||
"reportAnIssue": "問題を報告",
|
||||
"twilight": "Twilight",
|
||||
"madeWith": "<span aria-label='love'>❤️</span>と共に<a href='{link}' class='zen-link inline-block font-bold'>Zenチーム</a>が作りました"
|
||||
},
|
||||
"nav": {
|
||||
"brand": "Zenブラウザー",
|
||||
"menu": {
|
||||
"gettingStarted": "はじめに",
|
||||
"usefulLinks": "便利なリンク",
|
||||
"mods": "Mods",
|
||||
"download": "ダウンロード",
|
||||
"discord": "Discord",
|
||||
"releaseNotes": "リリースノート",
|
||||
"zenMods": "Zen Mods",
|
||||
"tryZenMods": "Zen Modsを試す",
|
||||
"zenModsDesc": "Zen Modsでブラウザ体験をカスタマイズ。",
|
||||
"releaseNotesDesc": "最新機能・改善情報はこちら。",
|
||||
"discordDesc": "Discordコミュニティで他のZenユーザーと交流!",
|
||||
"donate": "寄付 ❤️",
|
||||
"donateDesc": "Zen開発を寄付で応援。",
|
||||
"aboutUs": "私たちについて 🌟",
|
||||
"aboutUsDesc": "Zenのチームについて知る。",
|
||||
"documentation": "ドキュメント",
|
||||
"documentationDesc": "ドキュメントでZenの使い方を学ぶ。",
|
||||
"github": "GitHub",
|
||||
"githubDesc": "GitHubでZen開発に貢献。",
|
||||
"menu": "メニュー"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,4 +2,16 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-arrow-left-icon lucide-arrow-left", className]} {...props}><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-arrow-left-icon lucide-arrow-left', className]}
|
||||
{...props}><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg
|
||||
>
|
||||
|
|
|
@ -2,4 +2,16 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-arrow-right-icon lucide-arrow-right", className]} {...props}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-arrow-right-icon lucide-arrow-right', className]}
|
||||
{...props}><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg
|
||||
>
|
||||
|
|
|
@ -2,4 +2,16 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-arrow-up-icon lucide-arrow-up", className]} {...props}><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-arrow-up-icon lucide-arrow-up', className]}
|
||||
{...props}><path d="m5 12 7-7 7 7"></path><path d="M12 19V5"></path></svg
|
||||
>
|
||||
|
|
|
@ -2,4 +2,16 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-check-icon lucide-check", className]} {...props}><path d="M20 6 9 17l-5-5"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-check-icon lucide-check', className]}
|
||||
{...props}><path d="M20 6 9 17l-5-5"></path></svg
|
||||
>
|
||||
|
|
|
@ -2,4 +2,16 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-chevron-down-icon lucide-chevron-down", className]} {...props}><path d="m6 9 6 6 6-6"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-chevron-down-icon lucide-chevron-down', className]}
|
||||
{...props}><path d="m6 9 6 6 6-6"></path></svg
|
||||
>
|
||||
|
|
|
@ -2,4 +2,18 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-download-icon lucide-download", className]} {...props}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-download-icon lucide-download', className]}
|
||||
{...props}
|
||||
><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"
|
||||
></polyline><line x1="12" x2="12" y1="15" y2="3"></line></svg
|
||||
>
|
||||
|
|
|
@ -2,4 +2,18 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-external-link-icon lucide-external-link", className]} {...props}><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-external-link-icon lucide-external-link', className]}
|
||||
{...props}
|
||||
><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path
|
||||
d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg
|
||||
>
|
||||
|
|
|
@ -2,4 +2,19 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-github-icon lucide-github", className]} {...props}><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-github-icon lucide-github', className]}
|
||||
{...props}
|
||||
><path
|
||||
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
|
||||
></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg
|
||||
>
|
||||
|
|
|
@ -2,4 +2,18 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-info-icon lucide-info", className]} {...props}><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-info-icon lucide-info', className]}
|
||||
{...props}
|
||||
><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"
|
||||
></path></svg
|
||||
>
|
||||
|
|
|
@ -2,4 +2,18 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-lock-icon lucide-lock", className]} {...props}><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-lock-icon lucide-lock', className]}
|
||||
{...props}
|
||||
><rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"
|
||||
></path></svg
|
||||
>
|
||||
|
|
|
@ -2,4 +2,16 @@
|
|||
const { class: className, ...props } = Astro.props
|
||||
---
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-menu-icon lucide-menu", className]} {...props}><path d="M4 12h16"/><path d="M4 18h16"/><path d="M4 6h16"/></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class:list={['lucide lucide-menu-icon lucide-menu', className]}
|
||||
{...props}><path d="M4 12h16"></path><path d="M4 18h16"></path><path d="M4 6h16"></path></svg
|
||||
>
|
||||
|
|
|
@ -31,7 +31,6 @@ const locale = getLocale(Astro)
|
|||
return 'light'
|
||||
})()
|
||||
|
||||
|
||||
if (theme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
} else {
|
||||
|
@ -39,27 +38,32 @@ const locale = getLocale(Astro)
|
|||
}
|
||||
</script>
|
||||
|
||||
<!doctype html>
|
||||
<html lang={locale}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
{redirect ? <meta http-equiv="refresh" content={`0;url=${redirect}`} /> : null}
|
||||
<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="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",
|
||||
"@type":"WebSite",
|
||||
"name":"Zen Browser",
|
||||
"url":"https://zen-browser.app/"
|
||||
}
|
||||
</script>)}
|
||||
{
|
||||
isHome && (
|
||||
<>
|
||||
{/* Injecting schema to homepage only (for SEO) */}
|
||||
<script
|
||||
is:inline
|
||||
type="application/ld+json"
|
||||
set:html={JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: 'Zen Browser',
|
||||
url: 'https://zen-browser.app/',
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- ICO favicon as a fallback for browsers that don't support SVG favicons (Safari) -->
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
|
@ -72,10 +76,7 @@ const locale = getLocale(Astro)
|
|||
<meta property="og:title" content={title} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={ogImage ?? defaultOgImage} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={description ?? defaultDescription}
|
||||
/>
|
||||
<meta property="og:description" content={description ?? defaultDescription} />
|
||||
<meta property="og:color" content="#da755b" />
|
||||
<!-- Twitter card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
|
@ -88,16 +89,16 @@ const locale = getLocale(Astro)
|
|||
/>
|
||||
|
||||
<script>
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'%c✌️ Zen-Browser%c\nWelcome to a calmer internet!',
|
||||
'filter: invert(1); font-size: 28px; font-weight: bolder; font-family: "Rubik"; margin-top: 20px; margin-bottom: 8px;',
|
||||
'color: #f76f53; font-size: 16px; font-family: "Rubik"; margin-bottom: 20px;'
|
||||
);
|
||||
)
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body
|
||||
class="overflow-x-hidden bg-paper font-['bricolage-grotesque'] text-dark text-balance"
|
||||
class="min-h-[max(100dvh,_64rem)] overflow-x-hidden text-balance bg-paper font-['bricolage-grotesque'] text-dark"
|
||||
>
|
||||
<NavBar />
|
||||
<slot />
|
||||
|
@ -127,13 +128,13 @@ const locale = getLocale(Astro)
|
|||
--zen-paper: #f2f0e3;
|
||||
--zen-dark: #2e2e2e;
|
||||
--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'] {
|
||||
--zen-paper: #1f1f1f;
|
||||
--zen-dark: #d1cfc0;
|
||||
--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'
|
||||
|
||||
export interface ZenTheme {
|
||||
export type ZenTheme = {
|
||||
name: string
|
||||
description: string
|
||||
image: string
|
||||
|
@ -24,7 +24,7 @@ export async function getAllMods(): Promise<ZenTheme[]> {
|
|||
const res = await fetch(THEME_API)
|
||||
const json = await res.json()
|
||||
// convert dict to array
|
||||
const mods = Object.keys(json).map((key) => json[key])
|
||||
const mods = Object.keys(json).map(key => json[key])
|
||||
return mods
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
|
|
@ -17,9 +17,7 @@ const {
|
|||
<main
|
||||
class="container flex min-h-[70vh] flex-col items-center justify-center gap-6 py-24 text-center"
|
||||
>
|
||||
<Title class="text-7xl font-bold text-coral md:text-9xl xl:text-9xl">
|
||||
404
|
||||
</Title>
|
||||
<Title class="text-7xl font-bold text-coral md:text-9xl xl:text-9xl"> 404 </Title>
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<Description class="text-xl md:text-2xl">
|
||||
{notFound.title}
|
||||
|
@ -27,7 +25,7 @@ const {
|
|||
<p class="max-w-xl text-lg text-gray-500 dark:text-gray-400">
|
||||
{notFound.description}
|
||||
</p>
|
||||
<Button href={getLocalePath("/")} isPrimary class="w-fit">
|
||||
<Button href={getLocalePath('/')} isPrimary class="w-fit">
|
||||
{notFound.button}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -14,31 +14,27 @@ const {
|
|||
} = getUI(locale)
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={layout.about.title}
|
||||
description={layout.about.description}
|
||||
>
|
||||
<main
|
||||
class="flex min-h-screen flex-col py-24 container w-full gap-24"
|
||||
>
|
||||
<div class="w-full flex flex-col gap-6">
|
||||
<Layout title={layout.about.title} description={layout.about.description}>
|
||||
<main class="container flex min-h-screen w-full flex-col gap-24 py-24">
|
||||
<div class="flex w-full flex-col gap-6">
|
||||
<Description class="text-6xl font-bold leading-none">{about.title}</Description>
|
||||
<Description class="max-w-4xl">
|
||||
{about.description}
|
||||
</Description>
|
||||
<Button href="/donate" class="w-fit" isPrimary
|
||||
>{about.littleHelp}</Button
|
||||
>
|
||||
<Button href="/donate" class="w-fit" isPrimary>{about.littleHelp}</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
<div class="text-4xl lg:text-5xl font-bold leading-none">{about.mainTeam.title}</div>
|
||||
<Description>
|
||||
{about.mainTeam.description}
|
||||
</Description>
|
||||
<div class="flex flex-col gap-6">
|
||||
{Object.entries(about.mainTeam.members).map(([team, members]) => (
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<div class="text-4xl font-bold leading-none lg:text-5xl">{about.mainTeam.title}</div>
|
||||
<Description>
|
||||
{about.mainTeam.description}
|
||||
</Description>
|
||||
<div class="flex flex-col gap-6">
|
||||
{
|
||||
Object.entries(about.mainTeam.members).map(([team, members]) => (
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-3xl font-semibold">{about.mainTeam.subTitle[team as keyof typeof about.mainTeam.subTitle]}</div>
|
||||
<div class="text-3xl font-semibold">
|
||||
{about.mainTeam.subTitle[team as keyof typeof about.mainTeam.subTitle]}
|
||||
</div>
|
||||
<ul class="flex flex-col gap-2">
|
||||
{Object.entries(members).map(([_key, member]) => (
|
||||
<li class="text-sm">
|
||||
|
@ -54,24 +50,32 @@ const {
|
|||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
<div class="text-4xl lg:text-5xl font-bold leading-none">{about.contributors.title}</div>
|
||||
<Description>
|
||||
{about.contributors.description}
|
||||
</Description>
|
||||
<div class="flex flex-col gap-4 w-fit"><Description class="text-3xl font-semibold lg:text-4xl">{about.contributors.browser}</Description>
|
||||
<a href="https://github.com/zen-browser/desktop/graphs/contributors"
|
||||
</div>
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<div class="text-4xl font-bold leading-none lg:text-5xl">{about.contributors.title}</div>
|
||||
<Description>
|
||||
{about.contributors.description}
|
||||
</Description>
|
||||
<div class="flex w-fit flex-col gap-4">
|
||||
<Description class="text-3xl font-semibold lg:text-4xl"
|
||||
>{about.contributors.browser}</Description
|
||||
>
|
||||
<a href="https://github.com/zen-browser/desktop/graphs/contributors"
|
||||
><Image
|
||||
src="https://contributors-img.web.app/image?repo=zen-browser/desktop"
|
||||
alt="Contributors"
|
||||
width={500}
|
||||
height={500}
|
||||
/></a
|
||||
></div>
|
||||
<div class="flex flex-col gap-4 w-fit"><Description class="text-3xl font-semibold lg:text-4xl">{about.contributors.website}</Description>
|
||||
>
|
||||
</div>
|
||||
<div class="flex w-fit flex-col gap-4">
|
||||
<Description class="text-3xl font-semibold lg:text-4xl"
|
||||
>{about.contributors.website}</Description
|
||||
>
|
||||
<a href="https://github.com/zen-browser/www/graphs/contributors"
|
||||
><Image
|
||||
src="https://contributors-img.web.app/image?repo=zen-browser/www"
|
||||
|
@ -79,7 +83,8 @@ const {
|
|||
width={500}
|
||||
height={500}
|
||||
/></a
|
||||
></div></div>
|
||||
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
|
|
|
@ -14,41 +14,35 @@ const {
|
|||
---
|
||||
|
||||
<Layout title={layout.donate.title} description={layout.donate.description}>
|
||||
<main class="container pb-52 pt-24 flex flex-col items-center gap-12">
|
||||
<div class="flex flex-col gap-4 lg:text-center">
|
||||
<Description class="text-6xl font-bold">{donate.title}</Description>
|
||||
<Description class="max-w-3xl">
|
||||
{donate.description}
|
||||
<main class="container flex flex-col items-center gap-12 pb-52 pt-24">
|
||||
<div class="flex flex-col gap-4 lg:text-center">
|
||||
<Description class="text-6xl font-bold">{donate.title}</Description>
|
||||
<Description class="max-w-3xl">
|
||||
{donate.description}
|
||||
</Description>
|
||||
</div>
|
||||
<div class="grid max-w-5xl grid-cols-1 gap-12 text-center lg:grid-cols-[1fr_1px_1fr]">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="text-6xl font-bold">{donate.patreon.title}</div>
|
||||
<Description>
|
||||
{donate.patreon.description}
|
||||
</Description>
|
||||
<Button isPrimary href="https://www.patreon.com/zen_browser" class="w-fit">
|
||||
{donate.patreon.button}
|
||||
<ArrowRightIcon class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
class="grid max-w-5xl grid-cols-1 gap-12 text-center lg:grid-cols-[1fr_1px_1fr]"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="text-6xl font-bold">{donate.patreon.title}</div>
|
||||
<Description>
|
||||
{donate.patreon.description}
|
||||
</Description>
|
||||
<Button
|
||||
isPrimary
|
||||
href="https://www.patreon.com/zen_browser"
|
||||
class="w-fit"
|
||||
>
|
||||
{donate.patreon.button}
|
||||
<ArrowRightIcon class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<hr class="hidden h-72 w-[1px] bg-dark opacity-15 lg:block" />
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="text-6xl font-bold">{donate.koFi.title}</div>
|
||||
<Description>
|
||||
{donate.koFi.description}
|
||||
</Description>
|
||||
<Button href="https://ko-fi.com/zen_browser" isPrimary class="w-fit">
|
||||
{donate.koFi.button}
|
||||
<ArrowRightIcon class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<hr class="hidden h-72 w-[1px] bg-dark opacity-15 lg:block" />
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="text-6xl font-bold">{donate.koFi.title}</div>
|
||||
<Description>
|
||||
{donate.koFi.description}
|
||||
</Description>
|
||||
<Button href="https://ko-fi.com/zen_browser" isPrimary class="w-fit">
|
||||
{donate.koFi.button}
|
||||
<ArrowRightIcon class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
|
|
|
@ -27,7 +27,7 @@ const appleIcon = icon({ prefix: 'fab', iconName: 'apple' })
|
|||
const githubIcon = icon({ prefix: 'fab', iconName: 'github' })
|
||||
|
||||
const checksums = await getChecksums()
|
||||
const releases = getReleasesWithChecksums(checksums)
|
||||
const releases = getReleasesWithChecksums(locale)(checksums)
|
||||
|
||||
const platformNames = download.platformNames
|
||||
const platformDescriptions = download.platformDescriptions
|
||||
|
@ -39,9 +39,7 @@ const platformDescriptions = download.platformDescriptions
|
|||
<main class="flex min-h-screen flex-col px-6 data-[os='windows']:bg-zen-blue">
|
||||
<div class="container relative mx-auto py-12">
|
||||
<div class="mb-6 mt-12 flex flex-col gap-4">
|
||||
<Description id="download-title" class="text-6xl font-bold"
|
||||
>{download.title}</Description
|
||||
>
|
||||
<Description id="download-title" class="text-6xl font-bold">{download.title}</Description>
|
||||
<Description class="max-w-xl text-pretty">
|
||||
{download.description}
|
||||
</Description>
|
||||
|
@ -191,9 +189,7 @@ const platformDescriptions = download.platformDescriptions
|
|||
</section>
|
||||
|
||||
<!-- Security Notice -->
|
||||
<div
|
||||
class="bg-opaicty-10 grid grid-cols-[auto,1fr] gap-4 rounded-2xl bg-subtle p-6"
|
||||
>
|
||||
<div class="bg-opaicty-10 grid grid-cols-[auto,1fr] gap-4 rounded-2xl bg-subtle p-6">
|
||||
<div class="h-fit rounded-xl bg-subtle p-3">
|
||||
<LockIcon class="h-5 w-5" />
|
||||
</div>
|
||||
|
@ -202,10 +198,7 @@ const platformDescriptions = download.platformDescriptions
|
|||
<h3 class="mb-2 text-lg font-medium">
|
||||
{download.securityNotice.title}
|
||||
</h3>
|
||||
<p
|
||||
class="text-muted-foreground"
|
||||
set:html={download.securityNotice.description}
|
||||
/>
|
||||
<p class="text-muted-foreground" set:html={download.securityNotice.description} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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'
|
||||
|
||||
/** 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 }) {
|
||||
// Just in case the release notes array is empty for whatever reason.
|
||||
const latestDate = releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date as string) : new Date()
|
||||
const latestDate =
|
||||
releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date as string) : new Date()
|
||||
|
||||
const rssData: RSSOptions = {
|
||||
title: 'Zen Browser Release Notes',
|
||||
|
@ -79,7 +81,10 @@ function formatReleaseNote(releaseNote: ReleaseNote) {
|
|||
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('🖌 Theme Changes', releaseNote.themeChanges)
|
||||
content += addReleaseNoteSection('⭐ Features', releaseNote.features)
|
||||
|
@ -119,7 +124,9 @@ function fixToReleaseNote(fix?: Exclude<ReleaseNote['fixes'], undefined>[number]
|
|||
return note
|
||||
}
|
||||
|
||||
function breakingChangeToReleaseNote(breakingChange?: Exclude<ReleaseNote['breakingChanges'], undefined>[number]) {
|
||||
function breakingChangeToReleaseNote(
|
||||
breakingChange?: Exclude<ReleaseNote['breakingChanges'], undefined>[number]
|
||||
) {
|
||||
if (typeof breakingChange === 'string') {
|
||||
return breakingChange
|
||||
}
|
||||
|
|
|
@ -12,11 +12,7 @@ const locale = getLocale(Astro)
|
|||
const { layout } = getUI(locale)
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={layout.index.title}
|
||||
description={layout.index.description}
|
||||
isHome
|
||||
>
|
||||
<Layout title={layout.index.title} description={layout.index.description} isHome>
|
||||
<main class="container">
|
||||
<Hero />
|
||||
<Features />
|
||||
|
|
|
@ -6,13 +6,13 @@ import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
|
|||
import InfoIcon from '~/icons/InfoIcon.astro'
|
||||
import Layout from '~/layouts/Layout.astro'
|
||||
import { getAllMods, getAuthorLink, getLocalizedDate } from '~/mods'
|
||||
import { getUI } from '~/utils/i18n'
|
||||
import { getPath, getUI } from '~/utils/i18n'
|
||||
import { getLocale, getOtherLocales } from '~/utils/i18n'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const mods = await getAllMods()
|
||||
return mods.flatMap((mod) => [
|
||||
...getOtherLocales().map((locale) => ({
|
||||
return mods.flatMap(mod => [
|
||||
...getOtherLocales().map(locale => ({
|
||||
params: {
|
||||
slug: mod.id,
|
||||
locale: locale,
|
||||
|
@ -44,7 +44,9 @@ const dates = {
|
|||
updatedAt: getLocalizedDate(mod.updatedAt),
|
||||
}
|
||||
|
||||
const locale = getLocale(Astro as { params: { locale?: string } })
|
||||
const locale = getLocale(Astro)
|
||||
|
||||
const getLocalePath = getPath(locale)
|
||||
|
||||
const {
|
||||
routes: {
|
||||
|
@ -58,8 +60,8 @@ const {
|
|||
description={slug.description.replace('{name}', mod.name)}
|
||||
ogImage={mod.image}
|
||||
>
|
||||
<main class="mt-6 2xl:mt-0">
|
||||
<div class="mx-auto mb-24 mt-12 flex flex-col gap-6 px-8 lg:mt-32 lg:w-1/2">
|
||||
<main class="container mt-6 2xl:mt-0">
|
||||
<div class="mx-auto mb-24 mt-12 flex flex-col gap-6 lg:mt-32">
|
||||
<div
|
||||
id="install-theme-error"
|
||||
class="flex flex-col items-center justify-center gap-2 rounded-xl bg-red-200 p-2 pl-4 md:flex-row md:justify-between dark:bg-red-700"
|
||||
|
@ -79,7 +81,7 @@ const {
|
|||
<ArrowRightIcon class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<BackButton />
|
||||
<BackButton id="back-button" href={getLocalePath('/mods')} />
|
||||
<div>
|
||||
<Description class="text-6xl font-bold">{mod.name}</Description>
|
||||
<Description>{mod.description}</Description>
|
||||
|
@ -93,43 +95,28 @@ const {
|
|||
<div class="flex flex-shrink-0 flex-col gap-2 font-normal">
|
||||
<p
|
||||
set:html={slug.createdBy
|
||||
.replace('{author}', mod.author)
|
||||
.replace('{version}', mod.version)
|
||||
.replace('{link}', getAuthorLink(mod.author))}
|
||||
/>
|
||||
<p
|
||||
set:html={slug.creationDate.replace('{createdAt}', dates.createdAt)}
|
||||
.replace(
|
||||
'{author}',
|
||||
`<a href="${getAuthorLink(mod.author)}" class="zen-link font-bold">${mod.author}</a>`
|
||||
)
|
||||
.replace('{version}', mod.version)}
|
||||
/>
|
||||
<p set:html={slug.creationDate.replace('{createdAt}', dates.createdAt)} />
|
||||
{
|
||||
dates.createdAt !== dates.updatedAt && (
|
||||
<p
|
||||
set:html={slug.latestUpdate.replace(
|
||||
'{updatedAt}',
|
||||
dates.updatedAt,
|
||||
)}
|
||||
/>
|
||||
<p set:html={slug.latestUpdate.replace('{updatedAt}', dates.updatedAt)} />
|
||||
)
|
||||
}
|
||||
{
|
||||
mod.homepage && (
|
||||
<a
|
||||
href={mod.homepage}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="zen-link"
|
||||
>
|
||||
<a href={mod.homepage} target="_blank" rel="noopener noreferrer" class="zen-link">
|
||||
{slug.visitModHomepage}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div class="flex flex-col sm:items-end">
|
||||
<Button
|
||||
class="hidden"
|
||||
id="install-theme"
|
||||
extra={{ 'zen-theme-id': mod.id }}
|
||||
isPrimary
|
||||
>
|
||||
<Button class="hidden" id="install-theme" extra={{ 'zen-theme-id': mod.id }} isPrimary>
|
||||
{slug.installMod}
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -149,3 +136,13 @@ const {
|
|||
<div></div>
|
||||
</main></Layout
|
||||
>
|
||||
|
||||
<script>
|
||||
const backButton = document.getElementById('back-button') as HTMLAnchorElement
|
||||
const search = window.location.search
|
||||
if (search.length > 0) {
|
||||
const searchParams = new URLSearchParams(search)
|
||||
const backLink = `${backButton.href}?${searchParams.toString()}`
|
||||
backButton.href = backLink
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
import Description from '~/components/Description.astro'
|
||||
import ModsList from '~/components/ModsList'
|
||||
import ModsList from '~/components/ModsList.astro'
|
||||
import { CONSTANT } from '~/constants'
|
||||
import Layout from '~/layouts/Layout.astro'
|
||||
import { getAllMods } from '~/mods'
|
||||
|
@ -30,7 +30,7 @@ const allMods = (await getAllMods()) || []
|
|||
<ModsList
|
||||
allMods={allMods}
|
||||
locale={locale ?? CONSTANT.I18N.DEFAULT_LOCALE}
|
||||
client:load
|
||||
translations={mods}
|
||||
/>
|
||||
</main>
|
||||
</Layout>
|
||||
|
|
|
@ -12,10 +12,7 @@ const {
|
|||
} = getUI(locale)
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={layout.privacyPolicy.title}
|
||||
description={layout.privacyPolicy.description}
|
||||
>
|
||||
<Layout title={layout.privacyPolicy.title} description={layout.privacyPolicy.description}>
|
||||
<main class="mx-auto mt-52 w-1/2 pb-24">
|
||||
<Title id="privacy-policy" class="xl:text-6xl">{privacyPolicy.title}</Title>
|
||||
<div class="ml-4 font-bold">{privacyPolicy.lastUpdated}</div>
|
||||
|
@ -26,54 +23,39 @@ const {
|
|||
<div class="mx-12 my-12 flex gap-4 font-bold">
|
||||
{privacyPolicy.sections.introduction.summary}
|
||||
</div>
|
||||
<Title
|
||||
class="mt-16 text-4xl font-bold"
|
||||
id="1-information-we-do-not-collect"
|
||||
>
|
||||
<Title class="mt-16 text-4xl font-bold" id="1-information-we-do-not-collect">
|
||||
{privacyPolicy.sections.noCollect.title}
|
||||
</Title>
|
||||
<p>{privacyPolicy.sections.noCollect.body}</p>
|
||||
<h3 class="mt-4 text-xl font-bold" id="-1-1-no-telemetry-">
|
||||
<strong class="font-bold"
|
||||
>{privacyPolicy.sections.noTelemetry.title}</strong
|
||||
>
|
||||
<strong class="font-bold">{privacyPolicy.sections.noTelemetry.title}</strong>
|
||||
</h3>
|
||||
<p>{privacyPolicy.sections.noTelemetry.body}</p>
|
||||
<p>{privacyPolicy.sections.noTelemetry.body2}</p>
|
||||
<h3 class="mt-4 text-xl font-bold" id="-1-2-no-personal-data-collection-">
|
||||
<strong class="font-bold"
|
||||
>{privacyPolicy.sections.noPersonalData.title}</strong
|
||||
>
|
||||
<strong class="font-bold">{privacyPolicy.sections.noPersonalData.title}</strong>
|
||||
</h3>
|
||||
<p>{privacyPolicy.sections.noPersonalData.body}</p>
|
||||
<h3 class="mt-4 text-xl font-bold" id="-1-4-no-third-party-tracking-">
|
||||
<strong class="font-bold"
|
||||
>{privacyPolicy.sections.noThirdParty.title}</strong
|
||||
>
|
||||
<strong class="font-bold">{privacyPolicy.sections.noThirdParty.title}</strong>
|
||||
</h3>
|
||||
<p>{privacyPolicy.sections.noThirdParty.body}</p>
|
||||
<h3 class="mt-4 text-xl font-bold" id="-1-3-no-third-party-tracking-">
|
||||
<strong class="font-bold"
|
||||
>{privacyPolicy.sections.externalConnections.title}</strong
|
||||
>
|
||||
<strong class="font-bold">{privacyPolicy.sections.externalConnections.title}</strong>
|
||||
</h3>
|
||||
<p>{privacyPolicy.sections.externalConnections.body}</p>
|
||||
<Title
|
||||
class="mt-16 text-4xl font-bold"
|
||||
id="2-information-stored-locally-on-your-device"
|
||||
>
|
||||
<Title class="mt-16 text-4xl font-bold" id="2-information-stored-locally-on-your-device">
|
||||
{privacyPolicy.sections.localStorage.title}
|
||||
</Title>
|
||||
<h3 class="mt-4 text-xl font-bold" id="-2-1-browsing-data-">
|
||||
<strong class="font-bold"
|
||||
>{privacyPolicy.sections.browsingData.title}</strong
|
||||
>
|
||||
<strong class="font-bold">{privacyPolicy.sections.browsingData.title}</strong>
|
||||
</h3>
|
||||
<p>{privacyPolicy.sections.browsingData.body}</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong class="font-bold">{privacyPolicy.sections.cookies.title}</strong
|
||||
>: {privacyPolicy.sections.cookies.body}
|
||||
<strong class="font-bold">{privacyPolicy.sections.cookies.title}</strong>: {
|
||||
privacyPolicy.sections.cookies.body
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
<strong class="font-bold">{privacyPolicy.sections.cache.title}</strong>: {
|
||||
|
@ -91,9 +73,7 @@ const {
|
|||
<p>{privacyPolicy.sections.sync.body}</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
class="zen-link"
|
||||
href="https://www.mozilla.org/en-US/privacy/mozilla-accounts/"
|
||||
<a class="zen-link" href="https://www.mozilla.org/en-US/privacy/mozilla-accounts/"
|
||||
>{privacyPolicy.sections.sync.link1}</a
|
||||
>
|
||||
</li>
|
||||
|
@ -120,9 +100,7 @@ const {
|
|||
{privacyPolicy.sections.control.title}
|
||||
</Title>
|
||||
<h3 class="mt-4 text-xl font-bold" id="-6-1-data-deletion-">
|
||||
<strong class="font-bold"
|
||||
>{privacyPolicy.sections.control.deletionTitle}</strong
|
||||
>
|
||||
<strong class="font-bold">{privacyPolicy.sections.control.deletionTitle}</strong>
|
||||
</h3>
|
||||
<p>{privacyPolicy.sections.control.deletionBody}</p>
|
||||
<Title class="mt-16 text-4xl font-bold" id="7-our-website-and-services">
|
||||
|
@ -130,24 +108,16 @@ const {
|
|||
</Title>
|
||||
<p>{privacyPolicy.sections.website.body}</p>
|
||||
<h3 class="mt-4 text-xl font-bold" id="-7-1-external-links-">
|
||||
<strong class="font-bold"
|
||||
>{privacyPolicy.sections.website.externalLinksTitle}</strong
|
||||
>
|
||||
<strong class="font-bold">{privacyPolicy.sections.website.externalLinksTitle}</strong>
|
||||
</h3>
|
||||
<p>{privacyPolicy.sections.website.externalLinksBody}</p>
|
||||
<Title
|
||||
class="mt-16 text-4xl font-bold"
|
||||
id="8-changes-to-this-privacy-policy"
|
||||
>
|
||||
<Title class="mt-16 text-4xl font-bold" id="8-changes-to-this-privacy-policy">
|
||||
{privacyPolicy.sections.changes.title}
|
||||
</Title>
|
||||
<p>
|
||||
{privacyPolicy.sections.changes.body}
|
||||
</p>
|
||||
<Title
|
||||
class="mt-16 text-4xl font-bold"
|
||||
id="9-other-telemetry-done-by-mozilla-firefox"
|
||||
>
|
||||
<Title class="mt-16 text-4xl font-bold" id="9-other-telemetry-done-by-mozilla-firefox">
|
||||
{privacyPolicy.sections.otherTelemetry.title}
|
||||
</Title>
|
||||
<p>
|
||||
|
@ -155,9 +125,7 @@ const {
|
|||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Please check <a
|
||||
class="zen-link"
|
||||
href="https://www.mozilla.org/en-US/privacy/"
|
||||
Please check <a class="zen-link" href="https://www.mozilla.org/en-US/privacy/"
|
||||
>{privacyPolicy.sections.otherTelemetry.firefoxPrivacyNotice}</a
|
||||
>
|
||||
{privacyPolicy.sections.otherTelemetry.forMoreInformation}
|
||||
|
|
|
@ -15,7 +15,7 @@ export async function getStaticPaths() {
|
|||
const i18nPaths = getI18nPaths()
|
||||
|
||||
return i18nPaths.flatMap(({ params: { locale } }) => [
|
||||
...releaseNotes.map((release) => ({
|
||||
...releaseNotes.map(release => ({
|
||||
params: { slug: release.version, locale },
|
||||
props: { ...release },
|
||||
})),
|
||||
|
|
|
@ -21,38 +21,32 @@ const {
|
|||
<main
|
||||
class="container flex h-full min-h-[1000px] flex-1 flex-col items-center justify-center py-4"
|
||||
>
|
||||
<div
|
||||
id="release-notes"
|
||||
class="py-42 flex min-h-screen gap-8 w-full flex-col justify-center"
|
||||
>
|
||||
<Description class="mt-48 text-6xl font-bold">Changelog</Description>
|
||||
<div id="release-notes" class="py-42 flex min-h-screen w-full flex-col justify-center gap-8">
|
||||
<Description class="mt-48 text-6xl font-bold">{releaseNotes.topSection.title}</Description>
|
||||
<p
|
||||
class="text-base opacity-55"
|
||||
set:html={releaseNotes.topSection.description.replaceAll(
|
||||
'{latestVersion}',
|
||||
releaseNotesData[0].version,
|
||||
releaseNotesData[0].version
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
class="mt-8 flex w-fit flex-col gap-4 sm:mr-0 sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="mt-8 flex w-fit flex-col gap-4 sm:mr-0 sm:flex-row sm:items-center">
|
||||
<Button class="flex" isPrimary href="/donate">
|
||||
{releaseNotes.list.support}
|
||||
</Button>
|
||||
<Button id="navigate-to-version" href="#" class="flex">
|
||||
<Button id="navigate-to-version" href="#" class="flex" localePath={false}>
|
||||
{releaseNotes.list.navigateToVersion}
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
releaseNotesTwilight.features.length ||
|
||||
releaseNotesTwilight.fixes.length ? (
|
||||
releaseNotesTwilight.features.length || releaseNotesTwilight.fixes.length ? (
|
||||
<ReleaseNoteItem {...releaseNotesTwilight} isTwilight />
|
||||
) : null
|
||||
}
|
||||
{releaseNotesData.map((notes: any) => <ReleaseNoteItem {...notes} />)}
|
||||
{releaseNotesData.map(notes => <ReleaseNoteItem {...notes} />)}
|
||||
</div>
|
||||
</main>
|
||||
<Button href="#" id="scroll-top" isPrimary class="fixed bottom-8 right-8">
|
||||
<Button id="scroll-top" isPrimary class="fixed bottom-8 right-8" onclick="window.scrollTo(0, 0)">
|
||||
<p class="hidden items-center gap-2 sm:flex">
|
||||
{releaseNotes.backToTop}
|
||||
<ArrowUpIcon aria-hidden="true" class="size-4" />
|
||||
|
@ -67,7 +61,7 @@ const {
|
|||
<ModalBody>
|
||||
<div id="version-list" class="flex flex-col gap-2 text-xl text-dark">
|
||||
{
|
||||
releaseNotesData.map((note) => (
|
||||
releaseNotesData.map(note => (
|
||||
<button
|
||||
aria-label={`Navigate to version ${note.version}`}
|
||||
class="w-full text-left transition-colors duration-150 hover:text-coral"
|
||||
|
@ -81,7 +75,7 @@ const {
|
|||
</ModalBody>
|
||||
</Modal>
|
||||
<script>
|
||||
import { openModal, closeModal } from 'free-astro-components'
|
||||
import { closeModal, openModal } from 'free-astro-components'
|
||||
|
||||
const scrollTopButton = document.getElementById('scroll-top')
|
||||
const versionButton = document.getElementById('navigate-to-version')
|
||||
|
@ -110,11 +104,9 @@ const {
|
|||
if (!version) return
|
||||
window.location.hash = version
|
||||
|
||||
const versionDetails = document
|
||||
.getElementById(version)
|
||||
?.getElementsByTagName('details')
|
||||
const versionDetails = document.getElementById(version)?.getElementsByTagName('details')
|
||||
if (versionDetails && versionDetails.length > 0) {
|
||||
Array.from(versionDetails).forEach((accordion) => {
|
||||
Array.from(versionDetails).forEach(accordion => {
|
||||
accordion.setAttribute('open', '')
|
||||
})
|
||||
}
|
||||
|
@ -131,7 +123,7 @@ const {
|
|||
versionButton?.addEventListener('click', openVersionModal)
|
||||
versionList?.addEventListener('click', navigateToVersion)
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && modal?.hasAttribute('open')) {
|
||||
closeModal(modal)
|
||||
// Remove scroll lock if present
|
||||
|
|
|
@ -14,10 +14,6 @@ const {
|
|||
|
||||
<Layout title={layout.welcome.title} description={layout.welcome.description}>
|
||||
<main class="container">
|
||||
<Features
|
||||
title1={welcome.title[0]}
|
||||
title2={welcome.title[1]}
|
||||
title3={welcome.title[2]}
|
||||
/>
|
||||
<Features titles={welcome.title} />
|
||||
</main>
|
||||
</Layout>
|
||||
|
|
|
@ -27,24 +27,14 @@ if (latestVersion.version.split('.').length > 2 && whatsNewText[1] !== latestVer
|
|||
}
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={layout.whatsNew.title.replace(
|
||||
'{latestVersion.version}',
|
||||
latestVersion.version,
|
||||
)}
|
||||
>
|
||||
<Layout title={layout.whatsNew.title.replace('{latestVersion.version}', latestVersion.version)}>
|
||||
<main
|
||||
class="xl:mt-22 container flex flex-col gap-12 py-12 xl:grid xl:min-h-[calc(100vh-12rem)] xl:grid-cols-[2fr_3fr]"
|
||||
>
|
||||
<div class="flex flex-col gap-8">
|
||||
<div>
|
||||
<Description class="text-5xl font-bold md:text-6xl"
|
||||
>{
|
||||
whatsNew.title.replace(
|
||||
'{latestVersion.version}',
|
||||
latestVersion.version,
|
||||
)
|
||||
}</Description
|
||||
>{whatsNew.title.replace('{latestVersion.version}', latestVersion.version)}</Description
|
||||
>
|
||||
<Description>{latestVersion.date}</Description>
|
||||
</div>
|
||||
|
@ -52,10 +42,7 @@ if (latestVersion.version.split('.').length > 2 && whatsNewText[1] !== latestVer
|
|||
<Fragment set:html={whatsNewText[0].replace(/\n/g, '<br>')} />
|
||||
</div>
|
||||
<ul class="hidden list-disc flex-col gap-2 xl:container xl:flex">
|
||||
<a
|
||||
href="https://github.com/zen-browser/desktop/issues/new/choose"
|
||||
target="_blank"
|
||||
>
|
||||
<a href="https://github.com/zen-browser/desktop/issues/new/choose" target="_blank">
|
||||
<li>
|
||||
<Description class="text-base font-bold">
|
||||
{whatsNew.reportIssue}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import releaseNotesStable from './release-notes/stable.json'
|
||||
|
||||
interface FixWithIssue {
|
||||
type FixWithIssue = {
|
||||
description: string
|
||||
issue?: number
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ type Fix = string | FixWithIssue
|
|||
|
||||
export type BreakingChange = string | { description: string; link: string }
|
||||
|
||||
export interface ReleaseNote {
|
||||
export type ReleaseNote = {
|
||||
version: string
|
||||
date?: string // optional for twilight
|
||||
extra?: string
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"date": "12/07/2024",
|
||||
"extra": "This release is the second alpha release of the 1.0.0-alpha series. It includes a lot of bug fixes and improvements given the feedback we received from the first alpha release. This release is still not considered stable, but it's a big step towards the first stable release.",
|
||||
"features": ["Added support for macOS aaarch64!", "Some performance improvements"],
|
||||
"features": ["Added support for macOS aaarch64!", "Some performance improvements"],
|
||||
"fixes": [
|
||||
{
|
||||
"description": "Fixed rounded corners of browser views for some websites",
|
||||
|
@ -352,6 +353,7 @@
|
|||
"date": "05/08/2024",
|
||||
"extra": "This is a smaller release to fix some bugs and improve some small details.\n\nIm going to try doing more frequent releases from now on, see how it goes.",
|
||||
"features": ["Allow to remember sidebar width even after collapsing it."],
|
||||
"features": ["Allow to remember sidebar width even after collapsing it."],
|
||||
"fixes": [
|
||||
{
|
||||
"description": "Task Manager Icon Missing in Flatpak Version",
|
||||
|
@ -601,7 +603,10 @@
|
|||
"version": "1.0.0-a.30",
|
||||
"date": "26/08/2024",
|
||||
"extra": "This release is the thirtieth alpha release of the 1.0.0-alpha series.",
|
||||
"features": ["Added support for 24 more languages!", "Update installed mods from the browser settings"],
|
||||
"features": [
|
||||
"Added support for 24 more languages!",
|
||||
"Update installed mods from the browser settings"
|
||||
],
|
||||
"fixes": [
|
||||
{
|
||||
"description": "Letterboxing option is missing",
|
||||
|
@ -887,6 +892,7 @@
|
|||
}
|
||||
],
|
||||
"breakingChanges": ["Removed Galaxy and Dream mods", "Removed the 'legacy-toolbar' preference"],
|
||||
"breakingChanges": ["Removed Galaxy and Dream mods", "Removed the 'legacy-toolbar' preference"],
|
||||
"themeChanges": [
|
||||
"Themes will now be able to have string and number values",
|
||||
"The configuration schema for mods has been updated. All current mods have been updated automatically."
|
||||
|
@ -898,6 +904,7 @@
|
|||
"workflowId": 11000317603,
|
||||
"extra": "This update addresses some significant issues with the previous release.\n\nWe appreciate your patience and support!",
|
||||
"features": ["Added a new system for handling keyboard shortcuts"],
|
||||
"features": ["Added a new system for handling keyboard shortcuts"],
|
||||
"fixes": [
|
||||
{
|
||||
"description": "The New Tab button is not visible",
|
||||
|
@ -924,7 +931,11 @@
|
|||
"date": "24/09/2024",
|
||||
"workflowId": 11020784612,
|
||||
"extra": "This update is a small patch to fix some issues that weren't addressed in the previous release!",
|
||||
"features": ["Moved application menu button to the right", "Added new shortcuts", "Collapsed tab sidebar is now smaller"],
|
||||
"features": [
|
||||
"Moved application menu button to the right",
|
||||
"Added new shortcuts",
|
||||
"Collapsed tab sidebar is now smaller"
|
||||
],
|
||||
"fixes": [
|
||||
{
|
||||
"description": "Fixed issue with hovering over window control buttons (macOS)"
|
||||
|
@ -949,7 +960,9 @@
|
|||
"Improved Expand Tabs on Hover layout"
|
||||
],
|
||||
"themeChanges": ["Toggle inputs will not use the themed tertiary color"],
|
||||
"breakingChanges": ["The keyboard shortcuts will be overriden by the defaults ones in this update"],
|
||||
"breakingChanges": [
|
||||
"The keyboard shortcuts will be overriden by the defaults ones in this update"
|
||||
],
|
||||
"fixes": [
|
||||
{
|
||||
"description": "Fixed Firefox add-ons not updating",
|
||||
|
@ -1121,7 +1134,10 @@
|
|||
"description": "Fixed about page linking 'global Community' to a Mozilla page"
|
||||
}
|
||||
],
|
||||
"features": ["About page will now display the Firefox version used", "Disabled forcing container grouping for workspaces"]
|
||||
"features": [
|
||||
"About page will now display the Firefox version used",
|
||||
"Disabled forcing container grouping for workspaces"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.1-a.11",
|
||||
|
@ -1234,6 +1250,7 @@
|
|||
}
|
||||
],
|
||||
"breakingChanges": ["Removed Show Expand Button option from settings"],
|
||||
"breakingChanges": ["Removed Show Expand Button option from settings"],
|
||||
"themeChanges": [
|
||||
"The variable '--zen-main-browser-background' will now contain the generated gradient",
|
||||
"Added the 'unread' attribute for background tabs that haven't been accessed yet"
|
||||
|
@ -1258,7 +1275,9 @@
|
|||
"description": "Fixed sidebar webpanels being in a darker contrast"
|
||||
}
|
||||
],
|
||||
"features": ["Added a confirmation dialog when the gradient generator has successfully saved the gradient"]
|
||||
"features": [
|
||||
"Added a confirmation dialog when the gradient generator has successfully saved the gradient"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.1-a.15",
|
||||
|
@ -1304,6 +1323,7 @@
|
|||
}
|
||||
],
|
||||
"breakingChanges": ["Changed the default layout of the customizable UI buttons"],
|
||||
"breakingChanges": ["Changed the default layout of the customizable UI buttons"],
|
||||
"features": [
|
||||
"Added Zen Glance!",
|
||||
"Updated to the latest stable version of Firefox (132.0)",
|
||||
|
@ -1328,8 +1348,7 @@
|
|||
{
|
||||
"description": "Fixed wrong aligment on glance action buttons"
|
||||
}
|
||||
],
|
||||
"features": ["No new features, sorry"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.1-a.17",
|
||||
|
@ -2097,7 +2116,10 @@
|
|||
"date": "30/01/2025",
|
||||
"workflowId": 13062083313,
|
||||
"extra": "Quick fix for a critical bug that was introduced in the previous release.",
|
||||
"fixes": ["Fixed the browser not opening when having multiple windows", "Fixed macos fullscreen having a weird shadow"]
|
||||
"fixes": [
|
||||
"Fixed the browser not opening when having multiple windows",
|
||||
"Fixed macos fullscreen having a weird shadow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.7.5b",
|
||||
|
@ -2154,7 +2176,9 @@
|
|||
"Fixed opening glance tabs on essentials messing up the sidebar",
|
||||
"Fixed pinned tabs appearing on normal container after a restart"
|
||||
],
|
||||
"features": ["Tabs can now be dragged into pinned tabs by dragging them into the workspace indicator"],
|
||||
"features": [
|
||||
"Tabs can now be dragged into pinned tabs by dragging them into the workspace indicator"
|
||||
],
|
||||
"workflowId": 13209591935,
|
||||
"date": "08/02/2025"
|
||||
},
|
||||
|
@ -2244,6 +2268,7 @@
|
|||
"Other small fixes for compact mode not animating properly"
|
||||
],
|
||||
"features": ["localhost and http URL will no longer be trimmed in single toolbar layout"],
|
||||
"features": ["localhost and http URL will no longer be trimmed in single toolbar layout"],
|
||||
"workflowId": 13530880093,
|
||||
"date": "25/02/2025"
|
||||
},
|
||||
|
@ -2367,6 +2392,7 @@
|
|||
"version": "1.10.3b",
|
||||
"extra": "Terribly sorry for the frequent updates, this emergency release simply updates Firefox, fixing a critical vulnerability in chromium's sandboxing system. You can read more here:\n\n<a href='https://cyberinsider.com/firefox-says-its-vulnerable-to-chromes-zero-day-used-in-espionage-attacks/'>https://cyberinsider.com/firefox-says-its-vulnerable-to-chromes-zero-day-used-in-espionage-attacks/</a>",
|
||||
"features": ["Updated Firefox to 136.0.4"],
|
||||
"features": ["Updated Firefox to 136.0.4"],
|
||||
"workflowId": 14109635630,
|
||||
"date": "27/03/2025"
|
||||
},
|
||||
|
@ -2667,6 +2693,7 @@
|
|||
"version": "1.12.5b",
|
||||
"extra": "",
|
||||
"fixes": ["A weird shadow with the URL bar.", "'All tabs' button appearing unexpectedly."],
|
||||
"fixes": ["A weird shadow with the URL bar.", "'All tabs' button appearing unexpectedly."],
|
||||
"features": [],
|
||||
"workflowId": 15024223699,
|
||||
"date": "14/05/2025"
|
||||
|
@ -2684,7 +2711,10 @@
|
|||
"'All tabs' menu not showing any text when collapsed toolbar is enabled."
|
||||
],
|
||||
"security": "https://www.mozilla.org/en-US/security/advisories/mfsa2025-36/",
|
||||
"features": ["Updated to Firefox 138.0.4", "Better compact mode support for multiple toolbars."],
|
||||
"features": [
|
||||
"Updated to Firefox 138.0.4",
|
||||
"Better compact mode support for multiple toolbars."
|
||||
],
|
||||
"knownIssues": ["Selecting a tab on private mode doesn't scroll to make the tab visible."],
|
||||
"themeChanges": [
|
||||
"Changed the layout of workspaces and their icons internally to provide a more stable layout that doesn't require floating elements. We finally managed to get it to how we wanted it to be, so it will change less in the future."
|
||||
|
@ -2701,10 +2731,11 @@
|
|||
"extra": "",
|
||||
"fixes": [
|
||||
"Window control buttons not appearing on hover.",
|
||||
"Double clicking on the tab strip not opening a new tab.",
|
||||
"Double-clicking on the tab strip not opening a new tab.",
|
||||
"Dragging tabs with Glance open acting weird."
|
||||
],
|
||||
"features": ["A much better transition between workspaces gradients."],
|
||||
"features": ["A much better transition between workspaces gradients."],
|
||||
"workflowId": 15164169941,
|
||||
"image": false,
|
||||
"date": "21/05/2025"
|
||||
|
@ -2715,9 +2746,10 @@
|
|||
"fixes": [
|
||||
"Issues with custom colors on the gradient generator.",
|
||||
"Site notifications not appearing unless hovering on the top part of the screen.",
|
||||
"Some issues with glance + split view"
|
||||
"Some issues when using Glance and split view simultaneously."
|
||||
],
|
||||
"features": ["Private windows will now have 'private spaces'"],
|
||||
"features": ["Private windows will now have 'private spaces'"],
|
||||
"workflowId": 15193769814,
|
||||
"image": false,
|
||||
"date": "23/05/2025"
|
||||
|
@ -2725,7 +2757,10 @@
|
|||
{
|
||||
"version": "1.12.9b",
|
||||
"extra": "This update includes some fixes, security updates, and a few changes! We've included both the updates for firefox 139.0 and 139.0.1 into the same release, since firefox 139.0 had some rendering issues on windows for NVIDIA users, we've waiting a day to release this update to ensure that the issues were resolved.",
|
||||
"fixes": ["Pinned and essential tabs not restoring their icons correctly", "Other issues with split views and glance"],
|
||||
"fixes": [
|
||||
"Pinned and essential tabs not restoring their icons correctly",
|
||||
"Other issues with split views and glance"
|
||||
],
|
||||
"changes": [
|
||||
"Unloaded tabs won't be dimmed unless they are explicitly discarded",
|
||||
"Context menu icons have been removed to provide a cleaner look and feel"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import Button from '~/components/Button.astro'
|
||||
|
||||
describe('<Button />', () => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import ButtonCard from '~/components/download/ButtonCard.astro'
|
||||
|
||||
describe('<ButtonCard />', () => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import PlatformDownload from '~/components/download/PlatformDownload.astro'
|
||||
|
||||
const mockIcon = ['<svg></svg>']
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getReleasesWithChecksums } from '~/components/download/release-data'
|
||||
|
||||
describe('getReleasesWithChecksums', () => {
|
||||
|
@ -10,7 +11,7 @@ describe('getReleasesWithChecksums', () => {
|
|||
'zen.linux-x86_64.tar.xz': 'linx86sum',
|
||||
'zen.linux-aarch64.tar.xz': 'linaarchsum',
|
||||
}
|
||||
const releases = getReleasesWithChecksums(checksums)
|
||||
const releases = getReleasesWithChecksums('en')(checksums)
|
||||
expect(releases.macos.universal.checksum).toBe('macsum')
|
||||
expect(releases.windows.x86_64.checksum).toBe('winsum')
|
||||
expect(releases.windows.arm64.checksum).toBe('winarmsum')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test } from '@playwright/test'
|
||||
import type { BrowserContextOptions, Page } from '@playwright/test'
|
||||
import { expect, test, type BrowserContextOptions, type Page } from '@playwright/test'
|
||||
|
||||
import { getReleasesWithChecksums } from '~/components/download/release-data'
|
||||
import { CONSTANT } from '~/constants'
|
||||
|
||||
|
@ -11,10 +11,6 @@ const getPlatformSection = (page: Page, platform: string) =>
|
|||
const getPlatformButton = (page: Page, platform: string) =>
|
||||
page.locator(`button.platform-selector[data-platform='${platform}']`)
|
||||
|
||||
// Helper to get the platform download link
|
||||
const getPlatformDownloadLink = (page: Page, platform: string, label: string) =>
|
||||
page.locator(`#${platform}-downloads .download-link:has-text('${label}')`)
|
||||
|
||||
const platformConfigs: { name: string; userAgent: string; platform: string }[] = [
|
||||
{
|
||||
name: 'windows',
|
||||
|
@ -30,7 +26,8 @@ const platformConfigs: { name: string; userAgent: string; platform: string }[] =
|
|||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
]
|
||||
|
@ -48,7 +45,7 @@ test.describe('Download page default tab per platform', () => {
|
|||
await expect(getPlatformSection(page, name)).toBeVisible()
|
||||
await expect(getPlatformButton(page, name)).toHaveAttribute('data-active', 'true')
|
||||
// Other platforms should not be active
|
||||
for (const other of platformConfigs.filter((p) => p.name !== name)) {
|
||||
for (const other of platformConfigs.filter(p => p.name !== name)) {
|
||||
await expect(getPlatformSection(page, other.name)).toBeHidden()
|
||||
await expect(getPlatformButton(page, other.name)).not.toHaveAttribute('data-active', 'true')
|
||||
}
|
||||
|
@ -66,22 +63,30 @@ test.describe('Download page platform detection and tab switching', () => {
|
|||
await expect(getPlatformSection(page, platform)).toBeVisible()
|
||||
await expect(getPlatformButton(page, platform)).toHaveAttribute('data-active', 'true')
|
||||
// other platform sections should be hidden
|
||||
for (const otherPlatform of platforms.filter((p) => p !== platform)) {
|
||||
for (const otherPlatform of platforms.filter(p => p !== platform)) {
|
||||
await expect(getPlatformSection(page, otherPlatform)).toBeHidden()
|
||||
await expect(getPlatformButton(page, otherPlatform)).not.toHaveAttribute('data-active', 'true')
|
||||
await expect(getPlatformButton(page, otherPlatform)).not.toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Download page download links', () => {
|
||||
const releases = getReleasesWithChecksums(CONSTANT.CHECKSUMS)
|
||||
const releases = getReleasesWithChecksums('en')(CONSTANT.CHECKSUMS)
|
||||
|
||||
function getPlatformLinks(releases: ReturnType<typeof getReleasesWithChecksums>) {
|
||||
type Releases = ReturnType<ReturnType<typeof getReleasesWithChecksums>>
|
||||
function getPlatformLinks(releases: Releases) {
|
||||
return {
|
||||
mac: [releases.macos.universal],
|
||||
windows: [releases.windows.x86_64, releases.windows.arm64],
|
||||
linux: [releases.linux.x86_64.tarball, releases.linux.aarch64.tarball, releases.linux.flathub.all],
|
||||
linux: [
|
||||
releases.linux.x86_64.tarball,
|
||||
releases.linux.aarch64.tarball,
|
||||
releases.linux.flathub.all,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,7 +97,9 @@ test.describe('Download page download links', () => {
|
|||
await page.waitForLoadState('domcontentloaded')
|
||||
for (const platform of platforms) {
|
||||
await getPlatformButton(page, platform).click()
|
||||
for (const { label, link } of platformLinkSelectors[platform as keyof typeof platformLinkSelectors]) {
|
||||
for (const { label, link } of platformLinkSelectors[
|
||||
platform as keyof typeof platformLinkSelectors
|
||||
]) {
|
||||
const downloadLink = page.locator(`#${platform}-downloads .download-link[href="${link}"]`)
|
||||
await expect(downloadLink).toContainText(label)
|
||||
await expect(downloadLink).toHaveAttribute('href', link)
|
||||
|
|
|
@ -5,7 +5,8 @@ test('clicking back button navigates to previous page', async ({ page }) => {
|
|||
const currentUrl = page.url()
|
||||
const modCards = await page.locator('.mod-card').all()
|
||||
await modCards[0].click()
|
||||
await page.getByRole('button', { name: 'Back' }).click()
|
||||
await page.waitForURL('/mods/*')
|
||||
await page.getByTestId('back-button').click()
|
||||
await page.waitForURL(currentUrl)
|
||||
expect(page.url()).toStrictEqual(currentUrl)
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test'
|
|||
test('all routes do not return 404', async ({ page }) => {
|
||||
const routes = ['/', '/welcome', '/about', '/privacy-policy', '/download', '/donate', '/whatsnew']
|
||||
for (const route of routes) {
|
||||
const response = await page.goto(route)
|
||||
const response = await page.goto(route, { waitUntil: 'domcontentloaded' })
|
||||
expect(response?.status()).not.toBe(404)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { vi } from 'vitest'
|
||||
|
||||
import translation from '~/i18n/en/translation.json'
|
||||
|
||||
vi.mock('~/utils/i18n', () => ({
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { GetStaticPaths } from 'astro'
|
||||
import { type AstroGlobal, type GetStaticPaths } from 'astro'
|
||||
|
||||
import { CONSTANT } from '~/constants'
|
||||
import UI_EN from '~/i18n/en/translation.json'
|
||||
import { type UIProps } from '~/constants/i18n'
|
||||
|
||||
/**
|
||||
* Represents the available locales in the application
|
||||
* @typedef {string} Locale
|
||||
*/
|
||||
export type Locale = (typeof locales)[number]
|
||||
|
||||
|
@ -13,122 +13,121 @@ export type Locale = (typeof locales)[number]
|
|||
* @param {Locale} [locale] - The current locale
|
||||
* @returns {function(string): string} A function that transforms paths based on the locale
|
||||
*/
|
||||
export const getPath = (locale?: Locale) => (path: string) => {
|
||||
if (locale && locale !== CONSTANT.I18N.DEFAULT_LOCALE && !path.startsWith(`/${locale}`)) {
|
||||
return `/${locale}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
export const getPath =
|
||||
(locale?: Locale): ((arg0: string) => string) =>
|
||||
(path: string) => {
|
||||
if (locale && locale !== CONSTANT.I18N.DEFAULT_LOCALE && !path.startsWith(`/${locale}`)) {
|
||||
return `/${locale}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
}
|
||||
return path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the current locale from Astro's params, defaulting to the default locale
|
||||
* @param {Object} Astro - Astro's context object
|
||||
* @param {Object} [Astro.params] - Routing parameters
|
||||
* @param {string} [Astro.params.locale] - The current locale parameter
|
||||
* @returns {Locale} The determined locale
|
||||
* Retrieves the current locale from the Astro object.
|
||||
*
|
||||
* @param Astro - The Astro object containing the current locale information
|
||||
* @param Astro.currentLocale - The current locale string from Astro
|
||||
* @returns The current locale cast as a Locale type
|
||||
*/
|
||||
export const getLocale = (Astro: { params?: { locale?: string } }) => {
|
||||
if (Astro.params?.locale) {
|
||||
return Astro.params.locale as Locale
|
||||
}
|
||||
return CONSTANT.I18N.DEFAULT_LOCALE as Locale
|
||||
export const getLocale = (Astro: AstroGlobal): Locale => {
|
||||
return Astro.currentLocale as Locale
|
||||
}
|
||||
|
||||
/**
|
||||
* List of all supported locales
|
||||
* @type {Locale[]}
|
||||
*/
|
||||
export const locales = CONSTANT.I18N.LOCALES.map(({ value }) => value)
|
||||
|
||||
/**
|
||||
* List of locales excluding the default locale
|
||||
* @type {Locale[]}
|
||||
*/
|
||||
const otherLocales = CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE)
|
||||
const otherLocales = CONSTANT.I18N.LOCALES.filter(
|
||||
({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE
|
||||
).map(({ value }) => value)
|
||||
|
||||
/**
|
||||
* Retrieves locales other than the default locale
|
||||
* @returns {Locale[]} Array of non-default locales
|
||||
*/
|
||||
export const getOtherLocales = () => otherLocales
|
||||
|
||||
/**
|
||||
* Type definition for UI translations based on the English translation
|
||||
* @typedef {Object} UI
|
||||
*/
|
||||
export type UI = typeof UI_EN
|
||||
|
||||
/**
|
||||
* Mapping of locales to their UI translation objects
|
||||
* @type {Object.<Locale, UI>}
|
||||
*/
|
||||
export const ui = { en: UI_EN }
|
||||
export const getOtherLocales = (): Locale[] => otherLocales
|
||||
|
||||
/**
|
||||
* Retrieves UI translations for a given locale, merging with default translations
|
||||
* @param {Locale} [locale] - The target locale for translations
|
||||
* @returns {UI} Merged UI translations
|
||||
*/
|
||||
export const getUI = (locale?: Locale | string): UI => {
|
||||
export const getUI = (locale?: Locale | string): UIProps => {
|
||||
const validLocale = locales.includes(locale as Locale) ? locale : CONSTANT.I18N.DEFAULT_LOCALE
|
||||
const defaultUI = ui[CONSTANT.I18N.DEFAULT_LOCALE]
|
||||
const localeUI = ui[validLocale as Locale]
|
||||
const defaultUI = CONSTANT.I18N.LOCALES.find(
|
||||
({ value }) => value === CONSTANT.I18N.DEFAULT_LOCALE
|
||||
)?.ui
|
||||
const localeUI = CONSTANT.I18N.LOCALES.find(({ value }) => value === validLocale)?.ui
|
||||
|
||||
/**
|
||||
* Recursively merges two objects, with the override object taking precedence
|
||||
* @template T
|
||||
* @param {T} defaultObj - The default object to merge from
|
||||
* @param {Partial<T>} overrideObj - The object to merge over the default
|
||||
* @returns {T} The deeply merged object
|
||||
*/
|
||||
function deepMerge<T extends object>(defaultObj: T, overrideObj: Partial<T>): T {
|
||||
// Handle non-object cases
|
||||
if (typeof defaultObj !== 'object' || defaultObj === null) {
|
||||
return (overrideObj ?? defaultObj) as T
|
||||
}
|
||||
if (typeof overrideObj !== 'object' || overrideObj === null) {
|
||||
return (overrideObj ?? defaultObj) as T
|
||||
}
|
||||
|
||||
// Create a new object or array based on the default object's type
|
||||
const result = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj }
|
||||
|
||||
// Merge properties from the default object
|
||||
for (const key of Object.keys(defaultObj) as Array<keyof T>) {
|
||||
const defaultValue = defaultObj[key]
|
||||
const overrideValue = overrideObj[key]
|
||||
|
||||
// Recursively merge nested objects
|
||||
if (
|
||||
defaultValue !== null &&
|
||||
overrideValue !== null &&
|
||||
typeof defaultValue === 'object' &&
|
||||
typeof overrideValue === 'object'
|
||||
// Helper to recursively check for missing keys
|
||||
function checkMismatch(
|
||||
defaultObj: UIProps,
|
||||
localeObj: Partial<UIProps> = {},
|
||||
path: string[] = []
|
||||
): void {
|
||||
if (typeof defaultObj !== 'object' || defaultObj === null) return
|
||||
for (const key of Object.keys(defaultObj) as (keyof UIProps)[]) {
|
||||
if (!(key in localeObj)) {
|
||||
console.error(
|
||||
`[i18n] Missing translation key: ${[...path, key as string].join('.')} in locale '\x1b[1m${validLocale}\x1b[0m'. See src/i18n/${validLocale}/translation.json`
|
||||
)
|
||||
} else if (
|
||||
typeof defaultObj[key] === 'object' &&
|
||||
defaultObj[key] !== null &&
|
||||
typeof localeObj[key] === 'object' &&
|
||||
localeObj[key] !== null
|
||||
) {
|
||||
// Type assertion to handle nested merging
|
||||
;(result as Record<keyof T, unknown>)[key] = deepMerge(defaultValue as object, overrideValue as Partial<object>)
|
||||
} else if (overrideValue !== undefined) {
|
||||
// Override with the new value if it exists
|
||||
;(result as Record<keyof T, unknown>)[key] = overrideValue
|
||||
// @ts-expect-error: recursive structure
|
||||
checkMismatch(defaultObj[key], localeObj[key], [...path, key as string])
|
||||
}
|
||||
}
|
||||
|
||||
// Add any new properties from overrideObj
|
||||
for (const key of Object.keys(overrideObj) as Array<keyof T>) {
|
||||
if (!(key in defaultObj)) {
|
||||
;(result as Record<keyof T, unknown>)[key] = overrideObj[key]
|
||||
}
|
||||
}
|
||||
|
||||
return result as T
|
||||
}
|
||||
|
||||
return deepMerge(defaultUI, localeUI)
|
||||
// Deep merge: localeUI overrides defaultUI, fallback to defaultUI for missing keys
|
||||
function deepMerge(defaultObj: UIProps, localeObj: Partial<UIProps> = {}): UIProps {
|
||||
if (typeof defaultObj !== 'object' || defaultObj === null) return defaultObj
|
||||
if (typeof localeObj !== 'object' || localeObj === null) return defaultObj
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: any = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj }
|
||||
for (const key of Object.keys(defaultObj) as (keyof UIProps)[]) {
|
||||
if (key in localeObj) {
|
||||
if (
|
||||
typeof defaultObj[key] === 'object' &&
|
||||
defaultObj[key] !== null &&
|
||||
typeof localeObj[key] === 'object' &&
|
||||
localeObj[key] !== null
|
||||
) {
|
||||
// @ts-expect-error: recursive structure
|
||||
result[key] = deepMerge(defaultObj[key], localeObj[key])
|
||||
} else {
|
||||
result[key] = localeObj[key]
|
||||
}
|
||||
} else {
|
||||
result[key] = defaultObj[key]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
if (!defaultUI) {
|
||||
throw new Error('Default UI translation is missing!')
|
||||
}
|
||||
|
||||
if (localeUI && validLocale !== CONSTANT.I18N.DEFAULT_LOCALE) {
|
||||
checkMismatch(defaultUI, localeUI)
|
||||
return deepMerge(defaultUI, localeUI) as UIProps
|
||||
}
|
||||
|
||||
// If localeUI is undefined or locale is default, just return defaultUI
|
||||
return defaultUI
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates static paths for internationalization
|
||||
* @type {GetStaticPaths}
|
||||
* @returns {Array} An array of static paths for different locales
|
||||
*/
|
||||
export const getStaticPaths = (() => {
|
||||
|
@ -137,12 +136,14 @@ export const getStaticPaths = (() => {
|
|||
params: { locale: undefined },
|
||||
props: { locale: CONSTANT.I18N.DEFAULT_LOCALE },
|
||||
},
|
||||
...CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE).map(({ value }) => ({
|
||||
params: { locale: value },
|
||||
props: {
|
||||
locale: value,
|
||||
},
|
||||
})),
|
||||
...CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE).map(
|
||||
({ value }) => ({
|
||||
params: { locale: value },
|
||||
props: {
|
||||
locale: value,
|
||||
},
|
||||
})
|
||||
),
|
||||
]
|
||||
}) satisfies GetStaticPaths
|
||||
|
||||
|
@ -150,6 +151,6 @@ export const getStaticPaths = (() => {
|
|||
* Retrieves all available locales, including both default and non-default
|
||||
* @returns {Locale[]} Combined array of all locales
|
||||
*/
|
||||
export const getLocales = () => {
|
||||
export const getLocales = (): Locale[] => {
|
||||
return [...locales, ...otherLocales]
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"jsxImportSource": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue