mirror of
https://github.com/zen-browser/www.git
synced 2025-07-08 09:20:00 +02:00
Merge f615a43af5
into 1937be58a6
This commit is contained in:
commit
ab0873f3a9
76 changed files with 7794 additions and 2749 deletions
28
.eslint/astro.ts
Normal file
28
.eslint/astro.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { type Linter } from 'eslint'
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
||||
|
||||
import { astroFiles } from './shared'
|
||||
|
||||
export const astroConfig: Linter.Config = {
|
||||
name: 'eslint/astro',
|
||||
files: astroFiles,
|
||||
plugins: {
|
||||
'jsx-a11y': jsxA11y,
|
||||
},
|
||||
rules: {
|
||||
// Astro specific adjustments
|
||||
'@typescript-eslint/no-unused-vars': 'off', // Astro components can have unused props
|
||||
'import/no-unresolved': 'off',
|
||||
'no-undef': 'off', // Astro has global variables like Astro
|
||||
|
||||
// A11y rules for Astro
|
||||
'jsx-a11y/alt-text': 'error',
|
||||
'jsx-a11y/anchor-has-content': 'error',
|
||||
'jsx-a11y/anchor-is-valid': 'error',
|
||||
'jsx-a11y/click-events-have-key-events': 'error',
|
||||
'jsx-a11y/interactive-supports-focus': 'error',
|
||||
'jsx-a11y/no-redundant-roles': 'error',
|
||||
'jsx-a11y/img-redundant-alt': 'error',
|
||||
'jsx-a11y/no-access-key': 'error',
|
||||
},
|
||||
}
|
73
.eslint/base.ts
Normal file
73
.eslint/base.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { type Linter } from 'eslint'
|
||||
|
||||
import { sharedFiles } from './shared'
|
||||
|
||||
export const baseConfig: Linter.Config = {
|
||||
name: 'eslint/base',
|
||||
files: sharedFiles,
|
||||
rules: {
|
||||
'constructor-super': 'error',
|
||||
'for-direction': 'error',
|
||||
'getter-return': 'error',
|
||||
'no-async-promise-executor': 'error',
|
||||
'no-case-declarations': 'error',
|
||||
'no-class-assign': 'error',
|
||||
'no-compare-neg-zero': 'error',
|
||||
'no-cond-assign': 'error',
|
||||
'no-const-assign': 'error',
|
||||
'no-constant-binary-expression': 'error',
|
||||
'no-constant-condition': 'error',
|
||||
'no-control-regex': 'error',
|
||||
'no-debugger': 'error',
|
||||
'no-delete-var': 'error',
|
||||
'no-dupe-args': 'error',
|
||||
'no-dupe-class-members': 'error',
|
||||
'no-dupe-else-if': 'error',
|
||||
'no-dupe-keys': 'error',
|
||||
'no-duplicate-case': 'error',
|
||||
'no-empty-character-class': 'error',
|
||||
'no-empty-pattern': 'error',
|
||||
'no-empty-static-block': 'error',
|
||||
'no-ex-assign': 'error',
|
||||
'no-fallthrough': 'error',
|
||||
'no-func-assign': 'error',
|
||||
'no-global-assign': 'error',
|
||||
'no-import-assign': 'error',
|
||||
'no-invalid-regexp': 'error',
|
||||
'no-loss-of-precision': 'error',
|
||||
'no-misleading-character-class': 'error',
|
||||
'no-new-native-nonconstructor': 'error',
|
||||
'no-nonoctal-decimal-escape': 'error',
|
||||
'no-obj-calls': 'error',
|
||||
'no-octal': 'error',
|
||||
'no-redeclare': 'error',
|
||||
'no-regex-spaces': 'error',
|
||||
'no-self-assign': 'error',
|
||||
'no-setter-return': 'error',
|
||||
'no-this-before-super': 'error',
|
||||
'no-undef': 'error',
|
||||
'no-unexpected-multiline': 'error',
|
||||
'no-unreachable': 'error',
|
||||
'no-unsafe-finally': 'error',
|
||||
'no-unsafe-negation': 'error',
|
||||
'no-unsafe-optional-chaining': 'error',
|
||||
'no-unused-labels': 'error',
|
||||
'no-unused-private-class-members': 'error',
|
||||
'no-useless-backreference': 'error',
|
||||
'no-useless-catch': 'error',
|
||||
'no-var': 'error',
|
||||
'no-with': 'error',
|
||||
'require-yield': 'error',
|
||||
'use-isnan': 'error',
|
||||
'valid-typeof': 'error',
|
||||
|
||||
// Additional base rules
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
'prefer-const': 'error',
|
||||
'object-shorthand': 'error',
|
||||
'prefer-template': 'error',
|
||||
curly: ['error', 'all'],
|
||||
eqeqeq: ['error', 'always'],
|
||||
'no-implicit-coercion': 'error',
|
||||
},
|
||||
}
|
21
.eslint/config-files.ts
Normal file
21
.eslint/config-files.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { type Linter } from "eslint";
|
||||
|
||||
import { configFiles } from "./shared";
|
||||
|
||||
export const configFilesConfig: Linter.Config = {
|
||||
name: "eslint/config-files",
|
||||
files: configFiles,
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"import/no-default-export": "off",
|
||||
"import/default": "off", // Allow missing default exports in config files
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"prefer-const": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
};
|
64
.eslint/import.ts
Normal file
64
.eslint/import.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { type Linter } from 'eslint'
|
||||
// @ts-expect-error - no types available
|
||||
import importPlugin from 'eslint-plugin-import'
|
||||
|
||||
import { sharedFiles } from './shared'
|
||||
|
||||
export const importConfigArray: Linter.Config[] = [
|
||||
{
|
||||
name: 'eslint/import',
|
||||
files: sharedFiles,
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
},
|
||||
rules: {
|
||||
...importPlugin.configs.recommended.rules,
|
||||
...importPlugin.configs.typescript.rules,
|
||||
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
['parent', 'sibling'],
|
||||
'index',
|
||||
'object',
|
||||
'type',
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: {
|
||||
order: 'asc',
|
||||
caseInsensitive: true,
|
||||
},
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: '@/**',
|
||||
group: 'internal',
|
||||
position: 'before',
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ['builtin'],
|
||||
},
|
||||
],
|
||||
'import/no-unresolved': 'off', // TypeScript handles this
|
||||
'import/no-duplicates': ['error', { 'prefer-inline': true }],
|
||||
'import/consistent-type-specifier-style': ['error', 'prefer-inline'],
|
||||
'import/first': 'error',
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-default-export': 'off', // Allow default exports
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
},
|
||||
node: true,
|
||||
},
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
36
.eslint/javascript.ts
Normal file
36
.eslint/javascript.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { type Linter } from 'eslint'
|
||||
|
||||
import { javascriptFiles } from './shared'
|
||||
|
||||
export const javascriptConfig: Linter.Config = {
|
||||
name: 'eslint/javascript',
|
||||
files: javascriptFiles,
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
'no-debugger': 'error',
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error',
|
||||
'object-shorthand': 'error',
|
||||
'prefer-template': 'error',
|
||||
curly: ['error', 'all'],
|
||||
eqeqeq: ['error', 'always'],
|
||||
'no-implicit-coercion': 'error',
|
||||
},
|
||||
}
|
31
.eslint/jsx-a11y.ts
Normal file
31
.eslint/jsx-a11y.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { type Linter } from 'eslint'
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
||||
|
||||
import { astroFiles, javascriptFiles, typescriptFiles } from './shared'
|
||||
|
||||
export const jsxA11yConfig: Linter.Config = {
|
||||
name: 'eslint/jsx-a11y',
|
||||
files: [
|
||||
...astroFiles,
|
||||
...typescriptFiles.filter(f => f.includes('tsx')),
|
||||
...javascriptFiles.filter(f => f.includes('jsx')),
|
||||
],
|
||||
plugins: {
|
||||
'jsx-a11y': jsxA11y,
|
||||
},
|
||||
rules: {
|
||||
...jsxA11y.configs.recommended.rules,
|
||||
|
||||
// Additional a11y rules
|
||||
'jsx-a11y/alt-text': 'error',
|
||||
'jsx-a11y/anchor-has-content': 'error',
|
||||
'jsx-a11y/anchor-is-valid': 'error',
|
||||
'jsx-a11y/click-events-have-key-events': 'error',
|
||||
'jsx-a11y/interactive-supports-focus': 'error',
|
||||
'jsx-a11y/no-redundant-roles': 'error',
|
||||
'jsx-a11y/img-redundant-alt': 'error',
|
||||
'jsx-a11y/no-access-key': 'error',
|
||||
'jsx-a11y/label-has-associated-control': 'error',
|
||||
'jsx-a11y/no-autofocus': 'warn',
|
||||
},
|
||||
}
|
59
.eslint/react.ts
Normal file
59
.eslint/react.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { type Linter } from "eslint";
|
||||
import react from "eslint-plugin-react";
|
||||
import * as reactHooks from "eslint-plugin-react-hooks";
|
||||
|
||||
import { javascriptFiles, typescriptFiles } from "./shared";
|
||||
|
||||
export const reactConfig: Linter.Config = {
|
||||
name: "eslint/react",
|
||||
files: [
|
||||
...typescriptFiles.filter((f) => f.includes("tsx")),
|
||||
...javascriptFiles.filter((f) => f.includes("jsx")),
|
||||
],
|
||||
plugins: {
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
...react.configs.recommended.rules,
|
||||
|
||||
"react/react-in-jsx-scope": "off", // Not needed in React 17+
|
||||
"react/prop-types": "off", // Using TypeScript
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/jsx-key": "error",
|
||||
"react/jsx-no-useless-fragment": "error",
|
||||
"react/self-closing-comp": "error",
|
||||
"react/jsx-boolean-value": ["error", "never"],
|
||||
"react/jsx-curly-brace-presence": [
|
||||
"error",
|
||||
{ props: "never", children: "never" },
|
||||
],
|
||||
"react/function-component-definition": [
|
||||
"error",
|
||||
{
|
||||
namedComponents: "arrow-function",
|
||||
unnamedComponents: "arrow-function",
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "18.2", // React version
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const reactHooksConfig: Linter.Config = {
|
||||
name: "eslint/react-hooks",
|
||||
files: [
|
||||
...typescriptFiles.filter((f) => f.includes("tsx")),
|
||||
...javascriptFiles.filter((f) => f.includes("jsx")),
|
||||
],
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
},
|
||||
};
|
35
.eslint/shared.ts
Normal file
35
.eslint/shared.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
export const sharedFiles = [
|
||||
'**/*.js',
|
||||
'**/*.cjs',
|
||||
'**/*.mjs',
|
||||
'**/*.jsx',
|
||||
'**/*.ts',
|
||||
'**/*.cts',
|
||||
'**/*.mts',
|
||||
'**/*.tsx',
|
||||
'**/*.d.ts',
|
||||
]
|
||||
|
||||
export const sharedTestFiles = [
|
||||
'**/*.test.{ts,tsx,js,jsx}',
|
||||
'**/*.spec.{ts,tsx,js,jsx}',
|
||||
'**/tests/**/*.{ts,tsx,js,jsx}',
|
||||
'**/__tests__/**/*.{ts,tsx,js,jsx}',
|
||||
]
|
||||
|
||||
export const astroFiles = ['**/*.astro']
|
||||
|
||||
export const typescriptFiles = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '**/*.d.ts']
|
||||
|
||||
export const javascriptFiles = ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs']
|
||||
|
||||
export const configFiles = [
|
||||
'*.config.{ts,js,mjs}',
|
||||
'**/*.config.{ts,js,mjs}',
|
||||
'**/vite.config.*',
|
||||
'**/vitest.config.*',
|
||||
'**/playwright.config.*',
|
||||
'**/astro.config.*',
|
||||
'**/tailwind.config.*',
|
||||
'**/eslint.config.*',
|
||||
]
|
20
.eslint/test.ts
Normal file
20
.eslint/test.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { type Linter } from 'eslint'
|
||||
|
||||
import { sharedTestFiles } from './shared'
|
||||
|
||||
export const testConfig: Linter.Config = {
|
||||
name: 'eslint/test',
|
||||
files: sharedTestFiles,
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
},
|
||||
}
|
45
.eslint/typescript.ts
Normal file
45
.eslint/typescript.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { type Linter } from 'eslint'
|
||||
|
||||
import { typescriptFiles } from './shared'
|
||||
|
||||
export function createTypescriptConfig(tsConfigPath: string): Linter.Config {
|
||||
return {
|
||||
name: 'eslint/typescript',
|
||||
files: typescriptFiles,
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
project: tsConfigPath,
|
||||
tsconfigRootDir: process.cwd(),
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Basic TypeScript rules that work without type information
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
||||
|
||||
// Override base rules for TypeScript
|
||||
'no-unused-vars': 'off', // Handled by TypeScript
|
||||
'no-undef': 'off', // TypeScript handles this
|
||||
},
|
||||
}
|
||||
}
|
27
.github/workflows/ci-pipeline.yml
vendored
27
.github/workflows/ci-pipeline.yml
vendored
|
@ -3,6 +3,10 @@ on:
|
|||
pull_request:
|
||||
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
|
||||
|
|
47
.prettierignore
Normal file
47
.prettierignore
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
.astro/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Generated files
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Test outputs
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# Package files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Config files that should maintain their format
|
||||
wrangler.toml
|
||||
|
||||
# Binary files
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.gif
|
||||
*.svg
|
||||
*.ico
|
||||
*.woff
|
||||
*.woff2
|
||||
*.ttf
|
||||
*.eot
|
||||
|
||||
# IDE and OS
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Static assets
|
||||
public/fonts/
|
||||
public/favicon.ico
|
||||
public/favicon.svg
|
44
.vscode/settings.json
vendored
Normal file
44
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"astro"
|
||||
],
|
||||
"eslint.format.enable": false,
|
||||
"prettier.requireConfig": true,
|
||||
"[astro]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.astro": "astro"
|
||||
}
|
||||
}
|
|
@ -1,14 +1,11 @@
|
|||
import tailwind from '@astrojs/tailwind'
|
||||
// @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',
|
||||
|
@ -18,4 +15,16 @@ export default defineConfig({
|
|||
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;
|
8378
package-lock.json
generated
8378
package-lock.json
generated
File diff suppressed because it is too large
Load diff
46
package.json
46
package.json
|
@ -9,8 +9,10 @@
|
|||
"preview": "astro preview --port 3000",
|
||||
"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",
|
||||
|
@ -19,7 +21,7 @@
|
|||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/cloudflare": "^12.5.2",
|
||||
"@astrojs/preact": "^4.0.11",
|
||||
"@astrojs/react": "^4.3.0",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.3.1",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
|
@ -27,6 +29,8 @@
|
|||
"@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",
|
||||
|
@ -37,28 +41,56 @@
|
|||
"lucide-react": "^0.475.0",
|
||||
"motion": "^11.13.5",
|
||||
"postcss": "^8.5.1",
|
||||
"preact": "^10.26.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"sharp": "^0.33.5",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@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": "^3.114.8"
|
||||
"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,
|
||||
};
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,10 +11,7 @@ const {
|
|||
} = getUI(locale)
|
||||
---
|
||||
|
||||
<button
|
||||
onclick="window.history.back()"
|
||||
class="mb-8 flex w-min items-center gap-2"
|
||||
>
|
||||
<button type="button" onclick="window.history.back()" class="mb-8 flex w-min items-center gap-2">
|
||||
<ArrowLeftIcon class="size-4" />
|
||||
{slug.back}
|
||||
</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) => (
|
||||
|
|
|
@ -33,11 +33,7 @@ const {
|
|||
{community.title[2]}
|
||||
</motion.span>
|
||||
</Description>
|
||||
<motion.p
|
||||
client:load
|
||||
{...getTitleAnimation(0.6)}
|
||||
className="lg:w-1/2 lg:px-0"
|
||||
>
|
||||
<motion.p client:load {...getTitleAnimation(0.6)} className="lg:w-1/2 lg:px-0">
|
||||
{community.description}
|
||||
</motion.p>
|
||||
<div class="flex w-full flex-wrap gap-3 sm:gap-10 md:justify-center">
|
||||
|
@ -47,19 +43,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>
|
||||
|
|
|
@ -21,14 +21,11 @@ const {
|
|||
|
||||
const { title1 = features.title1, title2 = features.title2, title3 = features.title3 } = Astro.props
|
||||
|
||||
const descriptions = Object.values(features.featureTabs).map((tab) => tab.description)
|
||||
const descriptions = Object.values(features.featureTabs).map(tab => tab.description)
|
||||
---
|
||||
|
||||
<section
|
||||
id="Features"
|
||||
class="relative flex w-full flex-col py-12 text-start lg:py-36"
|
||||
>
|
||||
<Description class="mb-2 text-4xl sm:text-6xl font-bold">
|
||||
<section id="Features" class="relative flex w-full flex-col py-12 text-start lg:py-36">
|
||||
<Description class="mb-2 text-4xl font-bold sm:text-6xl">
|
||||
<motion.span client:load {...getTitleAnimation(0.2)}>
|
||||
{title1}
|
||||
</motion.span>
|
||||
|
@ -49,7 +46,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
<motion.button
|
||||
client:load
|
||||
{...getTitleAnimation()}
|
||||
class="feature-tab whitespace-nowrap"
|
||||
className="feature-tab whitespace-nowrap"
|
||||
data-active="true"
|
||||
>
|
||||
{features.featureTabs.workspaces.title}
|
||||
|
@ -57,21 +54,21 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
<motion.button
|
||||
client:load
|
||||
{...getTitleAnimation(0.2)}
|
||||
class="feature-tab whitespace-nowrap"
|
||||
className="feature-tab whitespace-nowrap"
|
||||
>
|
||||
{features.featureTabs.compactMode.title}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
client:load
|
||||
{...getTitleAnimation(0.4)}
|
||||
class="feature-tab whitespace-nowrap"
|
||||
className="feature-tab whitespace-nowrap"
|
||||
>
|
||||
{features.featureTabs.glance.title}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
client:load
|
||||
{...getTitleAnimation(0.6)}
|
||||
class="feature-tab whitespace-nowrap"
|
||||
className="feature-tab whitespace-nowrap"
|
||||
>
|
||||
{features.featureTabs.splitView.title}
|
||||
</motion.button>
|
||||
|
@ -79,12 +76,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
|
||||
<!-- Desktop features list -->
|
||||
<div id="features-list" class="hidden lg:flex lg:flex-col lg:gap-3">
|
||||
<motion.div
|
||||
client:load
|
||||
{...getTitleAnimation(0.8)}
|
||||
className="feature"
|
||||
data-active="true"
|
||||
>
|
||||
<motion.div client:load {...getTitleAnimation(0.8)} className="feature" data-active="true">
|
||||
<Description class="text-2xl font-bold">
|
||||
{features.featureTabs.workspaces.title}
|
||||
</Description>
|
||||
|
@ -119,14 +111,10 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
|||
</div>
|
||||
|
||||
<!-- Mobile description -->
|
||||
<div
|
||||
class="feature-description mt-4 lg:hidden"
|
||||
data-descriptions={descriptions}
|
||||
>
|
||||
</div>
|
||||
<div class="feature-description mt-4 lg:hidden" data-descriptions={descriptions}></div>
|
||||
</div>
|
||||
|
||||
<div class="sticky top-6 w-full lg:w-3/5 h-fit">
|
||||
<div class="sticky top-6 h-fit w-full lg:w-3/5">
|
||||
<div class="relative w-full">
|
||||
<div class="video-stack relative h-full w-full">
|
||||
<Video
|
||||
|
@ -173,24 +161,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 +192,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 +218,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>
|
||||
|
||||
|
|
|
@ -19,13 +19,8 @@ const {
|
|||
role="contentinfo"
|
||||
aria-label="Site footer"
|
||||
>
|
||||
<div
|
||||
class="container flex w-full flex-col items-start justify-between gap-12"
|
||||
>
|
||||
<section
|
||||
class="w-full text-center lg:w-1/2 lg:text-left"
|
||||
aria-labelledby="footer-title"
|
||||
>
|
||||
<div class="container flex w-full flex-col items-start justify-between gap-12">
|
||||
<section class="w-full text-center lg:w-1/2 lg:text-left" aria-labelledby="footer-title">
|
||||
<Description id="footer-title" class="text-6xl font-bold !text-paper"
|
||||
>{footer.title}</Description
|
||||
>
|
||||
|
@ -48,12 +43,8 @@ const {
|
|||
class="grid w-full max-w-5xl place-items-center gap-12 text-center sm:text-left"
|
||||
aria-label="Footer navigation and links"
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3 lg:w-full"
|
||||
>
|
||||
<div
|
||||
class="grid gap-8 sm:col-span-2 sm:grid-cols-2 md:col-span-1 md:grid-cols-1"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3 lg:w-full">
|
||||
<div class="grid gap-8 sm:col-span-2 sm:grid-cols-2 md:col-span-1 md:grid-cols-1">
|
||||
<section
|
||||
class="flex flex-col items-center gap-2 sm:items-start"
|
||||
aria-labelledby="follow-us-heading"
|
||||
|
@ -63,10 +54,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 +80,17 @@ const {
|
|||
</h2>
|
||||
<ul class="grid gap-2 opacity-80">
|
||||
<li>
|
||||
<a href="https://docs.zen-browser.app/" class="font-normal"
|
||||
>{footer.documentation}</a
|
||||
<a href="https://docs.zen-browser.app/" class="font-normal">{footer.documentation}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getLocalePath('/mods')} class="font-normal">{footer.zenMods}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getLocalePath('/release-notes')} class="font-normal">{footer.releaseNotes}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getLocalePath('/mods')} class="font-normal"
|
||||
>{footer.zenMods}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getLocalePath('/release-notes')} class="font-normal"
|
||||
>{footer.releaseNotes}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getLocalePath('/download?twilight')} class="font-normal"
|
||||
>{footer.twilight}</a
|
||||
<a href={getLocalePath('/download?twilight')} class="font-normal">{footer.twilight}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -119,19 +101,15 @@ const {
|
|||
</h2>
|
||||
<ul class="grid gap-2 opacity-80">
|
||||
<li>
|
||||
<a href="https://discord.gg/zen-browser" class="font-normal"
|
||||
>{footer.discord}</a
|
||||
<a href="https://discord.gg/zen-browser" class="font-normal">{footer.discord}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://uptime.zen-browser.app/" class="font-normal">{footer.uptimeStatus}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://uptime.zen-browser.app/" class="font-normal"
|
||||
>{footer.uptimeStatus}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/zen-browser/desktop/issues/new/choose"
|
||||
class="font-normal">{footer.reportAnIssue}</a
|
||||
<a href="https://github.com/zen-browser/desktop/issues/new/choose" class="font-normal"
|
||||
>{footer.reportAnIssue}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -148,11 +126,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,9 +36,7 @@ const {
|
|||
class="flex w-full flex-col items-center gap-[20%] py-32 text-center lg:gap-[25%]"
|
||||
>
|
||||
<div class="flex h-full flex-col items-center justify-center">
|
||||
<Title
|
||||
class="relative px-12 text-center font-normal leading-8 md:text-7xl lg:px-0 lg:text-9xl"
|
||||
>
|
||||
<Title class="relative px-12 text-center font-normal leading-8 md:text-7xl lg:px-0 lg:text-9xl">
|
||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||
{hero.title[0]}
|
||||
</motion.span>
|
||||
|
@ -49,11 +47,7 @@ const {
|
|||
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||
{hero.title[2]}
|
||||
</motion.span>
|
||||
<motion.span
|
||||
client:load
|
||||
{...getHeroTitleAnimation()}
|
||||
className="italic text-coral"
|
||||
>
|
||||
<motion.span client:load {...getHeroTitleAnimation()} className="italic text-coral">
|
||||
{hero.title[3]}
|
||||
</motion.span>
|
||||
<motion.span client:load {...getHeroTitleAnimation()}>
|
||||
|
@ -69,19 +63,19 @@ const {
|
|||
</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"
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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 { useEffect, useState, type FormEvent } from 'react'
|
||||
|
||||
import { useModsSearch } from '~/hooks/useModsSearch'
|
||||
import type { ZenTheme } from '~/mods'
|
||||
import { type Locale, getUI } from '~/utils/i18n'
|
||||
import { type ZenTheme } from '~/mods'
|
||||
import { getUI, type Locale } from '~/utils/i18n'
|
||||
|
||||
// Add icons to the library
|
||||
library.add(faSort, faSortUp, faSortDown)
|
||||
|
@ -13,12 +14,12 @@ const defaultSortIcon = icon({ prefix: 'fas', iconName: 'sort' })
|
|||
const ascSortIcon = icon({ prefix: 'fas', iconName: 'sort-up' })
|
||||
const descSortIcon = icon({ prefix: 'fas', iconName: 'sort-down' })
|
||||
|
||||
interface ModsListProps {
|
||||
type ModsListProps = {
|
||||
allMods: ZenTheme[]
|
||||
locale: Locale
|
||||
}
|
||||
|
||||
export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||
const ModsList = ({ allMods, locale }: ModsListProps) => {
|
||||
const {
|
||||
search,
|
||||
createdSort,
|
||||
|
@ -49,17 +50,17 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
|||
return defaultSortIcon
|
||||
}
|
||||
|
||||
function handleSearch(e: Event) {
|
||||
function handleSearch(e: FormEvent<HTMLInputElement>) {
|
||||
const target = e.target as HTMLInputElement
|
||||
setSearch(target.value)
|
||||
}
|
||||
|
||||
function handleLimitChange(e: Event) {
|
||||
function handleLimitChange(e: FormEvent<HTMLSelectElement>) {
|
||||
const target = e.target as HTMLSelectElement
|
||||
setLimit(Number.parseInt(target.value, 10))
|
||||
}
|
||||
|
||||
function handlePageSubmit(e: Event) {
|
||||
function handlePageSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
const newPage = Number.parseInt(pageInput, 10)
|
||||
if (!Number.isNaN(newPage) && newPage >= 1 && newPage <= totalPages) {
|
||||
|
@ -70,7 +71,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
|||
}
|
||||
}
|
||||
|
||||
function handlePageInputChange(e: Event) {
|
||||
function handlePageInputChange(e: FormEvent<HTMLInputElement>) {
|
||||
const target = e.target as HTMLInputElement
|
||||
setPageInput(target.value)
|
||||
}
|
||||
|
@ -100,6 +101,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
|||
if (index === 0) {
|
||||
return (
|
||||
<input
|
||||
key={index}
|
||||
aria-label="Page number"
|
||||
className="w-16 rounded border border-dark bg-transparent px-2 py-1 text-center text-sm"
|
||||
onInput={handlePageInputChange}
|
||||
|
@ -110,7 +112,9 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
|||
}
|
||||
return (
|
||||
<span className="text-sm" key={value}>
|
||||
{value.replace('{totalPages}', totalPages.toString()).replace('{totalItems}', totalItems.toString())}
|
||||
{value
|
||||
.replace('{totalPages}', totalPages.toString())
|
||||
.replace('{totalItems}', totalItems.toString())}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
|
@ -143,7 +147,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
|||
<div className="grid w-full grid-cols-2 place-items-center gap-4 sm:grid-cols-3">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-2 font-semibold text-md"
|
||||
className="text-md flex items-center gap-2 px-4 py-2 font-semibold"
|
||||
onClick={toggleCreatedSort}
|
||||
type="button"
|
||||
>
|
||||
|
@ -159,7 +163,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
|||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-2 font-semibold text-md"
|
||||
className="text-md flex items-center gap-2 px-4 py-2 font-semibold"
|
||||
onClick={toggleUpdatedSort}
|
||||
type="button"
|
||||
>
|
||||
|
@ -174,7 +178,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 px-4 py-2">
|
||||
<label className="font-semibold text-md" htmlFor="limit">
|
||||
<label className="text-md font-semibold" htmlFor="limit">
|
||||
{mods.sort.perPage}
|
||||
</label>
|
||||
<select
|
||||
|
@ -194,7 +198,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
|||
|
||||
<div className="grid w-full grid-cols-1 place-items-start gap-12 py-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{paginatedMods.length > 0 ? (
|
||||
paginatedMods.map((mod) => (
|
||||
paginatedMods.map(mod => (
|
||||
<a
|
||||
className="mod-card flex w-full flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
|
||||
href={`/mods/${mod.id}`}
|
||||
|
@ -209,17 +213,17 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-bold text-lg">
|
||||
{mod.name} <span className="ml-1 font-normal text-sm">by @{mod.author}</span>
|
||||
<h2 className="text-lg font-bold">
|
||||
{mod.name} <span className="ml-1 text-sm font-normal">by @{mod.author}</span>
|
||||
</h2>
|
||||
<p className="font-thin text-sm">{mod.description}</p>
|
||||
<p className="text-sm font-thin">{mod.description}</p>
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-4 grid place-items-center gap-4 place-self-center px-8 text-center">
|
||||
<h2 className="font-bold text-lg">{mods.noResults}</h2>
|
||||
<p className="font-thin text-sm">{mods.noResultsDescription}</p>
|
||||
<h2 className="text-lg font-bold">{mods.noResults}</h2>
|
||||
<p className="text-sm font-thin">{mods.noResultsDescription}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -228,3 +232,5 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModsList
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -25,7 +25,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 +36,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':
|
||||
|
@ -99,7 +103,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
|
||||
|
@ -110,75 +114,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('en-US', { dateStyle: 'long' })}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
props.extra?.length ? (
|
||||
|
@ -187,35 +194,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 {
|
||||
|
@ -241,10 +246,7 @@ generateItems(props.knownIssues, 'known')
|
|||
.ac-accordion {
|
||||
&.ac-accordion--light {
|
||||
> * + * {
|
||||
border-color: light-dark(
|
||||
rgba(0, 0, 0, 0.1),
|
||||
rgba(255, 255, 255, 0.1)
|
||||
) !important;
|
||||
border-color: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1)) !important;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,29 +22,25 @@ const {
|
|||
<li class="flex gap-2">
|
||||
<div
|
||||
class:list={[
|
||||
(type === 'security' && 'text-[#e3401f]') ||
|
||||
(type === 'feature' && 'text-[#bf3316] dark:text-[#ffb1a1]') ||
|
||||
(type === 'fix' && 'text-[#fe846b]') ||
|
||||
(type === 'theme' && 'text-[#f76f53]') ||
|
||||
(type === 'break' && 'text-[#471308] dark:text-[#D02908]') || ''
|
||||
, 'opacity-80 font-bold min-w-16']}
|
||||
>
|
||||
(type === 'security' && 'text-[#e3401f]') ||
|
||||
(type === 'feature' && 'text-[#bf3316] dark:text-[#ffb1a1]') ||
|
||||
(type === 'fix' && 'text-[#fe846b]') ||
|
||||
(type === 'theme' && 'text-[#f76f53]') ||
|
||||
(type === 'break' && 'text-[#471308] dark:text-[#D02908]') ||
|
||||
'',
|
||||
'min-w-16 font-bold opacity-80',
|
||||
]}
|
||||
>
|
||||
{itemType[type]}
|
||||
</div>
|
||||
<div>
|
||||
{content && (
|
||||
<span
|
||||
class="text-base opacity-80"
|
||||
set:html={content}
|
||||
/>
|
||||
)}
|
||||
{link && (
|
||||
<a
|
||||
href={link.href}
|
||||
class="text-base text-blue inline-block underline"
|
||||
>
|
||||
{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' })
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -45,7 +45,7 @@ const { label, href, checksum } = Astro.props
|
|||
Show SHA-256
|
||||
</span>
|
||||
<span class="checksum-tooltip popover absolute -left-14 -top-12 z-50 hidden min-w-[220px] items-center gap-2 whitespace-nowrap rounded-md border border-subtle bg-[rgba(255,255,255,0.98)] px-3 py-2 text-xs text-gray-700 opacity-100 shadow transition-opacity duration-150 group-focus-within/checksum:flex dark:bg-[rgba(24,24,27,0.98)] dark:text-gray-100">
|
||||
<span class="flex-1 truncate font-mono text-xs">{checksum}</span>
|
||||
<span class="font-mono flex-1 truncate text-xs">{checksum}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="copy-btn rounded bg-coral px-2 py-1 text-xs text-white hover:bg-coral/80 data-[twilight='true']:bg-zen-blue data-[twilight='true']:hover:bg-zen-blue/80"
|
||||
|
@ -89,14 +89,12 @@ const { label, href, checksum } = Astro.props
|
|||
|
||||
<script>
|
||||
const checksumButtons = document.querySelectorAll(
|
||||
'.checksum-icon-btn',
|
||||
'.checksum-icon-btn'
|
||||
) as NodeListOf<HTMLButtonElement>
|
||||
const checksumTooltips = document.querySelectorAll(
|
||||
'.checksum-tooltip',
|
||||
'.checksum-tooltip'
|
||||
) as NodeListOf<HTMLDivElement>
|
||||
const copyButtons = document.querySelectorAll(
|
||||
'.copy-btn',
|
||||
) as NodeListOf<HTMLButtonElement>
|
||||
const copyButtons = document.querySelectorAll('.copy-btn') as NodeListOf<HTMLButtonElement>
|
||||
|
||||
function stopEvent(e: Event) {
|
||||
e.preventDefault?.()
|
||||
|
@ -113,23 +111,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,9 +1,10 @@
|
|||
import { useEffect, useState } from 'preact/hooks'
|
||||
import type { ZenTheme } from '../mods'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { type ZenTheme } from '../mods'
|
||||
|
||||
type SortOrder = 'default' | 'asc' | 'desc'
|
||||
|
||||
interface ModsSearchState {
|
||||
type ModsSearchState = {
|
||||
search: string
|
||||
createdSort: SortOrder
|
||||
updatedSort: SortOrder
|
||||
|
@ -86,11 +87,11 @@ export function useModsSearch(mods: ZenTheme[]) {
|
|||
const searchTerm = state.search.toLowerCase()
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
mod =>
|
||||
mod.name.toLowerCase().includes(searchTerm) ||
|
||||
mod.description.toLowerCase().includes(searchTerm) ||
|
||||
mod.author.toLowerCase().includes(searchTerm) ||
|
||||
(mod.tags?.some((tag) => tag.toLowerCase().includes(searchTerm)) ?? false),
|
||||
(mod.tags?.some(tag => tag.toLowerCase().includes(searchTerm)) ?? false)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -120,34 +121,36 @@ export function useModsSearch(mods: ZenTheme[]) {
|
|||
const paginatedMods = filteredMods.slice(startIndex, endIndex)
|
||||
|
||||
const setSearch = (search: string) => {
|
||||
setState((prev) => ({ ...prev, search, page: 1 })) // Reset page when search changes
|
||||
setState(prev => ({ ...prev, search, page: 1 })) // Reset page when search changes
|
||||
}
|
||||
|
||||
const toggleCreatedSort = () => {
|
||||
setState((prev) => ({
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
createdSort: prev.createdSort === 'default' ? 'asc' : prev.createdSort === 'asc' ? 'desc' : 'default',
|
||||
createdSort:
|
||||
prev.createdSort === 'default' ? 'asc' : prev.createdSort === 'asc' ? 'desc' : 'default',
|
||||
page: 1, // Reset page when sort changes
|
||||
}))
|
||||
}
|
||||
|
||||
const toggleUpdatedSort = () => {
|
||||
setState((prev) => ({
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
updatedSort: prev.updatedSort === 'default' ? 'asc' : prev.updatedSort === 'asc' ? 'desc' : 'default',
|
||||
updatedSort:
|
||||
prev.updatedSort === 'default' ? 'asc' : prev.updatedSort === 'asc' ? 'desc' : 'default',
|
||||
page: 1, // Reset page when sort changes
|
||||
}))
|
||||
}
|
||||
|
||||
const setPage = (page: number) => {
|
||||
setState((prev) => ({
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
page: Math.max(1, Math.min(page, totalPages)),
|
||||
}))
|
||||
}
|
||||
|
||||
const setLimit = (limit: number) => {
|
||||
setState((prev) => ({ ...prev, limit, page: 1 })) // Reset page when limit changes
|
||||
setState(prev => ({ ...prev, limit, page: 1 })) // Reset page when limit changes
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -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,17 +89,15 @@ const locale = getLocale(Astro)
|
|||
/>
|
||||
|
||||
<script>
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'%c✌️ Zen-Browser%c\nWelcome to a calmer internet!',
|
||||
'filter: invert(1); font-size: 28px; font-weight: bolder; font-family: "Rubik"; margin-top: 20px; margin-bottom: 8px;',
|
||||
'color: #f76f53; font-size: 16px; font-family: "Rubik"; margin-bottom: 20px;'
|
||||
);
|
||||
)
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body
|
||||
class="overflow-x-hidden bg-paper font-['bricolage-grotesque'] text-dark text-balance"
|
||||
>
|
||||
<body class="overflow-x-hidden text-balance bg-paper font-['bricolage-grotesque'] text-dark">
|
||||
<NavBar />
|
||||
<slot />
|
||||
<Footer />
|
||||
|
@ -127,13 +126,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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -11,8 +11,8 @@ 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,
|
||||
|
@ -97,39 +97,22 @@ const {
|
|||
.replace('{version}', mod.version)
|
||||
.replace('{link}', getAuthorLink(mod.author))}
|
||||
/>
|
||||
<p
|
||||
set:html={slug.creationDate.replace('{createdAt}', dates.createdAt)}
|
||||
/>
|
||||
<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
|
||||
|
|
|
@ -27,10 +27,6 @@ const allMods = (await getAllMods()) || []
|
|||
</header>
|
||||
|
||||
<!-- Importing ModList component -->
|
||||
<ModsList
|
||||
allMods={allMods}
|
||||
locale={locale ?? CONSTANT.I18N.DEFAULT_LOCALE}
|
||||
client:load
|
||||
/>
|
||||
<ModsList allMods={allMods} locale={locale ?? CONSTANT.I18N.DEFAULT_LOCALE} client:load />
|
||||
</main>
|
||||
</Layout>
|
||||
|
|
|
@ -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,21 +21,16 @@ const {
|
|||
<main
|
||||
class="container flex h-full min-h-[1000px] flex-1 flex-col items-center justify-center py-4"
|
||||
>
|
||||
<div
|
||||
id="release-notes"
|
||||
class="py-42 flex min-h-screen gap-8 w-full flex-col justify-center"
|
||||
>
|
||||
<div id="release-notes" class="py-42 flex min-h-screen w-full flex-col justify-center gap-8">
|
||||
<Description class="mt-48 text-6xl font-bold">Changelog</Description>
|
||||
<p
|
||||
class="text-base opacity-55"
|
||||
set:html={releaseNotes.topSection.description.replaceAll(
|
||||
'{latestVersion}',
|
||||
releaseNotesData[0].version,
|
||||
releaseNotesData[0].version
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
class="mt-8 flex w-fit flex-col gap-4 sm:mr-0 sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="mt-8 flex w-fit flex-col gap-4 sm:mr-0 sm:flex-row sm:items-center">
|
||||
<Button class="flex" isPrimary href="/donate">
|
||||
{releaseNotes.list.support}
|
||||
</Button>
|
||||
|
@ -44,12 +39,11 @@ const {
|
|||
</Button>
|
||||
</div>
|
||||
{
|
||||
releaseNotesTwilight.features.length ||
|
||||
releaseNotesTwilight.fixes.length ? (
|
||||
releaseNotesTwilight.features.length || releaseNotesTwilight.fixes.length ? (
|
||||
<ReleaseNoteItem {...releaseNotesTwilight} isTwilight />
|
||||
) : null
|
||||
}
|
||||
{releaseNotesData.map((notes: any) => <ReleaseNoteItem {...notes} />)}
|
||||
{releaseNotesData.map(notes => <ReleaseNoteItem {...notes} />)}
|
||||
</div>
|
||||
</main>
|
||||
<Button href="#" id="scroll-top" isPrimary class="fixed bottom-8 right-8">
|
||||
|
@ -67,7 +61,7 @@ const {
|
|||
<ModalBody>
|
||||
<div id="version-list" class="flex flex-col gap-2 text-xl text-dark">
|
||||
{
|
||||
releaseNotesData.map((note) => (
|
||||
releaseNotesData.map(note => (
|
||||
<button
|
||||
aria-label={`Navigate to version ${note.version}`}
|
||||
class="w-full text-left transition-colors duration-150 hover:text-coral"
|
||||
|
@ -81,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 title1={welcome.title[0]} title2={welcome.title[1]} title3={welcome.title[2]} />
|
||||
</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
|
||||
|
|
|
@ -601,7 +601,10 @@
|
|||
"version": "1.0.0-a.30",
|
||||
"date": "26/08/2024",
|
||||
"extra": "This release is the thirtieth alpha release of the 1.0.0-alpha series.",
|
||||
"features": ["Added support for 24 more languages!", "Update installed mods from the browser settings"],
|
||||
"features": [
|
||||
"Added support for 24 more languages!",
|
||||
"Update installed mods from the browser settings"
|
||||
],
|
||||
"fixes": [
|
||||
{
|
||||
"description": "Letterboxing option is missing",
|
||||
|
@ -924,7 +927,11 @@
|
|||
"date": "24/09/2024",
|
||||
"workflowId": 11020784612,
|
||||
"extra": "This update is a small patch to fix some issues that weren't addressed in the previous release!",
|
||||
"features": ["Moved application menu button to the right", "Added new shortcuts", "Collapsed tab sidebar is now smaller"],
|
||||
"features": [
|
||||
"Moved application menu button to the right",
|
||||
"Added new shortcuts",
|
||||
"Collapsed tab sidebar is now smaller"
|
||||
],
|
||||
"fixes": [
|
||||
{
|
||||
"description": "Fixed issue with hovering over window control buttons (macOS)"
|
||||
|
@ -949,7 +956,9 @@
|
|||
"Improved Expand Tabs on Hover layout"
|
||||
],
|
||||
"themeChanges": ["Toggle inputs will not use the themed tertiary color"],
|
||||
"breakingChanges": ["The keyboard shortcuts will be overriden by the defaults ones in this update"],
|
||||
"breakingChanges": [
|
||||
"The keyboard shortcuts will be overriden by the defaults ones in this update"
|
||||
],
|
||||
"fixes": [
|
||||
{
|
||||
"description": "Fixed Firefox add-ons not updating",
|
||||
|
@ -1121,7 +1130,10 @@
|
|||
"description": "Fixed about page linking 'global Community' to a Mozilla page"
|
||||
}
|
||||
],
|
||||
"features": ["About page will now display the Firefox version used", "Disabled forcing container grouping for workspaces"]
|
||||
"features": [
|
||||
"About page will now display the Firefox version used",
|
||||
"Disabled forcing container grouping for workspaces"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.1-a.11",
|
||||
|
@ -1258,7 +1270,9 @@
|
|||
"description": "Fixed sidebar webpanels being in a darker contrast"
|
||||
}
|
||||
],
|
||||
"features": ["Added a confirmation dialog when the gradient generator has successfully saved the gradient"]
|
||||
"features": [
|
||||
"Added a confirmation dialog when the gradient generator has successfully saved the gradient"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.1-a.15",
|
||||
|
@ -2097,7 +2111,10 @@
|
|||
"date": "30/01/2025",
|
||||
"workflowId": 13062083313,
|
||||
"extra": "Quick fix for a critical bug that was introduced in the previous release.",
|
||||
"fixes": ["Fixed the browser not opening when having multiple windows", "Fixed macos fullscreen having a weird shadow"]
|
||||
"fixes": [
|
||||
"Fixed the browser not opening when having multiple windows",
|
||||
"Fixed macos fullscreen having a weird shadow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.7.5b",
|
||||
|
@ -2154,7 +2171,9 @@
|
|||
"Fixed opening glance tabs on essentials messing up the sidebar",
|
||||
"Fixed pinned tabs appearing on normal container after a restart"
|
||||
],
|
||||
"features": ["Tabs can now be dragged into pinned tabs by dragging them into the workspace indicator"],
|
||||
"features": [
|
||||
"Tabs can now be dragged into pinned tabs by dragging them into the workspace indicator"
|
||||
],
|
||||
"workflowId": 13209591935,
|
||||
"date": "08/02/2025"
|
||||
},
|
||||
|
@ -2684,7 +2703,10 @@
|
|||
"'All tabs' menu not showing any text when collapsed toolbar is enabled."
|
||||
],
|
||||
"security": "https://www.mozilla.org/en-US/security/advisories/mfsa2025-36/",
|
||||
"features": ["Updated to Firefox 138.0.4", "Better compact mode support for multiple toolbars."],
|
||||
"features": [
|
||||
"Updated to Firefox 138.0.4",
|
||||
"Better compact mode support for multiple toolbars."
|
||||
],
|
||||
"knownIssues": ["Selecting a tab on private mode doesn't scroll to make the tab visible."],
|
||||
"themeChanges": [
|
||||
"Changed the layout of workspaces and their icons internally to provide a more stable layout that doesn't require floating elements. We finally managed to get it to how we wanted it to be, so it will change less in the future."
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
@ -12,7 +12,7 @@ const getPlatformButton = (page: Page, platform: string) =>
|
|||
page.locator(`button.platform-selector[data-platform='${platform}']`)
|
||||
|
||||
// Helper to get the platform download link
|
||||
const getPlatformDownloadLink = (page: Page, platform: string, label: string) =>
|
||||
const _ = (page: Page, platform: string, label: string) =>
|
||||
page.locator(`#${platform}-downloads .download-link:has-text('${label}')`)
|
||||
|
||||
const platformConfigs: { name: string; userAgent: string; platform: string }[] = [
|
||||
|
@ -30,7 +30,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 +49,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,9 +67,12 @@ 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'
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -81,7 +85,11 @@ test.describe('Download page download links', () => {
|
|||
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 +100,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)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { vi } from 'vitest'
|
||||
|
||||
import translation from '~/i18n/en/translation.json'
|
||||
|
||||
vi.mock('~/utils/i18n', () => ({
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { GetStaticPaths } from 'astro'
|
||||
import { type GetStaticPaths } from 'astro'
|
||||
|
||||
import { CONSTANT } from '~/constants'
|
||||
import UI_EN from '~/i18n/en/translation.json'
|
||||
|
||||
|
@ -44,7 +45,9 @@ export const locales = CONSTANT.I18N.LOCALES.map(({ value }) => value)
|
|||
* List of locales excluding the default locale
|
||||
* @type {Locale[]}
|
||||
*/
|
||||
const otherLocales = CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE)
|
||||
const otherLocales = CONSTANT.I18N.LOCALES.filter(
|
||||
({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieves locales other than the default locale
|
||||
|
@ -94,7 +97,7 @@ export const getUI = (locale?: Locale | string): UI => {
|
|||
const result = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj }
|
||||
|
||||
// Merge properties from the default object
|
||||
for (const key of Object.keys(defaultObj) as Array<keyof T>) {
|
||||
for (const key of Object.keys(defaultObj) as (keyof T)[]) {
|
||||
const defaultValue = defaultObj[key]
|
||||
const overrideValue = overrideObj[key]
|
||||
|
||||
|
@ -106,7 +109,10 @@ export const getUI = (locale?: Locale | string): UI => {
|
|||
typeof overrideValue === 'object'
|
||||
) {
|
||||
// Type assertion to handle nested merging
|
||||
;(result as Record<keyof T, unknown>)[key] = deepMerge(defaultValue as object, overrideValue as Partial<object>)
|
||||
;(result as Record<keyof T, unknown>)[key] = deepMerge(
|
||||
defaultValue as object,
|
||||
overrideValue as Partial<object>
|
||||
)
|
||||
} else if (overrideValue !== undefined) {
|
||||
// Override with the new value if it exists
|
||||
;(result as Record<keyof T, unknown>)[key] = overrideValue
|
||||
|
@ -114,7 +120,7 @@ export const getUI = (locale?: Locale | string): UI => {
|
|||
}
|
||||
|
||||
// Add any new properties from overrideObj
|
||||
for (const key of Object.keys(overrideObj) as Array<keyof T>) {
|
||||
for (const key of Object.keys(overrideObj) as (keyof T)[]) {
|
||||
if (!(key in defaultObj)) {
|
||||
;(result as Record<keyof T, unknown>)[key] = overrideObj[key]
|
||||
}
|
||||
|
@ -137,12 +143,14 @@ export const getStaticPaths = (() => {
|
|||
params: { locale: undefined },
|
||||
props: { locale: CONSTANT.I18N.DEFAULT_LOCALE },
|
||||
},
|
||||
...CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE).map(({ value }) => ({
|
||||
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
|
||||
|
||||
|
|
|
@ -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