This commit is contained in:
Shintaro Jokagi 2025-05-29 01:00:26 +12:00 committed by GitHub
commit ab0873f3a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 7794 additions and 2749 deletions

28
.eslint/astro.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
},
}
}

View file

@ -3,6 +3,10 @@ on:
pull_request: pull_request:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
concurrency:
group: ci-pipeline-${{ github.head_ref }}
cancel-in-progress: true
jobs: jobs:
setup: setup:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -23,7 +27,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
biome: eslint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: setup needs: setup
steps: steps:
@ -39,8 +43,25 @@ jobs:
path: | path: |
node_modules node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
- name: Run Biome check - name: Run Eslint check
run: npx biome check ./src run: npm run lint
prettier:
runs-on: ubuntu-latest
needs: setup
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Restore node_modules from cache
uses: actions/cache@v4
with:
path: |
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
- name: Run Prettier check
run: npm run format:check
vitest: vitest:
runs-on: ubuntu-latest runs-on: ubuntu-latest

47
.prettierignore Normal file
View 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
View 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"
}
}

View file

@ -1,14 +1,11 @@
import tailwind from '@astrojs/tailwind' import react from '@astrojs/react'
// @ts-check
import { defineConfig } from 'astro/config'
import preact from '@astrojs/preact'
import sitemap from '@astrojs/sitemap' import sitemap from '@astrojs/sitemap'
import tailwind from '@astrojs/tailwind'
import { defineConfig } from 'astro/config'
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [tailwind(), preact({ compat: true }), sitemap()], integrations: [tailwind(), react(), sitemap()],
site: 'https://zen-browser.app', site: 'https://zen-browser.app',
i18n: { i18n: {
defaultLocale: 'en', defaultLocale: 'en',
@ -18,4 +15,16 @@ export default defineConfig({
prefixDefaultLocale: false, prefixDefaultLocale: false,
}, },
}, },
vite: {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
astro: ['astro'],
},
},
},
},
},
}) })

View file

@ -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
View file

@ -0,0 +1,94 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { includeIgnoreFile } from "@eslint/compat";
import { type TSESLint } from "@typescript-eslint/utils";
import prettierConfig from "eslint-config-prettier";
import astro from "eslint-plugin-astro";
import tseslint, { configs } from "typescript-eslint";
// Import modular configurations
import { astroConfig } from "./.eslint/astro";
import { baseConfig } from "./.eslint/base";
import { configFilesConfig } from "./.eslint/config-files";
import { importConfigArray } from "./.eslint/import";
import { javascriptConfig } from "./.eslint/javascript";
import { jsxA11yConfig } from "./.eslint/jsx-a11y";
import { reactConfig, reactHooksConfig } from "./.eslint/react";
import { testConfig } from "./.eslint/test";
import { createTypescriptConfig } from "./.eslint/typescript";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const tsConfigPath = resolve(__dirname, "./tsconfig.json");
const ignoresConfig = {
name: "eslint/ignores",
ignores: [
// Build outputs
"**/dist/**",
"**/build/**",
"**/.astro/**",
"**/node_modules/**",
// Test outputs
"**/coverage/**",
"**/playwright-report/**",
"**/test-results/**",
// Config files that don't need linting
"**/*.config.js",
"**/*.config.mjs",
"**/wrangler.toml",
// Other common ignores
"**/.next/**",
"**/.nuxt/**",
"**/.output/**",
"**/.vercel/**",
"**/.netlify/**",
"**/public/**",
"**/*.min.js",
"**/*.d.ts",
"**/CHANGELOG.md",
"**/README.md",
],
} satisfies TSESLint.FlatConfig.Config;
const config: TSESLint.FlatConfig.ConfigArray = tseslint.config(
// Include .gitignore patterns
includeIgnoreFile(resolve(__dirname, ".gitignore")),
// Core configurations
ignoresConfig,
baseConfig,
// TypeScript ecosystem
...configs.strict,
...configs.stylistic,
createTypescriptConfig(tsConfigPath),
// Import management
...importConfigArray,
// Framework specific
reactConfig,
reactHooksConfig,
jsxA11yConfig,
// Astro specific
...astro.configs.recommended,
astroConfig,
// Language specific
javascriptConfig,
// Special cases
testConfig,
configFilesConfig,
// Prettier integration (must be last)
prettierConfig,
);
export default config;

8376
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,8 +9,10 @@
"preview": "astro preview --port 3000", "preview": "astro preview --port 3000",
"wrangler": "wrangler", "wrangler": "wrangler",
"astro": "astro", "astro": "astro",
"lint": "biome lint ./src", "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.astro",
"format": "biome format ./src", "lint:fix": "eslint ./src --ext .js,.jsx,.ts,.tsx,.astro --fix",
"format": "prettier --write ./src",
"format:check": "prettier --check ./src",
"prepare": "husky", "prepare": "husky",
"test": "npx vitest run", "test": "npx vitest run",
"test:coverage": "npx vitest --coverage", "test:coverage": "npx vitest --coverage",
@ -19,7 +21,7 @@
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@astrojs/cloudflare": "^12.5.2", "@astrojs/cloudflare": "^12.5.2",
"@astrojs/preact": "^4.0.11", "@astrojs/react": "^4.3.0",
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.3.1", "@astrojs/sitemap": "^3.3.1",
"@astrojs/tailwind": "^6.0.2", "@astrojs/tailwind": "^6.0.2",
@ -27,6 +29,8 @@
"@fortawesome/fontawesome-svg-core": "^6.7.1", "@fortawesome/fontawesome-svg-core": "^6.7.1",
"@fortawesome/free-brands-svg-icons": "^6.7.1", "@fortawesome/free-brands-svg-icons": "^6.7.1",
"@fortawesome/free-solid-svg-icons": "^6.7.1", "@fortawesome/free-solid-svg-icons": "^6.7.1",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"astro": "^5.7.10", "astro": "^5.7.10",
"astro-navbar": "^2.3.7", "astro-navbar": "^2.3.7",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
@ -37,28 +41,56 @@
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"motion": "^11.13.5", "motion": "^11.13.5",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"preact": "^10.26.2", "react": "^19.1.0",
"react-dom": "^19.1.0",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.0",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.15",
"typescript": "^5.6.3" "typescript": "^5.6.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@eslint/compat": "^1.2.9",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.27.0",
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/eslint-plugin-jsx-a11y": "^6.10.0",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^22.15.18", "@types/node": "^22.15.18",
"@typescript-eslint/eslint-plugin": "^8.33.0",
"@typescript-eslint/parser": "^8.33.0",
"@typescript-eslint/utils": "^8.33.0",
"@vitest/coverage-istanbul": "^3.1.3", "@vitest/coverage-istanbul": "^3.1.3",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^4.4.1",
"eslint-plugin-astro": "^1.3.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"lint-staged": "^15.2.7", "lint-staged": "^15.2.7",
"prettier": "3.5.3",
"prettier-plugin-astro": "0.14.1",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript-eslint": "^8.33.0",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.3", "vitest": "^3.1.3",
"wrangler": "^3.114.8" "wrangler": "^4.17.0"
},
"overrides": {
"jiti": "2.4.2"
}, },
"lint-staged": { "lint-staged": {
"src/**/*.{ts,tsx,astro,js,jsx}": ["biome check --write ."] "src/**/*.{ts,tsx,astro,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"src/**/*.{json,md,css}": [
"prettier --write"
]
} }
} }

View file

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

58
prettier.config.js Normal file
View 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,
};

View file

@ -7,7 +7,7 @@ export function getTitleAnimation(delay = 0, duration = 0.3, once = true) {
filter: 'blur(0px)', filter: 'blur(0px)',
transition: { duration, delay }, transition: { duration, delay },
}, },
viewport: { once: once }, viewport: { once },
} }
} }

View file

@ -11,10 +11,7 @@ const {
} = getUI(locale) } = getUI(locale)
--- ---
<button <button type="button" onclick="window.history.back()" class="mb-8 flex w-min items-center gap-2">
onclick="window.history.back()"
class="mb-8 flex w-min items-center gap-2"
>
<ArrowLeftIcon class="size-4" /> <ArrowLeftIcon class="size-4" />
{slug.back} {slug.back}
</button> </button>

View file

@ -4,10 +4,7 @@ const sizes = [216, 396, 576, 756]
const borderWidths = [20, 30, 40, 50] const borderWidths = [20, 30, 40, 50]
--- ---
<div <div id="circles" class:list={['pointer-events-none inset-0 overflow-hidden', classList]}>
id="circles"
class:list={['pointer-events-none inset-0 overflow-hidden', classList]}
>
<div class="mx-auto opacity-10 lg:opacity-100"> <div class="mx-auto opacity-10 lg:opacity-100">
{ {
[...Array(4)].map((_, i) => ( [...Array(4)].map((_, i) => (

View file

@ -33,11 +33,7 @@ const {
{community.title[2]} {community.title[2]}
</motion.span> </motion.span>
</Description> </Description>
<motion.p <motion.p client:load {...getTitleAnimation(0.6)} className="lg:w-1/2 lg:px-0">
client:load
{...getTitleAnimation(0.6)}
className="lg:w-1/2 lg:px-0"
>
{community.description} {community.description}
</motion.p> </motion.p>
<div class="flex w-full flex-wrap gap-3 sm:gap-10 md:justify-center"> <div class="flex w-full flex-wrap gap-3 sm:gap-10 md:justify-center">
@ -47,19 +43,11 @@ const {
<span>{community.lists.freeAndOpenSource.title}</span> <span>{community.lists.freeAndOpenSource.title}</span>
</Button> </Button>
</motion.span> </motion.span>
<motion.div <motion.div client:load {...getTitleAnimation(1)} className="flex items-center gap-4">
client:load
{...getTitleAnimation(1)}
className="flex items-center gap-4"
>
<CheckIcon class="size-4" /> <CheckIcon class="size-4" />
<span>{community.lists.simpleYetPowerful.title}</span> <span>{community.lists.simpleYetPowerful.title}</span>
</motion.div> </motion.div>
<motion.div <motion.div client:load {...getTitleAnimation(1.2)} className="flex items-center gap-4">
client:load
{...getTitleAnimation(1.2)}
className="flex items-center gap-4"
>
<CheckIcon class="size-4" /> <CheckIcon class="size-4" />
<span>{community.lists.privateAndAlwaysUpToDate.title}</span> <span>{community.lists.privateAndAlwaysUpToDate.title}</span>
</motion.div> </motion.div>

View file

@ -21,14 +21,11 @@ const {
const { title1 = features.title1, title2 = features.title2, title3 = features.title3 } = Astro.props const { title1 = features.title1, title2 = features.title2, title3 = features.title3 } = Astro.props
const descriptions = Object.values(features.featureTabs).map((tab) => tab.description) const descriptions = Object.values(features.featureTabs).map(tab => tab.description)
--- ---
<section <section id="Features" class="relative flex w-full flex-col py-12 text-start lg:py-36">
id="Features" <Description class="mb-2 text-4xl font-bold sm:text-6xl">
class="relative flex w-full flex-col py-12 text-start lg:py-36"
>
<Description class="mb-2 text-4xl sm:text-6xl font-bold">
<motion.span client:load {...getTitleAnimation(0.2)}> <motion.span client:load {...getTitleAnimation(0.2)}>
{title1} {title1}
</motion.span> </motion.span>
@ -49,7 +46,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
<motion.button <motion.button
client:load client:load
{...getTitleAnimation()} {...getTitleAnimation()}
class="feature-tab whitespace-nowrap" className="feature-tab whitespace-nowrap"
data-active="true" data-active="true"
> >
{features.featureTabs.workspaces.title} {features.featureTabs.workspaces.title}
@ -57,21 +54,21 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
<motion.button <motion.button
client:load client:load
{...getTitleAnimation(0.2)} {...getTitleAnimation(0.2)}
class="feature-tab whitespace-nowrap" className="feature-tab whitespace-nowrap"
> >
{features.featureTabs.compactMode.title} {features.featureTabs.compactMode.title}
</motion.button> </motion.button>
<motion.button <motion.button
client:load client:load
{...getTitleAnimation(0.4)} {...getTitleAnimation(0.4)}
class="feature-tab whitespace-nowrap" className="feature-tab whitespace-nowrap"
> >
{features.featureTabs.glance.title} {features.featureTabs.glance.title}
</motion.button> </motion.button>
<motion.button <motion.button
client:load client:load
{...getTitleAnimation(0.6)} {...getTitleAnimation(0.6)}
class="feature-tab whitespace-nowrap" className="feature-tab whitespace-nowrap"
> >
{features.featureTabs.splitView.title} {features.featureTabs.splitView.title}
</motion.button> </motion.button>
@ -79,12 +76,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
<!-- Desktop features list --> <!-- Desktop features list -->
<div id="features-list" class="hidden lg:flex lg:flex-col lg:gap-3"> <div id="features-list" class="hidden lg:flex lg:flex-col lg:gap-3">
<motion.div <motion.div client:load {...getTitleAnimation(0.8)} className="feature" data-active="true">
client:load
{...getTitleAnimation(0.8)}
className="feature"
data-active="true"
>
<Description class="text-2xl font-bold"> <Description class="text-2xl font-bold">
{features.featureTabs.workspaces.title} {features.featureTabs.workspaces.title}
</Description> </Description>
@ -119,14 +111,10 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
</div> </div>
<!-- Mobile description --> <!-- Mobile description -->
<div <div class="feature-description mt-4 lg:hidden" data-descriptions={descriptions}></div>
class="feature-description mt-4 lg:hidden"
data-descriptions={descriptions}
>
</div>
</div> </div>
<div class="sticky top-6 w-full lg:w-3/5 h-fit"> <div class="sticky top-6 h-fit w-full lg:w-3/5">
<div class="relative w-full"> <div class="relative w-full">
<div class="video-stack relative h-full w-full"> <div class="video-stack relative h-full w-full">
<Video <Video
@ -173,24 +161,16 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
</section> </section>
<script> <script>
const features = document.querySelectorAll( const features = document.querySelectorAll('.feature, .feature-tab') as NodeListOf<HTMLElement>
'.feature, .feature-tab',
) as NodeListOf<HTMLElement>
// Set initial description // Set initial description
const descriptionEl = document.querySelector( const descriptionEl = document.querySelector('.feature-description') as HTMLDivElement
'.feature-description',
) as HTMLDivElement
const descriptions = descriptionEl?.dataset.descriptions?.split(',') const descriptions = descriptionEl?.dataset.descriptions?.split(',')
if (descriptionEl && descriptions) { if (descriptionEl && descriptions) {
descriptionEl.textContent = descriptions[0] descriptionEl.textContent = descriptions[0]
} }
function changeToFeature({ function changeToFeature({ target }: { target: HTMLElement | undefined | null }) {
target,
}: {
target: HTMLElement | undefined | null
}) {
target = target?.closest('.feature, .feature-tab') target = target?.closest('.feature, .feature-tab')
if (!target) return if (!target) return
@ -212,9 +192,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
descriptionEl.textContent = descriptions[index] descriptionEl.textContent = descriptions[index]
} }
const videos = document.querySelectorAll( const videos = document.querySelectorAll('.feature-video') as NodeListOf<HTMLVideoElement>
'.feature-video',
) as NodeListOf<HTMLVideoElement>
videos.forEach((vid, i) => { videos.forEach((vid, i) => {
const yOffset = (i - index) * 20 const yOffset = (i - index) * 20
const zOffset = i === index ? 0 : -100 - Math.abs(i - index) * 50 const zOffset = i === index ? 0 : -100 - Math.abs(i - index) * 50
@ -240,9 +218,11 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
} }
for (const feature of features) { for (const feature of features) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
feature.addEventListener('click', changeToFeature as any) feature.addEventListener('click', changeToFeature as any)
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
changeToFeature({ target: features[0] as any }) changeToFeature({ target: features[0] as any })
</script> </script>

View file

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

View file

@ -36,9 +36,7 @@ const {
class="flex w-full flex-col items-center gap-[20%] py-32 text-center lg:gap-[25%]" class="flex w-full flex-col items-center gap-[20%] py-32 text-center lg:gap-[25%]"
> >
<div class="flex h-full flex-col items-center justify-center"> <div class="flex h-full flex-col items-center justify-center">
<Title <Title class="relative px-12 text-center font-normal leading-8 md:text-7xl lg:px-0 lg:text-9xl">
class="relative px-12 text-center font-normal leading-8 md:text-7xl lg:px-0 lg:text-9xl"
>
<motion.span client:load {...getHeroTitleAnimation()}> <motion.span client:load {...getHeroTitleAnimation()}>
{hero.title[0]} {hero.title[0]}
</motion.span> </motion.span>
@ -49,11 +47,7 @@ const {
<motion.span client:load {...getHeroTitleAnimation()}> <motion.span client:load {...getHeroTitleAnimation()}>
{hero.title[2]} {hero.title[2]}
</motion.span> </motion.span>
<motion.span <motion.span client:load {...getHeroTitleAnimation()} className="italic text-coral">
client:load
{...getHeroTitleAnimation()}
className="italic text-coral"
>
{hero.title[3]} {hero.title[3]}
</motion.span> </motion.span>
<motion.span client:load {...getHeroTitleAnimation()}> <motion.span client:load {...getHeroTitleAnimation()}>
@ -69,19 +63,19 @@ const {
</motion.span> </motion.span>
<div class="mt-6 flex w-2/3 flex-col gap-3 sm:gap-6 md:w-fit md:flex-row"> <div class="mt-6 flex w-2/3 flex-col gap-3 sm:gap-6 md:w-fit md:flex-row">
<motion.span client:load {...getHeroTitleAnimation()}> <motion.span client:load {...getHeroTitleAnimation()}>
<Button class="w-full" href={getLocalePath("/download")} isPrimary> <Button class="w-full" href={getLocalePath('/download')} isPrimary>
{hero.buttons.beta} {hero.buttons.beta}
<ArrowRightIcon class="size-4" /> <ArrowRightIcon class="size-4" />
</Button> </Button>
</motion.span> </motion.span>
<motion.span client:load {...getHeroTitleAnimation()}> <motion.span client:load {...getHeroTitleAnimation()}>
<Button href={getLocalePath("/donate")}>{hero.buttons.support}</Button> <Button href={getLocalePath('/donate')}>{hero.buttons.support}</Button>
</motion.span> </motion.span>
</div> </div>
<motion.span <motion.span
client:load client:load
{...getHeroTitleAnimation()} {...getHeroTitleAnimation()}
class="mx-auto translate-y-16 !transform" className="mx-auto translate-y-16 !transform"
> >
<SocialMediaStrip /> <SocialMediaStrip />
</motion.span> </motion.span>

View file

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

View file

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

View file

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

View file

@ -25,7 +25,9 @@ if (props.date) {
} }
const ffVersion = getReleaseNoteFirefoxVersion(props) const ffVersion = getReleaseNoteFirefoxVersion(props)
const currentReleaseIndex = releaseNotesData.findIndex((releaseNote: ReleaseNote) => releaseNote.version === props.version) const currentReleaseIndex = releaseNotesData.findIndex(
(releaseNote: ReleaseNote) => releaseNote.version === props.version
)
const prevReleaseNote = releaseNotesData[currentReleaseIndex + 1] const prevReleaseNote = releaseNotesData[currentReleaseIndex + 1]
let compareLink = '' let compareLink = ''
if (prevReleaseNote && !isTwilight) { if (prevReleaseNote && !isTwilight) {
@ -34,13 +36,15 @@ if (prevReleaseNote && !isTwilight) {
const isLatest = currentReleaseIndex === 0 const isLatest = currentReleaseIndex === 0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const listItems = {} as any const listItems = {} as any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const generateItems = (items: any, type: string) => { const generateItems = (items: any, type: string) => {
if (!items) return if (!items) return
if (!listItems[type]) { if (!listItems[type]) {
listItems[type] = [] listItems[type] = []
} }
// biome-ignore lint/complexity/noForEach: We dont need to use a for loop here // eslint-disable-next-line @typescript-eslint/no-explicit-any
items.forEach((item: any) => { items.forEach((item: any) => {
switch (type) { switch (type) {
case 'feature': case 'feature':
@ -99,7 +103,7 @@ generateItems(props.knownIssues, 'known')
class="release-note-item relative mt-12 flex flex-col pt-24 lg:flex-row" class="release-note-item relative mt-12 flex flex-col pt-24 lg:flex-row"
id={props.version} id={props.version}
> >
<div class="px-5 md:px-10 md:pr-32 w-full gap-2 flex flex-col"> <div class="flex w-full flex-col gap-2 px-5 md:px-10 md:pr-32">
{ {
isTwilight ? ( isTwilight ? (
<a <a
@ -110,37 +114,40 @@ generateItems(props.knownIssues, 'known')
</a> </a>
) : null ) : null
} }
<div class="w-full sm:flex justify-between"> <div class="w-full justify-between sm:flex">
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-0 text-sm font-bold opacity-80"> <div
class="flex flex-col gap-1 text-sm font-bold opacity-80 sm:flex-row sm:items-center sm:gap-0"
>
{ {
isTwilight ? ( isTwilight ? (
<> <>
{releaseNoteItem.twilightChanges} {props.version.replaceAll( {releaseNoteItem.twilightChanges}{' '}
'{version}', {props.version.replaceAll('{version}', props.version)}
props.version,
)}
</> </>
) : ( ) : (
<> <>{releaseNoteItem.releaseChanges.replaceAll('{version}', props.version)}</>
{releaseNoteItem.releaseChanges.replaceAll(
'{version}',
props.version,
)}
</>
) )
} }
{ {
ffVersion ? ( ffVersion ? (
<>
<span class="text-muted-foreground mx-3 hidden sm:block">•</span> <span class="text-muted-foreground mx-3 hidden sm:block">•</span>
<a rel="noopener noreferrer"class="text-xs underline decoration-wavy text-coral opacity-80" href={`https://www.mozilla.org/en-US/firefox/${ffVersion}/releasenotes/`} target="_blank" rel="noopener noreferrer"> <a
rel="noopener noreferrer"
class="text-xs text-coral underline decoration-wavy opacity-80"
href={`https://www.mozilla.org/en-US/firefox/${ffVersion}/releasenotes/`}
target="_blank"
rel="noopener noreferrer"
>
{releaseNoteItem.firefoxVersion.replace('{version}', ffVersion)} {releaseNoteItem.firefoxVersion.replace('{version}', ffVersion)}
</a> </a>
</>
) : null ) : null
} }
<span class="text-muted-foreground mx-3 hidden sm:block">•</span> <span class="text-muted-foreground mx-3 hidden sm:block">•</span>
<a <a
rel="noopener noreferrer" rel="noopener noreferrer"
class="zen-link whitespace-nowrap !no-underline text-xs opacity-80" class="zen-link whitespace-nowrap text-xs !no-underline opacity-80"
target="_blank" target="_blank"
href={`https://github.com/zen-browser/desktop/releases/tag/${isTwilight ? 'twilight' : props.version}`} href={`https://github.com/zen-browser/desktop/releases/tag/${isTwilight ? 'twilight' : props.version}`}
>{releaseNoteItem.githubRelease}</a >{releaseNoteItem.githubRelease}</a
@ -151,7 +158,7 @@ generateItems(props.knownIssues, 'known')
<span class="text-muted-foreground mx-3 hidden sm:block">•</span> <span class="text-muted-foreground mx-3 hidden sm:block">•</span>
<a <a
rel="noopener noreferrer" rel="noopener noreferrer"
class="zen-link whitespace-nowrap !no-underline text-xs opacity-80" class="zen-link whitespace-nowrap text-xs !no-underline opacity-80"
target="_blank" target="_blank"
href={`https://github.com/zen-browser/desktop/actions/runs/${props.workflowId}`} href={`https://github.com/zen-browser/desktop/actions/runs/${props.workflowId}`}
> >
@ -166,7 +173,7 @@ generateItems(props.knownIssues, 'known')
<span class="text-muted-foreground mx-3 hidden sm:block">•</span> <span class="text-muted-foreground mx-3 hidden sm:block">•</span>
<a <a
rel="noopener noreferrer" rel="noopener noreferrer"
class="zen-link whitespace-nowrap !no-underline text-xs opacity-80" class="zen-link whitespace-nowrap text-xs !no-underline opacity-80"
target="_blank" target="_blank"
href={compareLink} href={compareLink}
> >
@ -176,7 +183,7 @@ generateItems(props.knownIssues, 'known')
) : null ) : null
} }
</div> </div>
<div class="text-xs opacity-80 font-bold"> <div class="text-xs font-bold opacity-80">
{date && date.toLocaleDateString('en-US', { dateStyle: 'long' })} {date && date.toLocaleDateString('en-US', { dateStyle: 'long' })}
</div> </div>
</div> </div>
@ -187,8 +194,8 @@ generateItems(props.knownIssues, 'known')
</p> </p>
) : null ) : null
} }
{isTwilight || isLatest ? ( {
isTwilight || isLatest ? (
<div class="text-muted-forground flex text-sm opacity-70"> <div class="text-muted-forground flex text-sm opacity-70">
{isTwilight ? <InfoIcon class="mx-4 my-0 size-6 text-yellow-500" /> : null} {isTwilight ? <InfoIcon class="mx-4 my-0 size-6 text-yellow-500" /> : null}
<p class="m-0"> <p class="m-0">
@ -196,27 +203,25 @@ generateItems(props.knownIssues, 'known')
<span set:html={releaseNoteItem.reportIssues} /> <span set:html={releaseNoteItem.reportIssues} />
</p> </p>
</div> </div>
) : null} ) : null
<div class="gap-8 flex flex-col mt-4"> }
<div class="mt-4 flex flex-col gap-8">
{ {
Object.keys(listItems).map((type) => { Object.keys(listItems).map(type => {
const items = listItems[type]; const items = listItems[type]
if (items.length === 0) return null; if (items.length === 0) return null
return ( return (
<ul class="gap-1 flex flex-col"> <ul class="flex flex-col gap-1">
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
{items.map((item: any) => ( {items.map((item: any) => (
<ReleaseNoteListItem <ReleaseNoteListItem type={item.type} content={item.content} link={item.link} />
type={item.type}
content={item.content}
link={item.link}
/>
))} ))}
</ul> </ul>
); )
</div>
}) })
} }
</div> </div>
</div>
<style is:global> <style is:global>
.ac-accordion-item-title { .ac-accordion-item-title {
@apply !text-dark; @apply !text-dark;
@ -241,10 +246,7 @@ generateItems(props.knownIssues, 'known')
.ac-accordion { .ac-accordion {
&.ac-accordion--light { &.ac-accordion--light {
> * + * { > * + * {
border-color: light-dark( border-color: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1)) !important;
rgba(0, 0, 0, 0.1),
rgba(255, 255, 255, 0.1)
) !important;
width: 100%; width: 100%;
} }
} }

View file

@ -26,25 +26,21 @@ const {
(type === 'feature' && 'text-[#bf3316] dark:text-[#ffb1a1]') || (type === 'feature' && 'text-[#bf3316] dark:text-[#ffb1a1]') ||
(type === 'fix' && 'text-[#fe846b]') || (type === 'fix' && 'text-[#fe846b]') ||
(type === 'theme' && 'text-[#f76f53]') || (type === 'theme' && 'text-[#f76f53]') ||
(type === 'break' && 'text-[#471308] dark:text-[#D02908]') || '' (type === 'break' && 'text-[#471308] dark:text-[#D02908]') ||
, 'opacity-80 font-bold min-w-16']} '',
> 'min-w-16 font-bold opacity-80',
]}
>
{itemType[type]} {itemType[type]}
</div> </div>
<div> <div>
{content && ( {content && <span class="text-base opacity-80" set:html={content} />}
<span {
class="text-base opacity-80" link && (
set:html={content} <a href={link.href} class="text-blue inline-block text-base underline">
/>
)}
{link && (
<a
href={link.href}
class="text-base text-blue inline-block underline"
>
{link.text} {link.text}
</a> </a>
)} )
}
</div> </div>
</li> </li>

View file

@ -2,7 +2,13 @@
const { gap = 4 } = Astro.props const { gap = 4 } = Astro.props
import { icon, library } from '@fortawesome/fontawesome-svg-core' import { icon, library } from '@fortawesome/fontawesome-svg-core'
import { faBluesky, faGithub, faMastodon, faReddit, faXTwitter } from '@fortawesome/free-brands-svg-icons' import {
faBluesky,
faGithub,
faMastodon,
faReddit,
faXTwitter,
} from '@fortawesome/free-brands-svg-icons'
library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit) library.add(faMastodon, faBluesky, faGithub, faXTwitter, faReddit)
const Mastodon = icon({ prefix: 'fab', iconName: 'mastodon' }) const Mastodon = icon({ prefix: 'fab', iconName: 'mastodon' })

View file

@ -9,10 +9,7 @@ const { label, className = '' } = Astro.props
<button <button
type="button" type="button"
class:list={[ class:list={['inline-flex h-8 w-8 cursor-pointer items-center justify-center', className]}
'inline-flex h-8 w-8 cursor-pointer items-center justify-center',
className,
]}
id="theme-switcher" id="theme-switcher"
aria-label={label || 'Toggle theme'} aria-label={label || 'Toggle theme'}
> >
@ -29,9 +26,7 @@ const { label, className = '' } = Astro.props
</button> </button>
<script> <script>
const themeSwitch = document.getElementById( const themeSwitch = document.getElementById('theme-switcher') as HTMLButtonElement
'theme-switcher',
) as HTMLButtonElement
const resolveTheme = () => { const resolveTheme = () => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) { if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {

View file

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

View file

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

View file

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

View file

@ -76,7 +76,7 @@
// Apply twilight mode to all relevant elements // Apply twilight mode to all relevant elements
const coralElements = document.querySelectorAll( const coralElements = document.querySelectorAll(
'.download-browser-logo, .release-type-tag, .decorative-gradient, .download-link, .download-arrow-icon, .download-card__icon, .checksum-icon-btn, .copy-btn, .flathub-download', '.download-browser-logo, .release-type-tag, .decorative-gradient, .download-link, .download-arrow-icon, .download-card__icon, .checksum-icon-btn, .copy-btn, .flathub-download'
) )
for (const element of coralElements) { for (const element of coralElements) {
element.setAttribute('data-twilight', 'true') element.setAttribute('data-twilight', 'true')
@ -88,10 +88,7 @@
if (!link.id.includes('beta')) { if (!link.id.includes('beta')) {
const href = link.getAttribute('href') const href = link.getAttribute('href')
if (href && href.includes('/latest/download/')) { if (href && href.includes('/latest/download/')) {
const twilightHref = href.replace( const twilightHref = href.replace('/latest/download/', '/download/twilight/')
'/latest/download/',
'/download/twilight/',
)
link.setAttribute('href', twilightHref) link.setAttribute('href', twilightHref)
} }
} }

View file

@ -42,7 +42,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
<div <div
id={`${platform}-downloads`} id={`${platform}-downloads`}
data-active={platform === "mac"} data-active={platform === 'mac'}
class="platform-section data-[active='false']:hidden" class="platform-section data-[active='false']:hidden"
> >
<div class="items-center gap-8 md:flex"> <div class="items-center gap-8 md:flex">
@ -56,7 +56,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
<p class="text-muted-foreground mb-6" set:html={description} /> <p class="text-muted-foreground mb-6" set:html={description} />
<div class="space-y-6"> <div class="space-y-6">
{ {
platform === "linux" ? ( platform === 'linux' ? (
<> <>
{releases.flathub && releases.flathub.all.label && ( {releases.flathub && releases.flathub.all.label && (
<article class="flathub-download data-[twilight='true']:hidden"> <article class="flathub-download data-[twilight='true']:hidden">
@ -71,24 +71,16 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
</article> </article>
)} )}
{releases.x86_64 && {releases.x86_64 &&
typeof releases.x86_64 === "object" && typeof releases.x86_64 === 'object' &&
"tarball" in releases.x86_64 && 'tarball' in releases.x86_64 &&
(releases.x86_64.tarball) && ( releases.x86_64.tarball && (
<article> <article>
<h4 class="mb-3 text-lg font-medium">x86_64</h4> <h4 class="mb-3 text-lg font-medium">x86_64</h4>
<div class=""> <div class="">
{releases.x86_64.tarball && ( {releases.x86_64.tarball && (
<DownloadCard <DownloadCard
label={ label={releases.x86_64.tarball.label ? releases.x86_64.tarball.label : ''}
releases.x86_64.tarball.label href={releases.x86_64.tarball.link ? releases.x86_64.tarball.link : ''}
? releases.x86_64.tarball.label
: ""
}
href={
releases.x86_64.tarball.link
? releases.x86_64.tarball.link
: ""
}
variant="x86_64" variant="x86_64"
checksum={releases.x86_64.tarball.checksum} checksum={releases.x86_64.tarball.checksum}
/> />
@ -97,24 +89,18 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
</article> </article>
)} )}
{releases.aarch64 && {releases.aarch64 &&
typeof releases.aarch64 === "object" && typeof releases.aarch64 === 'object' &&
"tarball" in releases.aarch64 && 'tarball' in releases.aarch64 &&
(releases.aarch64.tarball) && ( releases.aarch64.tarball && (
<article> <article>
<h4 class="mb-3 text-lg font-medium">ARM64</h4> <h4 class="mb-3 text-lg font-medium">ARM64</h4>
<div class="gap-3"> <div class="gap-3">
{releases.aarch64.tarball && ( {releases.aarch64.tarball && (
<DownloadCard <DownloadCard
label={ label={
releases.aarch64.tarball.label releases.aarch64.tarball.label ? releases.aarch64.tarball.label : ''
? releases.aarch64.tarball.label
: ""
}
href={
releases.aarch64.tarball.link
? releases.aarch64.tarball.link
: ""
} }
href={releases.aarch64.tarball.link ? releases.aarch64.tarball.link : ''}
variant="aarch64" variant="aarch64"
checksum={releases.aarch64.tarball.checksum} checksum={releases.aarch64.tarball.checksum}
/> />
@ -133,9 +119,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
checksum={releases.universal.checksum} checksum={releases.universal.checksum}
/> />
)} )}
{releases.x86_64 && {releases.x86_64 && isFlatReleaseInfo(releases.x86_64) && releases.x86_64.label && (
isFlatReleaseInfo(releases.x86_64) &&
releases.x86_64.label && (
<DownloadCard <DownloadCard
label={releases.x86_64.label} label={releases.x86_64.label}
href={releases.x86_64.link} href={releases.x86_64.link}
@ -158,11 +142,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
<div <div
class="download-browser-logo flex justify-center text-coral transition-colors data-[twilight='true']:text-zen-blue md:w-1/3" class="download-browser-logo flex justify-center text-coral transition-colors data-[twilight='true']:text-zen-blue md:w-1/3"
> >
<Image <Image src={AppIconDark} alt="Zen Browser" class="w-32 translate-y-6 transform dark:hidden" />
src={AppIconDark}
alt="Zen Browser"
class="w-32 translate-y-6 transform dark:hidden"
/>
<Image <Image
src={AppIconLight} src={AppIconLight}
alt="Zen Browser" alt="Zen Browser"

View file

@ -1,9 +1,10 @@
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'react'
import type { ZenTheme } from '../mods'
import { type ZenTheme } from '../mods'
type SortOrder = 'default' | 'asc' | 'desc' type SortOrder = 'default' | 'asc' | 'desc'
interface ModsSearchState { type ModsSearchState = {
search: string search: string
createdSort: SortOrder createdSort: SortOrder
updatedSort: SortOrder updatedSort: SortOrder
@ -86,11 +87,11 @@ export function useModsSearch(mods: ZenTheme[]) {
const searchTerm = state.search.toLowerCase() const searchTerm = state.search.toLowerCase()
if (searchTerm) { if (searchTerm) {
filtered = filtered.filter( filtered = filtered.filter(
(mod) => mod =>
mod.name.toLowerCase().includes(searchTerm) || mod.name.toLowerCase().includes(searchTerm) ||
mod.description.toLowerCase().includes(searchTerm) || mod.description.toLowerCase().includes(searchTerm) ||
mod.author.toLowerCase().includes(searchTerm) || mod.author.toLowerCase().includes(searchTerm) ||
(mod.tags?.some((tag) => tag.toLowerCase().includes(searchTerm)) ?? false), (mod.tags?.some(tag => tag.toLowerCase().includes(searchTerm)) ?? false)
) )
} }
@ -120,34 +121,36 @@ export function useModsSearch(mods: ZenTheme[]) {
const paginatedMods = filteredMods.slice(startIndex, endIndex) const paginatedMods = filteredMods.slice(startIndex, endIndex)
const setSearch = (search: string) => { const setSearch = (search: string) => {
setState((prev) => ({ ...prev, search, page: 1 })) // Reset page when search changes setState(prev => ({ ...prev, search, page: 1 })) // Reset page when search changes
} }
const toggleCreatedSort = () => { const toggleCreatedSort = () => {
setState((prev) => ({ setState(prev => ({
...prev, ...prev,
createdSort: prev.createdSort === 'default' ? 'asc' : prev.createdSort === 'asc' ? 'desc' : 'default', createdSort:
prev.createdSort === 'default' ? 'asc' : prev.createdSort === 'asc' ? 'desc' : 'default',
page: 1, // Reset page when sort changes page: 1, // Reset page when sort changes
})) }))
} }
const toggleUpdatedSort = () => { const toggleUpdatedSort = () => {
setState((prev) => ({ setState(prev => ({
...prev, ...prev,
updatedSort: prev.updatedSort === 'default' ? 'asc' : prev.updatedSort === 'asc' ? 'desc' : 'default', updatedSort:
prev.updatedSort === 'default' ? 'asc' : prev.updatedSort === 'asc' ? 'desc' : 'default',
page: 1, // Reset page when sort changes page: 1, // Reset page when sort changes
})) }))
} }
const setPage = (page: number) => { const setPage = (page: number) => {
setState((prev) => ({ setState(prev => ({
...prev, ...prev,
page: Math.max(1, Math.min(page, totalPages)), page: Math.max(1, Math.min(page, totalPages)),
})) }))
} }
const setLimit = (limit: number) => { const setLimit = (limit: number) => {
setState((prev) => ({ ...prev, limit, page: 1 })) // Reset page when limit changes setState(prev => ({ ...prev, limit, page: 1 })) // Reset page when limit changes
} }
return { return {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,6 @@ const locale = getLocale(Astro)
return 'light' return 'light'
})() })()
if (theme === 'light') { if (theme === 'light') {
document.documentElement.setAttribute('data-theme', 'light') document.documentElement.setAttribute('data-theme', 'light')
} else { } else {
@ -39,27 +38,32 @@ const locale = getLocale(Astro)
} }
</script> </script>
<!doctype html>
<html lang={locale}> <html lang={locale}>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
{redirect ? <meta http-equiv="refresh" content={`0;url=${redirect}`} /> : null} {redirect ? <meta http-equiv="refresh" content={`0;url=${redirect}`} /> : null}
<meta name="description" content={description ?? defaultDescription} /> <meta name="description" content={description ?? defaultDescription} />
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="sitemap" href="/sitemap-0.xml" /> <link rel="sitemap" href="/sitemap-0.xml" />
{isHome && (
// @prettier-ignore
<!-- Injecting schema to homepage only (for SEO) -->
<script is:inline type="application/ld+json">
{ {
"@context":"https://schema.org", isHome && (
"@type":"WebSite", <>
"name":"Zen Browser", {/* Injecting schema to homepage only (for SEO) */}
"url":"https://zen-browser.app/" <script
is:inline
type="application/ld+json"
set:html={JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Zen Browser',
url: 'https://zen-browser.app/',
})}
/>
</>
)
} }
</script>)}
<!-- ICO favicon as a fallback for browsers that don't support SVG favicons (Safari) --> <!-- ICO favicon as a fallback for browsers that don't support SVG favicons (Safari) -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
@ -72,10 +76,7 @@ const locale = getLocale(Astro)
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:image" content={ogImage ?? defaultOgImage} /> <meta property="og:image" content={ogImage ?? defaultOgImage} />
<meta <meta property="og:description" content={description ?? defaultDescription} />
property="og:description"
content={description ?? defaultDescription}
/>
<meta property="og:color" content="#da755b" /> <meta property="og:color" content="#da755b" />
<!-- Twitter card --> <!-- Twitter card -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
@ -88,17 +89,15 @@ const locale = getLocale(Astro)
/> />
<script> <script>
// eslint-disable-next-line no-console
console.log( console.log(
'%c✌ Zen-Browser%c\nWelcome to a calmer internet!', '%c✌ Zen-Browser%c\nWelcome to a calmer internet!',
'filter: invert(1); font-size: 28px; font-weight: bolder; font-family: "Rubik"; margin-top: 20px; margin-bottom: 8px;', 'filter: invert(1); font-size: 28px; font-weight: bolder; font-family: "Rubik"; margin-top: 20px; margin-bottom: 8px;',
'color: #f76f53; font-size: 16px; font-family: "Rubik"; margin-bottom: 20px;' 'color: #f76f53; font-size: 16px; font-family: "Rubik"; margin-bottom: 20px;'
); )
</script> </script>
</head> </head>
<body <body class="overflow-x-hidden text-balance bg-paper font-['bricolage-grotesque'] text-dark">
class="overflow-x-hidden bg-paper font-['bricolage-grotesque'] text-dark text-balance"
>
<NavBar /> <NavBar />
<slot /> <slot />
<Footer /> <Footer />
@ -127,13 +126,13 @@ const locale = getLocale(Astro)
--zen-paper: #f2f0e3; --zen-paper: #f2f0e3;
--zen-dark: #2e2e2e; --zen-dark: #2e2e2e;
--zen-muted: rgba(0, 0, 0, 0.05); --zen-muted: rgba(0, 0, 0, 0.05);
--zen-subtle: rgba(0,0,0,0.05); --zen-subtle: rgba(0, 0, 0, 0.05);
&[data-theme='dark'] { &[data-theme='dark'] {
--zen-paper: #1f1f1f; --zen-paper: #1f1f1f;
--zen-dark: #d1cfc0; --zen-dark: #d1cfc0;
--zen-muted: rgba(255, 255, 255, 0.05); --zen-muted: rgba(255, 255, 255, 0.05);
--zen-subtle: rgba(255,255,255,0.1); --zen-subtle: rgba(255, 255, 255, 0.1);
} }
} }

View file

@ -1,6 +1,6 @@
import { format } from 'date-fns' import { format } from 'date-fns'
export interface ZenTheme { export type ZenTheme = {
name: string name: string
description: string description: string
image: string image: string
@ -24,7 +24,7 @@ export async function getAllMods(): Promise<ZenTheme[]> {
const res = await fetch(THEME_API) const res = await fetch(THEME_API)
const json = await res.json() const json = await res.json()
// convert dict to array // convert dict to array
const mods = Object.keys(json).map((key) => json[key]) const mods = Object.keys(json).map(key => json[key])
return mods return mods
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View file

@ -17,9 +17,7 @@ const {
<main <main
class="container flex min-h-[70vh] flex-col items-center justify-center gap-6 py-24 text-center" class="container flex min-h-[70vh] flex-col items-center justify-center gap-6 py-24 text-center"
> >
<Title class="text-7xl font-bold text-coral md:text-9xl xl:text-9xl"> <Title class="text-7xl font-bold text-coral md:text-9xl xl:text-9xl"> 404 </Title>
404
</Title>
<div class="flex flex-col items-center gap-6"> <div class="flex flex-col items-center gap-6">
<Description class="text-xl md:text-2xl"> <Description class="text-xl md:text-2xl">
{notFound.title} {notFound.title}
@ -27,7 +25,7 @@ const {
<p class="max-w-xl text-lg text-gray-500 dark:text-gray-400"> <p class="max-w-xl text-lg text-gray-500 dark:text-gray-400">
{notFound.description} {notFound.description}
</p> </p>
<Button href={getLocalePath("/")} isPrimary class="w-fit"> <Button href={getLocalePath('/')} isPrimary class="w-fit">
{notFound.button} {notFound.button}
</Button> </Button>
</div> </div>

View file

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

View file

@ -14,26 +14,20 @@ const {
--- ---
<Layout title={layout.donate.title} description={layout.donate.description}> <Layout title={layout.donate.title} description={layout.donate.description}>
<main class="container pb-52 pt-24 flex flex-col items-center gap-12"> <main class="container flex flex-col items-center gap-12 pb-52 pt-24">
<div class="flex flex-col gap-4 lg:text-center"> <div class="flex flex-col gap-4 lg:text-center">
<Description class="text-6xl font-bold">{donate.title}</Description> <Description class="text-6xl font-bold">{donate.title}</Description>
<Description class="max-w-3xl"> <Description class="max-w-3xl">
{donate.description} {donate.description}
</Description> </Description>
</div> </div>
<div <div class="grid max-w-5xl grid-cols-1 gap-12 text-center lg:grid-cols-[1fr_1px_1fr]">
class="grid max-w-5xl grid-cols-1 gap-12 text-center lg:grid-cols-[1fr_1px_1fr]"
>
<div class="flex flex-col items-center gap-4"> <div class="flex flex-col items-center gap-4">
<div class="text-6xl font-bold">{donate.patreon.title}</div> <div class="text-6xl font-bold">{donate.patreon.title}</div>
<Description> <Description>
{donate.patreon.description} {donate.patreon.description}
</Description> </Description>
<Button <Button isPrimary href="https://www.patreon.com/zen_browser" class="w-fit">
isPrimary
href="https://www.patreon.com/zen_browser"
class="w-fit"
>
{donate.patreon.button} {donate.patreon.button}
<ArrowRightIcon class="size-4" /> <ArrowRightIcon class="size-4" />
</Button> </Button>

View file

@ -39,9 +39,7 @@ const platformDescriptions = download.platformDescriptions
<main class="flex min-h-screen flex-col px-6 data-[os='windows']:bg-zen-blue"> <main class="flex min-h-screen flex-col px-6 data-[os='windows']:bg-zen-blue">
<div class="container relative mx-auto py-12"> <div class="container relative mx-auto py-12">
<div class="mb-6 mt-12 flex flex-col gap-4"> <div class="mb-6 mt-12 flex flex-col gap-4">
<Description id="download-title" class="text-6xl font-bold" <Description id="download-title" class="text-6xl font-bold">{download.title}</Description>
>{download.title}</Description
>
<Description class="max-w-xl text-pretty"> <Description class="max-w-xl text-pretty">
{download.description} {download.description}
</Description> </Description>
@ -191,9 +189,7 @@ const platformDescriptions = download.platformDescriptions
</section> </section>
<!-- Security Notice --> <!-- Security Notice -->
<div <div class="bg-opaicty-10 grid grid-cols-[auto,1fr] gap-4 rounded-2xl bg-subtle p-6">
class="bg-opaicty-10 grid grid-cols-[auto,1fr] gap-4 rounded-2xl bg-subtle p-6"
>
<div class="h-fit rounded-xl bg-subtle p-3"> <div class="h-fit rounded-xl bg-subtle p-3">
<LockIcon class="h-5 w-5" /> <LockIcon class="h-5 w-5" />
</div> </div>
@ -202,10 +198,7 @@ const platformDescriptions = download.platformDescriptions
<h3 class="mb-2 text-lg font-medium"> <h3 class="mb-2 text-lg font-medium">
{download.securityNotice.title} {download.securityNotice.title}
</h3> </h3>
<p <p class="text-muted-foreground" set:html={download.securityNotice.description} />
class="text-muted-foreground"
set:html={download.securityNotice.description}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import rss, { type RSSOptions } from '@astrojs/rss' import rss, { type RSSOptions } from '@astrojs/rss'
import { releaseNotes } from '~/release-notes'
import type { ReleaseNote } from '~/release-notes' import { releaseNotes, type ReleaseNote } from '~/release-notes'
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from '~/utils/i18n'
/** The default number of entries to include in the RSS feed. */ /** The default number of entries to include in the RSS feed. */
@ -12,7 +13,8 @@ const RSS_ENTRY_LIMIT = 20
*/ */
export function GET(context: { url: URL }) { export function GET(context: { url: URL }) {
// Just in case the release notes array is empty for whatever reason. // Just in case the release notes array is empty for whatever reason.
const latestDate = releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date as string) : new Date() const latestDate =
releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date as string) : new Date()
const rssData: RSSOptions = { const rssData: RSSOptions = {
title: 'Zen Browser Release Notes', title: 'Zen Browser Release Notes',
@ -79,7 +81,10 @@ function formatReleaseNote(releaseNote: ReleaseNote) {
content += `<p>${releaseNote.extra.replace(/(\n)/g, '<br />')}</p>` content += `<p>${releaseNote.extra.replace(/(\n)/g, '<br />')}</p>`
} }
content += addReleaseNoteSection('⚠️ Breaking changes', releaseNote.breakingChanges?.map(breakingChangeToReleaseNote)) content += addReleaseNoteSection(
'⚠️ Breaking changes',
releaseNote.breakingChanges?.map(breakingChangeToReleaseNote)
)
content += addReleaseNoteSection('✓ Fixes', releaseNote.fixes?.map(fixToReleaseNote)) content += addReleaseNoteSection('✓ Fixes', releaseNote.fixes?.map(fixToReleaseNote))
content += addReleaseNoteSection('🖌 Theme Changes', releaseNote.themeChanges) content += addReleaseNoteSection('🖌 Theme Changes', releaseNote.themeChanges)
content += addReleaseNoteSection('⭐ Features', releaseNote.features) content += addReleaseNoteSection('⭐ Features', releaseNote.features)
@ -119,7 +124,9 @@ function fixToReleaseNote(fix?: Exclude<ReleaseNote['fixes'], undefined>[number]
return note return note
} }
function breakingChangeToReleaseNote(breakingChange?: Exclude<ReleaseNote['breakingChanges'], undefined>[number]) { function breakingChangeToReleaseNote(
breakingChange?: Exclude<ReleaseNote['breakingChanges'], undefined>[number]
) {
if (typeof breakingChange === 'string') { if (typeof breakingChange === 'string') {
return breakingChange return breakingChange
} }

View file

@ -12,11 +12,7 @@ const locale = getLocale(Astro)
const { layout } = getUI(locale) const { layout } = getUI(locale)
--- ---
<Layout <Layout title={layout.index.title} description={layout.index.description} isHome>
title={layout.index.title}
description={layout.index.description}
isHome
>
<main class="container"> <main class="container">
<Hero /> <Hero />
<Features /> <Features />

View file

@ -11,8 +11,8 @@ import { getLocale, getOtherLocales } from '~/utils/i18n'
export async function getStaticPaths() { export async function getStaticPaths() {
const mods = await getAllMods() const mods = await getAllMods()
return mods.flatMap((mod) => [ return mods.flatMap(mod => [
...getOtherLocales().map((locale) => ({ ...getOtherLocales().map(locale => ({
params: { params: {
slug: mod.id, slug: mod.id,
locale: locale, locale: locale,
@ -97,39 +97,22 @@ const {
.replace('{version}', mod.version) .replace('{version}', mod.version)
.replace('{link}', getAuthorLink(mod.author))} .replace('{link}', getAuthorLink(mod.author))}
/> />
<p <p set:html={slug.creationDate.replace('{createdAt}', dates.createdAt)} />
set:html={slug.creationDate.replace('{createdAt}', dates.createdAt)}
/>
{ {
dates.createdAt !== dates.updatedAt && ( dates.createdAt !== dates.updatedAt && (
<p <p set:html={slug.latestUpdate.replace('{updatedAt}', dates.updatedAt)} />
set:html={slug.latestUpdate.replace(
'{updatedAt}',
dates.updatedAt,
)}
/>
) )
} }
{ {
mod.homepage && ( mod.homepage && (
<a <a href={mod.homepage} target="_blank" rel="noopener noreferrer" class="zen-link">
href={mod.homepage}
target="_blank"
rel="noopener noreferrer"
class="zen-link"
>
{slug.visitModHomepage} {slug.visitModHomepage}
</a> </a>
) )
} }
</div> </div>
<div class="flex flex-col sm:items-end"> <div class="flex flex-col sm:items-end">
<Button <Button class="hidden" id="install-theme" extra={{ 'zen-theme-id': mod.id }} isPrimary>
class="hidden"
id="install-theme"
extra={{ 'zen-theme-id': mod.id }}
isPrimary
>
{slug.installMod} {slug.installMod}
</Button> </Button>
<Button <Button

View file

@ -27,10 +27,6 @@ const allMods = (await getAllMods()) || []
</header> </header>
<!-- Importing ModList component --> <!-- Importing ModList component -->
<ModsList <ModsList allMods={allMods} locale={locale ?? CONSTANT.I18N.DEFAULT_LOCALE} client:load />
allMods={allMods}
locale={locale ?? CONSTANT.I18N.DEFAULT_LOCALE}
client:load
/>
</main> </main>
</Layout> </Layout>

View file

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

View file

@ -15,7 +15,7 @@ export async function getStaticPaths() {
const i18nPaths = getI18nPaths() const i18nPaths = getI18nPaths()
return i18nPaths.flatMap(({ params: { locale } }) => [ return i18nPaths.flatMap(({ params: { locale } }) => [
...releaseNotes.map((release) => ({ ...releaseNotes.map(release => ({
params: { slug: release.version, locale }, params: { slug: release.version, locale },
props: { ...release }, props: { ...release },
})), })),

View file

@ -21,21 +21,16 @@ const {
<main <main
class="container flex h-full min-h-[1000px] flex-1 flex-col items-center justify-center py-4" class="container flex h-full min-h-[1000px] flex-1 flex-col items-center justify-center py-4"
> >
<div <div id="release-notes" class="py-42 flex min-h-screen w-full flex-col justify-center gap-8">
id="release-notes"
class="py-42 flex min-h-screen gap-8 w-full flex-col justify-center"
>
<Description class="mt-48 text-6xl font-bold">Changelog</Description> <Description class="mt-48 text-6xl font-bold">Changelog</Description>
<p <p
class="text-base opacity-55" class="text-base opacity-55"
set:html={releaseNotes.topSection.description.replaceAll( set:html={releaseNotes.topSection.description.replaceAll(
'{latestVersion}', '{latestVersion}',
releaseNotesData[0].version, releaseNotesData[0].version
)} )}
/> />
<div <div class="mt-8 flex w-fit flex-col gap-4 sm:mr-0 sm:flex-row sm:items-center">
class="mt-8 flex w-fit flex-col gap-4 sm:mr-0 sm:flex-row sm:items-center"
>
<Button class="flex" isPrimary href="/donate"> <Button class="flex" isPrimary href="/donate">
{releaseNotes.list.support} {releaseNotes.list.support}
</Button> </Button>
@ -44,12 +39,11 @@ const {
</Button> </Button>
</div> </div>
{ {
releaseNotesTwilight.features.length || releaseNotesTwilight.features.length || releaseNotesTwilight.fixes.length ? (
releaseNotesTwilight.fixes.length ? (
<ReleaseNoteItem {...releaseNotesTwilight} isTwilight /> <ReleaseNoteItem {...releaseNotesTwilight} isTwilight />
) : null ) : null
} }
{releaseNotesData.map((notes: any) => <ReleaseNoteItem {...notes} />)} {releaseNotesData.map(notes => <ReleaseNoteItem {...notes} />)}
</div> </div>
</main> </main>
<Button href="#" id="scroll-top" isPrimary class="fixed bottom-8 right-8"> <Button href="#" id="scroll-top" isPrimary class="fixed bottom-8 right-8">
@ -67,7 +61,7 @@ const {
<ModalBody> <ModalBody>
<div id="version-list" class="flex flex-col gap-2 text-xl text-dark"> <div id="version-list" class="flex flex-col gap-2 text-xl text-dark">
{ {
releaseNotesData.map((note) => ( releaseNotesData.map(note => (
<button <button
aria-label={`Navigate to version ${note.version}`} aria-label={`Navigate to version ${note.version}`}
class="w-full text-left transition-colors duration-150 hover:text-coral" class="w-full text-left transition-colors duration-150 hover:text-coral"
@ -81,7 +75,7 @@ const {
</ModalBody> </ModalBody>
</Modal> </Modal>
<script> <script>
import { openModal, closeModal } from 'free-astro-components' import { closeModal, openModal } from 'free-astro-components'
const scrollTopButton = document.getElementById('scroll-top') const scrollTopButton = document.getElementById('scroll-top')
const versionButton = document.getElementById('navigate-to-version') const versionButton = document.getElementById('navigate-to-version')
@ -110,11 +104,9 @@ const {
if (!version) return if (!version) return
window.location.hash = version window.location.hash = version
const versionDetails = document const versionDetails = document.getElementById(version)?.getElementsByTagName('details')
.getElementById(version)
?.getElementsByTagName('details')
if (versionDetails && versionDetails.length > 0) { if (versionDetails && versionDetails.length > 0) {
Array.from(versionDetails).forEach((accordion) => { Array.from(versionDetails).forEach(accordion => {
accordion.setAttribute('open', '') accordion.setAttribute('open', '')
}) })
} }
@ -131,7 +123,7 @@ const {
versionButton?.addEventListener('click', openVersionModal) versionButton?.addEventListener('click', openVersionModal)
versionList?.addEventListener('click', navigateToVersion) versionList?.addEventListener('click', navigateToVersion)
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', e => {
if (e.key === 'Escape' && modal?.hasAttribute('open')) { if (e.key === 'Escape' && modal?.hasAttribute('open')) {
closeModal(modal) closeModal(modal)
// Remove scroll lock if present // Remove scroll lock if present

View file

@ -14,10 +14,6 @@ const {
<Layout title={layout.welcome.title} description={layout.welcome.description}> <Layout title={layout.welcome.title} description={layout.welcome.description}>
<main class="container"> <main class="container">
<Features <Features title1={welcome.title[0]} title2={welcome.title[1]} title3={welcome.title[2]} />
title1={welcome.title[0]}
title2={welcome.title[1]}
title3={welcome.title[2]}
/>
</main> </main>
</Layout> </Layout>

View file

@ -27,24 +27,14 @@ if (latestVersion.version.split('.').length > 2 && whatsNewText[1] !== latestVer
} }
--- ---
<Layout <Layout title={layout.whatsNew.title.replace('{latestVersion.version}', latestVersion.version)}>
title={layout.whatsNew.title.replace(
'{latestVersion.version}',
latestVersion.version,
)}
>
<main <main
class="xl:mt-22 container flex flex-col gap-12 py-12 xl:grid xl:min-h-[calc(100vh-12rem)] xl:grid-cols-[2fr_3fr]" class="xl:mt-22 container flex flex-col gap-12 py-12 xl:grid xl:min-h-[calc(100vh-12rem)] xl:grid-cols-[2fr_3fr]"
> >
<div class="flex flex-col gap-8"> <div class="flex flex-col gap-8">
<div> <div>
<Description class="text-5xl font-bold md:text-6xl" <Description class="text-5xl font-bold md:text-6xl"
>{ >{whatsNew.title.replace('{latestVersion.version}', latestVersion.version)}</Description
whatsNew.title.replace(
'{latestVersion.version}',
latestVersion.version,
)
}</Description
> >
<Description>{latestVersion.date}</Description> <Description>{latestVersion.date}</Description>
</div> </div>
@ -52,10 +42,7 @@ if (latestVersion.version.split('.').length > 2 && whatsNewText[1] !== latestVer
<Fragment set:html={whatsNewText[0].replace(/\n/g, '<br>')} /> <Fragment set:html={whatsNewText[0].replace(/\n/g, '<br>')} />
</div> </div>
<ul class="hidden list-disc flex-col gap-2 xl:container xl:flex"> <ul class="hidden list-disc flex-col gap-2 xl:container xl:flex">
<a <a href="https://github.com/zen-browser/desktop/issues/new/choose" target="_blank">
href="https://github.com/zen-browser/desktop/issues/new/choose"
target="_blank"
>
<li> <li>
<Description class="text-base font-bold"> <Description class="text-base font-bold">
{whatsNew.reportIssue} {whatsNew.reportIssue}

View file

@ -1,6 +1,6 @@
import releaseNotesStable from './release-notes/stable.json' import releaseNotesStable from './release-notes/stable.json'
interface FixWithIssue { type FixWithIssue = {
description: string description: string
issue?: number issue?: number
} }
@ -9,7 +9,7 @@ type Fix = string | FixWithIssue
export type BreakingChange = string | { description: string; link: string } export type BreakingChange = string | { description: string; link: string }
export interface ReleaseNote { export type ReleaseNote = {
version: string version: string
date?: string // optional for twilight date?: string // optional for twilight
extra?: string extra?: string

View file

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

View file

@ -1,5 +1,6 @@
import { experimental_AstroContainer as AstroContainer } from 'astro/container' import { experimental_AstroContainer as AstroContainer } from 'astro/container'
import { beforeEach, describe, expect, it } from 'vitest' import { beforeEach, describe, expect, it } from 'vitest'
import Button from '~/components/Button.astro' import Button from '~/components/Button.astro'
describe('<Button />', () => { describe('<Button />', () => {

View file

@ -1,5 +1,6 @@
import { experimental_AstroContainer as AstroContainer } from 'astro/container' import { experimental_AstroContainer as AstroContainer } from 'astro/container'
import { beforeEach, describe, expect, it } from 'vitest' import { beforeEach, describe, expect, it } from 'vitest'
import ButtonCard from '~/components/download/ButtonCard.astro' import ButtonCard from '~/components/download/ButtonCard.astro'
describe('<ButtonCard />', () => { describe('<ButtonCard />', () => {

View file

@ -1,5 +1,6 @@
import { experimental_AstroContainer as AstroContainer } from 'astro/container' import { experimental_AstroContainer as AstroContainer } from 'astro/container'
import { beforeEach, describe, expect, it } from 'vitest' import { beforeEach, describe, expect, it } from 'vitest'
import PlatformDownload from '~/components/download/PlatformDownload.astro' import PlatformDownload from '~/components/download/PlatformDownload.astro'
const mockIcon = ['<svg></svg>'] const mockIcon = ['<svg></svg>']

View file

@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { getReleasesWithChecksums } from '~/components/download/release-data' import { getReleasesWithChecksums } from '~/components/download/release-data'
describe('getReleasesWithChecksums', () => { describe('getReleasesWithChecksums', () => {

View file

@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test' import { expect, test, type BrowserContextOptions, type Page } from '@playwright/test'
import type { BrowserContextOptions, Page } from '@playwright/test'
import { getReleasesWithChecksums } from '~/components/download/release-data' import { getReleasesWithChecksums } from '~/components/download/release-data'
import { CONSTANT } from '~/constants' import { CONSTANT } from '~/constants'
@ -12,7 +12,7 @@ const getPlatformButton = (page: Page, platform: string) =>
page.locator(`button.platform-selector[data-platform='${platform}']`) page.locator(`button.platform-selector[data-platform='${platform}']`)
// Helper to get the platform download link // Helper to get the platform download link
const getPlatformDownloadLink = (page: Page, platform: string, label: string) => const _ = (page: Page, platform: string, label: string) =>
page.locator(`#${platform}-downloads .download-link:has-text('${label}')`) page.locator(`#${platform}-downloads .download-link:has-text('${label}')`)
const platformConfigs: { name: string; userAgent: string; platform: string }[] = [ const platformConfigs: { name: string; userAgent: string; platform: string }[] = [
@ -30,7 +30,8 @@ const platformConfigs: { name: string; userAgent: string; platform: string }[] =
}, },
{ {
name: 'linux', name: 'linux',
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', userAgent:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
platform: 'Linux x86_64', platform: 'Linux x86_64',
}, },
] ]
@ -48,7 +49,7 @@ test.describe('Download page default tab per platform', () => {
await expect(getPlatformSection(page, name)).toBeVisible() await expect(getPlatformSection(page, name)).toBeVisible()
await expect(getPlatformButton(page, name)).toHaveAttribute('data-active', 'true') await expect(getPlatformButton(page, name)).toHaveAttribute('data-active', 'true')
// Other platforms should not be active // Other platforms should not be active
for (const other of platformConfigs.filter((p) => p.name !== name)) { for (const other of platformConfigs.filter(p => p.name !== name)) {
await expect(getPlatformSection(page, other.name)).toBeHidden() await expect(getPlatformSection(page, other.name)).toBeHidden()
await expect(getPlatformButton(page, other.name)).not.toHaveAttribute('data-active', 'true') await expect(getPlatformButton(page, other.name)).not.toHaveAttribute('data-active', 'true')
} }
@ -66,9 +67,12 @@ test.describe('Download page platform detection and tab switching', () => {
await expect(getPlatformSection(page, platform)).toBeVisible() await expect(getPlatformSection(page, platform)).toBeVisible()
await expect(getPlatformButton(page, platform)).toHaveAttribute('data-active', 'true') await expect(getPlatformButton(page, platform)).toHaveAttribute('data-active', 'true')
// other platform sections should be hidden // other platform sections should be hidden
for (const otherPlatform of platforms.filter((p) => p !== platform)) { for (const otherPlatform of platforms.filter(p => p !== platform)) {
await expect(getPlatformSection(page, otherPlatform)).toBeHidden() await expect(getPlatformSection(page, otherPlatform)).toBeHidden()
await expect(getPlatformButton(page, otherPlatform)).not.toHaveAttribute('data-active', 'true') await expect(getPlatformButton(page, otherPlatform)).not.toHaveAttribute(
'data-active',
'true'
)
} }
} }
}) })
@ -81,7 +85,11 @@ test.describe('Download page download links', () => {
return { return {
mac: [releases.macos.universal], mac: [releases.macos.universal],
windows: [releases.windows.x86_64, releases.windows.arm64], windows: [releases.windows.x86_64, releases.windows.arm64],
linux: [releases.linux.x86_64.tarball, releases.linux.aarch64.tarball, releases.linux.flathub.all], linux: [
releases.linux.x86_64.tarball,
releases.linux.aarch64.tarball,
releases.linux.flathub.all,
],
} }
} }
@ -92,7 +100,9 @@ test.describe('Download page download links', () => {
await page.waitForLoadState('domcontentloaded') await page.waitForLoadState('domcontentloaded')
for (const platform of platforms) { for (const platform of platforms) {
await getPlatformButton(page, platform).click() await getPlatformButton(page, platform).click()
for (const { label, link } of platformLinkSelectors[platform as keyof typeof platformLinkSelectors]) { for (const { label, link } of platformLinkSelectors[
platform as keyof typeof platformLinkSelectors
]) {
const downloadLink = page.locator(`#${platform}-downloads .download-link[href="${link}"]`) const downloadLink = page.locator(`#${platform}-downloads .download-link[href="${link}"]`)
await expect(downloadLink).toContainText(label) await expect(downloadLink).toContainText(label)
await expect(downloadLink).toHaveAttribute('href', link) await expect(downloadLink).toHaveAttribute('href', link)

View file

@ -1,4 +1,5 @@
import { vi } from 'vitest' import { vi } from 'vitest'
import translation from '~/i18n/en/translation.json' import translation from '~/i18n/en/translation.json'
vi.mock('~/utils/i18n', () => ({ vi.mock('~/utils/i18n', () => ({

View file

@ -1,4 +1,5 @@
import type { GetStaticPaths } from 'astro' import { type GetStaticPaths } from 'astro'
import { CONSTANT } from '~/constants' import { CONSTANT } from '~/constants'
import UI_EN from '~/i18n/en/translation.json' import UI_EN from '~/i18n/en/translation.json'
@ -44,7 +45,9 @@ export const locales = CONSTANT.I18N.LOCALES.map(({ value }) => value)
* List of locales excluding the default locale * List of locales excluding the default locale
* @type {Locale[]} * @type {Locale[]}
*/ */
const otherLocales = CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE) const otherLocales = CONSTANT.I18N.LOCALES.filter(
({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE
)
/** /**
* Retrieves locales other than the default locale * Retrieves locales other than the default locale
@ -94,7 +97,7 @@ export const getUI = (locale?: Locale | string): UI => {
const result = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj } const result = Array.isArray(defaultObj) ? [...defaultObj] : { ...defaultObj }
// Merge properties from the default object // Merge properties from the default object
for (const key of Object.keys(defaultObj) as Array<keyof T>) { for (const key of Object.keys(defaultObj) as (keyof T)[]) {
const defaultValue = defaultObj[key] const defaultValue = defaultObj[key]
const overrideValue = overrideObj[key] const overrideValue = overrideObj[key]
@ -106,7 +109,10 @@ export const getUI = (locale?: Locale | string): UI => {
typeof overrideValue === 'object' typeof overrideValue === 'object'
) { ) {
// Type assertion to handle nested merging // Type assertion to handle nested merging
;(result as Record<keyof T, unknown>)[key] = deepMerge(defaultValue as object, overrideValue as Partial<object>) ;(result as Record<keyof T, unknown>)[key] = deepMerge(
defaultValue as object,
overrideValue as Partial<object>
)
} else if (overrideValue !== undefined) { } else if (overrideValue !== undefined) {
// Override with the new value if it exists // Override with the new value if it exists
;(result as Record<keyof T, unknown>)[key] = overrideValue ;(result as Record<keyof T, unknown>)[key] = overrideValue
@ -114,7 +120,7 @@ export const getUI = (locale?: Locale | string): UI => {
} }
// Add any new properties from overrideObj // Add any new properties from overrideObj
for (const key of Object.keys(overrideObj) as Array<keyof T>) { for (const key of Object.keys(overrideObj) as (keyof T)[]) {
if (!(key in defaultObj)) { if (!(key in defaultObj)) {
;(result as Record<keyof T, unknown>)[key] = overrideObj[key] ;(result as Record<keyof T, unknown>)[key] = overrideObj[key]
} }
@ -137,12 +143,14 @@ export const getStaticPaths = (() => {
params: { locale: undefined }, params: { locale: undefined },
props: { locale: CONSTANT.I18N.DEFAULT_LOCALE }, props: { locale: CONSTANT.I18N.DEFAULT_LOCALE },
}, },
...CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE).map(({ value }) => ({ ...CONSTANT.I18N.LOCALES.filter(({ value }) => value !== CONSTANT.I18N.DEFAULT_LOCALE).map(
({ value }) => ({
params: { locale: value }, params: { locale: value },
props: { props: {
locale: value, locale: value,
}, },
})), })
),
] ]
}) satisfies GetStaticPaths }) satisfies GetStaticPaths

View file

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