mirror of
https://github.com/zen-browser/www.git
synced 2025-07-07 08:55:32 +02:00
feat(prettier): add prettier formatting
This commit is contained in:
parent
01f4dac75d
commit
7fafa6bc69
85 changed files with 5670 additions and 2788 deletions
|
@ -1,28 +1,28 @@
|
||||||
import type { Linter } from "eslint";
|
import { type Linter } from 'eslint'
|
||||||
// @ts-expect-error - no types available
|
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
||||||
import jsxA11y from "eslint-plugin-jsx-a11y";
|
|
||||||
import { astroFiles } from "./shared";
|
import { astroFiles } from './shared'
|
||||||
|
|
||||||
export const astroConfig: Linter.Config = {
|
export const astroConfig: Linter.Config = {
|
||||||
name: "eslint/astro",
|
name: 'eslint/astro',
|
||||||
files: astroFiles,
|
files: astroFiles,
|
||||||
plugins: {
|
plugins: {
|
||||||
"jsx-a11y": jsxA11y,
|
'jsx-a11y': jsxA11y,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// Astro specific adjustments
|
// Astro specific adjustments
|
||||||
"@typescript-eslint/no-unused-vars": "off", // Astro components can have unused props
|
'@typescript-eslint/no-unused-vars': 'off', // Astro components can have unused props
|
||||||
"import/no-unresolved": "off",
|
'import/no-unresolved': 'off',
|
||||||
"no-undef": "off", // Astro has global variables like Astro
|
'no-undef': 'off', // Astro has global variables like Astro
|
||||||
|
|
||||||
// A11y rules for Astro
|
// A11y rules for Astro
|
||||||
"jsx-a11y/alt-text": "error",
|
'jsx-a11y/alt-text': 'error',
|
||||||
"jsx-a11y/anchor-has-content": "error",
|
'jsx-a11y/anchor-has-content': 'error',
|
||||||
"jsx-a11y/anchor-is-valid": "error",
|
'jsx-a11y/anchor-is-valid': 'error',
|
||||||
"jsx-a11y/click-events-have-key-events": "error",
|
'jsx-a11y/click-events-have-key-events': 'error',
|
||||||
"jsx-a11y/interactive-supports-focus": "error",
|
'jsx-a11y/interactive-supports-focus': 'error',
|
||||||
"jsx-a11y/no-redundant-roles": "error",
|
'jsx-a11y/no-redundant-roles': 'error',
|
||||||
"jsx-a11y/img-redundant-alt": "error",
|
'jsx-a11y/img-redundant-alt': 'error',
|
||||||
"jsx-a11y/no-access-key": "error",
|
'jsx-a11y/no-access-key': 'error',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
131
.eslint/base.ts
131
.eslint/base.ts
|
@ -1,72 +1,73 @@
|
||||||
import type { Linter } from "eslint";
|
import { type Linter } from 'eslint'
|
||||||
import { sharedFiles } from "./shared";
|
|
||||||
|
import { sharedFiles } from './shared'
|
||||||
|
|
||||||
export const baseConfig: Linter.Config = {
|
export const baseConfig: Linter.Config = {
|
||||||
name: "eslint/base",
|
name: 'eslint/base',
|
||||||
files: sharedFiles,
|
files: sharedFiles,
|
||||||
rules: {
|
rules: {
|
||||||
"constructor-super": "error",
|
'constructor-super': 'error',
|
||||||
"for-direction": "error",
|
'for-direction': 'error',
|
||||||
"getter-return": "error",
|
'getter-return': 'error',
|
||||||
"no-async-promise-executor": "error",
|
'no-async-promise-executor': 'error',
|
||||||
"no-case-declarations": "error",
|
'no-case-declarations': 'error',
|
||||||
"no-class-assign": "error",
|
'no-class-assign': 'error',
|
||||||
"no-compare-neg-zero": "error",
|
'no-compare-neg-zero': 'error',
|
||||||
"no-cond-assign": "error",
|
'no-cond-assign': 'error',
|
||||||
"no-const-assign": "error",
|
'no-const-assign': 'error',
|
||||||
"no-constant-binary-expression": "error",
|
'no-constant-binary-expression': 'error',
|
||||||
"no-constant-condition": "error",
|
'no-constant-condition': 'error',
|
||||||
"no-control-regex": "error",
|
'no-control-regex': 'error',
|
||||||
"no-debugger": "error",
|
'no-debugger': 'error',
|
||||||
"no-delete-var": "error",
|
'no-delete-var': 'error',
|
||||||
"no-dupe-args": "error",
|
'no-dupe-args': 'error',
|
||||||
"no-dupe-class-members": "error",
|
'no-dupe-class-members': 'error',
|
||||||
"no-dupe-else-if": "error",
|
'no-dupe-else-if': 'error',
|
||||||
"no-dupe-keys": "error",
|
'no-dupe-keys': 'error',
|
||||||
"no-duplicate-case": "error",
|
'no-duplicate-case': 'error',
|
||||||
"no-empty-character-class": "error",
|
'no-empty-character-class': 'error',
|
||||||
"no-empty-pattern": "error",
|
'no-empty-pattern': 'error',
|
||||||
"no-empty-static-block": "error",
|
'no-empty-static-block': 'error',
|
||||||
"no-ex-assign": "error",
|
'no-ex-assign': 'error',
|
||||||
"no-fallthrough": "error",
|
'no-fallthrough': 'error',
|
||||||
"no-func-assign": "error",
|
'no-func-assign': 'error',
|
||||||
"no-global-assign": "error",
|
'no-global-assign': 'error',
|
||||||
"no-import-assign": "error",
|
'no-import-assign': 'error',
|
||||||
"no-invalid-regexp": "error",
|
'no-invalid-regexp': 'error',
|
||||||
"no-loss-of-precision": "error",
|
'no-loss-of-precision': 'error',
|
||||||
"no-misleading-character-class": "error",
|
'no-misleading-character-class': 'error',
|
||||||
"no-new-native-nonconstructor": "error",
|
'no-new-native-nonconstructor': 'error',
|
||||||
"no-nonoctal-decimal-escape": "error",
|
'no-nonoctal-decimal-escape': 'error',
|
||||||
"no-obj-calls": "error",
|
'no-obj-calls': 'error',
|
||||||
"no-octal": "error",
|
'no-octal': 'error',
|
||||||
"no-redeclare": "error",
|
'no-redeclare': 'error',
|
||||||
"no-regex-spaces": "error",
|
'no-regex-spaces': 'error',
|
||||||
"no-self-assign": "error",
|
'no-self-assign': 'error',
|
||||||
"no-setter-return": "error",
|
'no-setter-return': 'error',
|
||||||
"no-this-before-super": "error",
|
'no-this-before-super': 'error',
|
||||||
"no-undef": "error",
|
'no-undef': 'error',
|
||||||
"no-unexpected-multiline": "error",
|
'no-unexpected-multiline': 'error',
|
||||||
"no-unreachable": "error",
|
'no-unreachable': 'error',
|
||||||
"no-unsafe-finally": "error",
|
'no-unsafe-finally': 'error',
|
||||||
"no-unsafe-negation": "error",
|
'no-unsafe-negation': 'error',
|
||||||
"no-unsafe-optional-chaining": "error",
|
'no-unsafe-optional-chaining': 'error',
|
||||||
"no-unused-labels": "error",
|
'no-unused-labels': 'error',
|
||||||
"no-unused-private-class-members": "error",
|
'no-unused-private-class-members': 'error',
|
||||||
"no-useless-backreference": "error",
|
'no-useless-backreference': 'error',
|
||||||
"no-useless-catch": "error",
|
'no-useless-catch': 'error',
|
||||||
"no-var": "error",
|
'no-var': 'error',
|
||||||
"no-with": "error",
|
'no-with': 'error',
|
||||||
"require-yield": "error",
|
'require-yield': 'error',
|
||||||
"use-isnan": "error",
|
'use-isnan': 'error',
|
||||||
"valid-typeof": "error",
|
'valid-typeof': 'error',
|
||||||
|
|
||||||
// Additional base rules
|
// Additional base rules
|
||||||
"no-console": ["warn", { allow: ["warn", "error"] }],
|
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||||
"prefer-const": "error",
|
'prefer-const': 'error',
|
||||||
"object-shorthand": "error",
|
'object-shorthand': 'error',
|
||||||
"prefer-template": "error",
|
'prefer-template': 'error',
|
||||||
curly: ["error", "all"],
|
curly: ['error', 'all'],
|
||||||
eqeqeq: ["error", "always"],
|
eqeqeq: ['error', 'always'],
|
||||||
"no-implicit-coercion": "error",
|
'no-implicit-coercion': 'error',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Linter } from "eslint";
|
import { type Linter } from "eslint";
|
||||||
|
|
||||||
import { configFiles } from "./shared";
|
import { configFiles } from "./shared";
|
||||||
|
|
||||||
export const configFilesConfig: Linter.Config = {
|
export const configFilesConfig: Linter.Config = {
|
||||||
|
@ -8,6 +9,7 @@ export const configFilesConfig: Linter.Config = {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
"import/no-default-export": "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-assignment": "off",
|
||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
"@typescript-eslint/no-unsafe-call": "off",
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import type { Linter } from "eslint";
|
import { type Linter } from 'eslint'
|
||||||
// @ts-expect-error - no types available
|
// @ts-expect-error - no types available
|
||||||
import importPlugin from "eslint-plugin-import";
|
import importPlugin from 'eslint-plugin-import'
|
||||||
import { sharedFiles } from "./shared";
|
|
||||||
|
import { sharedFiles } from './shared'
|
||||||
|
|
||||||
export const importConfigArray: Linter.Config[] = [
|
export const importConfigArray: Linter.Config[] = [
|
||||||
{
|
{
|
||||||
name: "eslint/import",
|
name: 'eslint/import',
|
||||||
files: sharedFiles,
|
files: sharedFiles,
|
||||||
plugins: {
|
plugins: {
|
||||||
import: importPlugin,
|
import: importPlugin,
|
||||||
|
@ -14,50 +15,50 @@ export const importConfigArray: Linter.Config[] = [
|
||||||
...importPlugin.configs.recommended.rules,
|
...importPlugin.configs.recommended.rules,
|
||||||
...importPlugin.configs.typescript.rules,
|
...importPlugin.configs.typescript.rules,
|
||||||
|
|
||||||
"import/order": [
|
'import/order': [
|
||||||
"error",
|
'error',
|
||||||
{
|
{
|
||||||
groups: [
|
groups: [
|
||||||
"builtin",
|
'builtin',
|
||||||
"external",
|
'external',
|
||||||
"internal",
|
'internal',
|
||||||
["parent", "sibling"],
|
['parent', 'sibling'],
|
||||||
"index",
|
'index',
|
||||||
"object",
|
'object',
|
||||||
"type",
|
'type',
|
||||||
],
|
],
|
||||||
"newlines-between": "always",
|
'newlines-between': 'always',
|
||||||
alphabetize: {
|
alphabetize: {
|
||||||
order: "asc",
|
order: 'asc',
|
||||||
caseInsensitive: true,
|
caseInsensitive: true,
|
||||||
},
|
},
|
||||||
pathGroups: [
|
pathGroups: [
|
||||||
{
|
{
|
||||||
pattern: "@/**",
|
pattern: '@/**',
|
||||||
group: "internal",
|
group: 'internal',
|
||||||
position: "before",
|
position: 'before',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pathGroupsExcludedImportTypes: ["builtin"],
|
pathGroupsExcludedImportTypes: ['builtin'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"import/no-unresolved": "off", // TypeScript handles this
|
'import/no-unresolved': 'off', // TypeScript handles this
|
||||||
"import/no-duplicates": ["error", { "prefer-inline": true }],
|
'import/no-duplicates': ['error', { 'prefer-inline': true }],
|
||||||
"import/consistent-type-specifier-style": ["error", "prefer-inline"],
|
'import/consistent-type-specifier-style': ['error', 'prefer-inline'],
|
||||||
"import/first": "error",
|
'import/first': 'error',
|
||||||
"import/newline-after-import": "error",
|
'import/newline-after-import': 'error',
|
||||||
"import/no-default-export": "off", // Allow default exports
|
'import/no-default-export': 'off', // Allow default exports
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
"import/resolver": {
|
'import/resolver': {
|
||||||
typescript: {
|
typescript: {
|
||||||
alwaysTryTypes: true,
|
alwaysTryTypes: true,
|
||||||
},
|
},
|
||||||
node: true,
|
node: true,
|
||||||
},
|
},
|
||||||
"import/parsers": {
|
'import/parsers': {
|
||||||
"@typescript-eslint/parser": [".ts", ".tsx"],
|
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import type { Linter } from "eslint";
|
import { type Linter } from 'eslint'
|
||||||
import { javascriptFiles } from "./shared";
|
|
||||||
|
import { javascriptFiles } from './shared'
|
||||||
|
|
||||||
export const javascriptConfig: Linter.Config = {
|
export const javascriptConfig: Linter.Config = {
|
||||||
name: "eslint/javascript",
|
name: 'eslint/javascript',
|
||||||
files: javascriptFiles,
|
files: javascriptFiles,
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: 'latest',
|
||||||
sourceType: "module",
|
sourceType: 'module',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true,
|
jsx: true,
|
||||||
|
@ -14,22 +15,22 @@ export const javascriptConfig: Linter.Config = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"no-unused-vars": [
|
'no-unused-vars': [
|
||||||
"error",
|
'error',
|
||||||
{
|
{
|
||||||
argsIgnorePattern: "^_",
|
argsIgnorePattern: '^_',
|
||||||
varsIgnorePattern: "^_",
|
varsIgnorePattern: '^_',
|
||||||
ignoreRestSiblings: true,
|
ignoreRestSiblings: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"no-console": ["warn", { allow: ["warn", "error"] }],
|
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||||
"no-debugger": "error",
|
'no-debugger': 'error',
|
||||||
"prefer-const": "error",
|
'prefer-const': 'error',
|
||||||
"no-var": "error",
|
'no-var': 'error',
|
||||||
"object-shorthand": "error",
|
'object-shorthand': 'error',
|
||||||
"prefer-template": "error",
|
'prefer-template': 'error',
|
||||||
curly: ["error", "all"],
|
curly: ['error', 'all'],
|
||||||
eqeqeq: ["error", "always"],
|
eqeqeq: ['error', 'always'],
|
||||||
"no-implicit-coercion": "error",
|
'no-implicit-coercion': 'error',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
import type { Linter } from "eslint";
|
import { type Linter } from 'eslint'
|
||||||
// @ts-expect-error - no types available
|
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
||||||
import jsxA11y from "eslint-plugin-jsx-a11y";
|
|
||||||
import { astroFiles, javascriptFiles, typescriptFiles } from "./shared";
|
import { astroFiles, javascriptFiles, typescriptFiles } from './shared'
|
||||||
|
|
||||||
export const jsxA11yConfig: Linter.Config = {
|
export const jsxA11yConfig: Linter.Config = {
|
||||||
name: "eslint/jsx-a11y",
|
name: 'eslint/jsx-a11y',
|
||||||
files: [
|
files: [
|
||||||
...astroFiles,
|
...astroFiles,
|
||||||
...typescriptFiles.filter((f) => f.includes("tsx")),
|
...typescriptFiles.filter(f => f.includes('tsx')),
|
||||||
...javascriptFiles.filter((f) => f.includes("jsx")),
|
...javascriptFiles.filter(f => f.includes('jsx')),
|
||||||
],
|
],
|
||||||
plugins: {
|
plugins: {
|
||||||
"jsx-a11y": jsxA11y,
|
'jsx-a11y': jsxA11y,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...jsxA11y.configs.recommended.rules,
|
...jsxA11y.configs.recommended.rules,
|
||||||
|
|
||||||
// Additional a11y rules
|
// Additional a11y rules
|
||||||
"jsx-a11y/alt-text": "error",
|
'jsx-a11y/alt-text': 'error',
|
||||||
"jsx-a11y/anchor-has-content": "error",
|
'jsx-a11y/anchor-has-content': 'error',
|
||||||
"jsx-a11y/anchor-is-valid": "error",
|
'jsx-a11y/anchor-is-valid': 'error',
|
||||||
"jsx-a11y/click-events-have-key-events": "error",
|
'jsx-a11y/click-events-have-key-events': 'error',
|
||||||
"jsx-a11y/interactive-supports-focus": "error",
|
'jsx-a11y/interactive-supports-focus': 'error',
|
||||||
"jsx-a11y/no-redundant-roles": "error",
|
'jsx-a11y/no-redundant-roles': 'error',
|
||||||
"jsx-a11y/img-redundant-alt": "error",
|
'jsx-a11y/img-redundant-alt': 'error',
|
||||||
"jsx-a11y/no-access-key": "error",
|
'jsx-a11y/no-access-key': 'error',
|
||||||
"jsx-a11y/label-has-associated-control": "error",
|
'jsx-a11y/label-has-associated-control': 'error',
|
||||||
"jsx-a11y/no-autofocus": "warn",
|
'jsx-a11y/no-autofocus': 'warn',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import type { Linter } from "eslint";
|
import { type Linter } from "eslint";
|
||||||
import react from "eslint-plugin-react";
|
import react from "eslint-plugin-react";
|
||||||
|
import * as reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
|
||||||
import { javascriptFiles, typescriptFiles } from "./shared";
|
import { javascriptFiles, typescriptFiles } from "./shared";
|
||||||
|
|
||||||
export const reactConfig: Linter.Config = {
|
export const reactConfig: Linter.Config = {
|
||||||
|
@ -14,7 +16,7 @@ export const reactConfig: Linter.Config = {
|
||||||
rules: {
|
rules: {
|
||||||
...react.configs.recommended.rules,
|
...react.configs.recommended.rules,
|
||||||
|
|
||||||
"react/react-in-jsx-scope": "off", // Not needed in modern React/Preact
|
"react/react-in-jsx-scope": "off", // Not needed in React 17+
|
||||||
"react/prop-types": "off", // Using TypeScript
|
"react/prop-types": "off", // Using TypeScript
|
||||||
"react/jsx-uses-react": "off",
|
"react/jsx-uses-react": "off",
|
||||||
"react/jsx-uses-vars": "error",
|
"react/jsx-uses-vars": "error",
|
||||||
|
@ -37,13 +39,11 @@ export const reactConfig: Linter.Config = {
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
version: "detect",
|
version: "18.2", // React version
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
import reactHooks from "eslint-plugin-react-hooks";
|
|
||||||
|
|
||||||
export const reactHooksConfig: Linter.Config = {
|
export const reactHooksConfig: Linter.Config = {
|
||||||
name: "eslint/react-hooks",
|
name: "eslint/react-hooks",
|
||||||
files: [
|
files: [
|
||||||
|
|
|
@ -1,41 +1,35 @@
|
||||||
export const sharedFiles = [
|
export const sharedFiles = [
|
||||||
"**/*.js",
|
'**/*.js',
|
||||||
"**/*.cjs",
|
'**/*.cjs',
|
||||||
"**/*.mjs",
|
'**/*.mjs',
|
||||||
"**/*.jsx",
|
'**/*.jsx',
|
||||||
"**/*.ts",
|
'**/*.ts',
|
||||||
"**/*.cts",
|
'**/*.cts',
|
||||||
"**/*.mts",
|
'**/*.mts',
|
||||||
"**/*.tsx",
|
'**/*.tsx',
|
||||||
"**/*.d.ts",
|
'**/*.d.ts',
|
||||||
];
|
]
|
||||||
|
|
||||||
export const sharedTestFiles = [
|
export const sharedTestFiles = [
|
||||||
"**/*.test.{ts,tsx,js,jsx}",
|
'**/*.test.{ts,tsx,js,jsx}',
|
||||||
"**/*.spec.{ts,tsx,js,jsx}",
|
'**/*.spec.{ts,tsx,js,jsx}',
|
||||||
"**/tests/**/*.{ts,tsx,js,jsx}",
|
'**/tests/**/*.{ts,tsx,js,jsx}',
|
||||||
"**/__tests__/**/*.{ts,tsx,js,jsx}",
|
'**/__tests__/**/*.{ts,tsx,js,jsx}',
|
||||||
];
|
]
|
||||||
|
|
||||||
export const astroFiles = ["**/*.astro"];
|
export const astroFiles = ['**/*.astro']
|
||||||
|
|
||||||
export const typescriptFiles = [
|
export const typescriptFiles = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '**/*.d.ts']
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
"**/*.mts",
|
|
||||||
"**/*.cts",
|
|
||||||
"**/*.d.ts",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const javascriptFiles = ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"];
|
export const javascriptFiles = ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs']
|
||||||
|
|
||||||
export const configFiles = [
|
export const configFiles = [
|
||||||
"*.config.{ts,js,mjs}",
|
'*.config.{ts,js,mjs}',
|
||||||
"**/*.config.{ts,js,mjs}",
|
'**/*.config.{ts,js,mjs}',
|
||||||
"**/vite.config.*",
|
'**/vite.config.*',
|
||||||
"**/vitest.config.*",
|
'**/vitest.config.*',
|
||||||
"**/playwright.config.*",
|
'**/playwright.config.*',
|
||||||
"**/astro.config.*",
|
'**/astro.config.*',
|
||||||
"**/tailwind.config.*",
|
'**/tailwind.config.*',
|
||||||
"**/eslint.config.*",
|
'**/eslint.config.*',
|
||||||
];
|
]
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import type { Linter } from "eslint";
|
import { type Linter } from 'eslint'
|
||||||
import { sharedTestFiles } from "./shared";
|
|
||||||
|
import { sharedTestFiles } from './shared'
|
||||||
|
|
||||||
export const testConfig: Linter.Config = {
|
export const testConfig: Linter.Config = {
|
||||||
name: "eslint/test",
|
name: 'eslint/test',
|
||||||
files: sharedTestFiles,
|
files: sharedTestFiles,
|
||||||
rules: {
|
rules: {
|
||||||
"no-console": "off",
|
'no-console': 'off',
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||||
"@typescript-eslint/no-unsafe-call": "off",
|
'@typescript-eslint/no-unsafe-call': 'off',
|
||||||
"@typescript-eslint/no-unsafe-return": "off",
|
'@typescript-eslint/no-unsafe-return': 'off',
|
||||||
"@typescript-eslint/no-unsafe-argument": "off",
|
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||||
"@typescript-eslint/unbound-method": "off",
|
'@typescript-eslint/unbound-method': 'off',
|
||||||
"import/no-extraneous-dependencies": "off",
|
'import/no-extraneous-dependencies': 'off',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import type { Linter } from "eslint";
|
import { type Linter } from 'eslint'
|
||||||
import { typescriptFiles } from "./shared";
|
|
||||||
|
import { typescriptFiles } from './shared'
|
||||||
|
|
||||||
export function createTypescriptConfig(tsConfigPath: string): Linter.Config {
|
export function createTypescriptConfig(tsConfigPath: string): Linter.Config {
|
||||||
return {
|
return {
|
||||||
name: "eslint/typescript",
|
name: 'eslint/typescript',
|
||||||
files: typescriptFiles,
|
files: typescriptFiles,
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: 'latest',
|
||||||
sourceType: "module",
|
sourceType: 'module',
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true,
|
jsx: true,
|
||||||
},
|
},
|
||||||
|
@ -18,27 +19,27 @@ export function createTypescriptConfig(tsConfigPath: string): Linter.Config {
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// Basic TypeScript rules that work without type information
|
// Basic TypeScript rules that work without type information
|
||||||
"@typescript-eslint/no-unused-vars": [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
"error",
|
'error',
|
||||||
{
|
{
|
||||||
argsIgnorePattern: "^_",
|
argsIgnorePattern: '^_',
|
||||||
varsIgnorePattern: "^_",
|
varsIgnorePattern: '^_',
|
||||||
ignoreRestSiblings: true,
|
ignoreRestSiblings: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||||
"@typescript-eslint/consistent-type-imports": [
|
'@typescript-eslint/consistent-type-imports': [
|
||||||
"error",
|
'error',
|
||||||
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
|
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
||||||
],
|
],
|
||||||
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
|
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
||||||
|
|
||||||
// Override base rules for TypeScript
|
// Override base rules for TypeScript
|
||||||
"no-unused-vars": "off", // Handled by TypeScript
|
'no-unused-vars': 'off', // Handled by TypeScript
|
||||||
"no-undef": "off", // TypeScript handles this
|
'no-undef': 'off', // TypeScript handles this
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
4
.github/workflows/ci-pipeline.yml
vendored
4
.github/workflows/ci-pipeline.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
biome:
|
format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: setup
|
needs: setup
|
||||||
steps:
|
steps:
|
||||||
|
@ -40,7 +40,7 @@ jobs:
|
||||||
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 Biome check
|
||||||
run: npx biome check ./src
|
run: npm run lint && npm run format:check
|
||||||
|
|
||||||
vitest:
|
vitest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
11
README.md
11
README.md
|
@ -9,8 +9,13 @@ Zen Browser Website
|
||||||
|
|
||||||
[](https://uptime.zen-browser.app)
|
[](https://uptime.zen-browser.app)
|
||||||
|
|
||||||
This repository contains the source code for the Zen Browser Website. We are thrilled to welcome you to our community. Before you start, please read this document to understand how you can contribute to this project.
|
This repository contains the source code for the Zen Browser Website. We are
|
||||||
|
thrilled to welcome you to our community. Before you start, please read this
|
||||||
|
document to understand how you can contribute to this project.
|
||||||
|
|
||||||
Zen Browser's website is built with [Astro.js](https://astro.build/), [TypeScript](https://www.typescriptlang.org/), and [Tailwind CSS](https://tailwindcss.com/).
|
Zen Browser's website is built with [Astro.js](https://astro.build/),
|
||||||
|
[TypeScript](https://www.typescriptlang.org/), and
|
||||||
|
[Tailwind CSS](https://tailwindcss.com/).
|
||||||
|
|
||||||
If you are interested in contributing to this project, please read the [Contributing Guidelines](https://docs.zen-browser.app/contribute/www).
|
If you are interested in contributing to this project, please read the
|
||||||
|
[Contributing Guidelines](https://docs.zen-browser.app/contribute/www).
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import tailwind from '@astrojs/tailwind'
|
import tailwind from "@astrojs/tailwind";
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig } from 'astro/config'
|
import { defineConfig } from "astro/config";
|
||||||
|
|
||||||
import preact from '@astrojs/preact'
|
import react from "@astrojs/react";
|
||||||
|
|
||||||
import sitemap from '@astrojs/sitemap'
|
import sitemap from "@astrojs/sitemap";
|
||||||
|
|
||||||
// https://astro.build/config
|
// 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",
|
||||||
locales: ['en'],
|
locales: ["en"],
|
||||||
routing: {
|
routing: {
|
||||||
fallbackType: 'rewrite',
|
fallbackType: "rewrite",
|
||||||
prefixDefaultLocale: false,
|
prefixDefaultLocale: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { includeIgnoreFile } from "@eslint/compat";
|
|
||||||
import type { TSESLint } from "@typescript-eslint/utils";
|
|
||||||
import prettierConfig from "eslint-config-prettier";
|
|
||||||
import astro from "eslint-plugin-astro";
|
|
||||||
import { dirname, resolve } from "node:path";
|
import { dirname, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
|
import { includeIgnoreFile } from "@eslint/compat";
|
||||||
|
import { type TSESLint } from "@typescript-eslint/utils";
|
||||||
|
import prettierConfig from "eslint-config-prettier";
|
||||||
|
import astro from "eslint-plugin-astro";
|
||||||
|
import tseslint, { configs } from "typescript-eslint";
|
||||||
|
|
||||||
// Import modular configurations
|
// Import modular configurations
|
||||||
import { astroConfig } from "./.eslint/astro";
|
import { astroConfig } from "./.eslint/astro";
|
||||||
|
@ -63,8 +64,8 @@ const config: TSESLint.FlatConfig.ConfigArray = tseslint.config(
|
||||||
ignoresConfig,
|
ignoresConfig,
|
||||||
baseConfig,
|
baseConfig,
|
||||||
// TypeScript ecosystem
|
// TypeScript ecosystem
|
||||||
...tseslint.configs.strict,
|
...configs.strict,
|
||||||
...tseslint.configs.stylistic,
|
...configs.stylistic,
|
||||||
createTypescriptConfig(tsConfigPath),
|
createTypescriptConfig(tsConfigPath),
|
||||||
|
|
||||||
// Import management
|
// Import management
|
||||||
|
|
5372
package-lock.json
generated
5372
package-lock.json
generated
File diff suppressed because it is too large
Load diff
24
package.json
24
package.json
|
@ -9,12 +9,10 @@
|
||||||
"preview": "astro preview --port 3000",
|
"preview": "astro preview --port 3000",
|
||||||
"wrangler": "wrangler",
|
"wrangler": "wrangler",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.astro",
|
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.astro",
|
||||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx,.astro --fix",
|
"lint:fix": "eslint ./src --ext .js,.jsx,.ts,.tsx,.astro --fix",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write ./src",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check ./src",
|
||||||
"biome:lint": "biome lint ./src",
|
|
||||||
"biome:format": "biome format ./src",
|
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"test": "npx vitest run",
|
"test": "npx vitest run",
|
||||||
"test:coverage": "npx vitest --coverage",
|
"test:coverage": "npx vitest --coverage",
|
||||||
|
@ -23,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",
|
||||||
|
@ -31,18 +29,20 @@
|
||||||
"@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",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"free-astro-components": "^1.2.0",
|
"free-astro-components": "^1.2.0",
|
||||||
"jiti": "^2.4.2",
|
|
||||||
"lucide-astro": "^0.460.0",
|
"lucide-astro": "^0.460.0",
|
||||||
"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",
|
||||||
|
@ -56,6 +56,7 @@
|
||||||
"@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/eslint-plugin": "^8.33.0",
|
||||||
|
@ -71,10 +72,11 @@
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"jiti": "^2.4.2",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"lint-staged": "^15.2.7",
|
"lint-staged": "^15.2.7",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "3.5.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"typescript-eslint": "^8.33.0",
|
"typescript-eslint": "^8.33.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -7,7 +7,7 @@ export default {
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
useTabs: false,
|
useTabs: false,
|
||||||
semi: false,
|
semi: false,
|
||||||
singleQuote: true,
|
singleQuote: false,
|
||||||
quoteProps: "as-needed",
|
quoteProps: "as-needed",
|
||||||
trailingComma: "es5",
|
trailingComma: "es5",
|
||||||
bracketSpacing: true,
|
bracketSpacing: true,
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
export function getTitleAnimation(delay = 0, duration = 0.3, once = true) {
|
export function getTitleAnimation(delay = 0, duration = 0.3, once = true) {
|
||||||
return {
|
return {
|
||||||
initial: { opacity: 0.001, translateY: 20, filter: 'blur(4px)' },
|
initial: { opacity: 0.001, translateY: 20, filter: "blur(4px)" },
|
||||||
whileInView: {
|
whileInView: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
translateY: 0,
|
translateY: 0,
|
||||||
filter: 'blur(0px)',
|
filter: "blur(0px)",
|
||||||
transition: { duration, delay },
|
transition: { duration, delay },
|
||||||
},
|
},
|
||||||
viewport: { once: once },
|
viewport: { once },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
import ArrowLeftIcon from '~/icons/ArrowLeftIcon.astro'
|
import ArrowLeftIcon from "~/icons/ArrowLeftIcon.astro"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
|
@ -11,10 +11,7 @@ const {
|
||||||
} = getUI(locale)
|
} = getUI(locale)
|
||||||
---
|
---
|
||||||
|
|
||||||
<button
|
<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>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import { getLocale, getPath } from '~/utils/i18n'
|
import { getLocale, getPath } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
const getLocalePath = getPath(locale)
|
const getLocalePath = getPath(locale)
|
||||||
|
@ -13,15 +13,15 @@ const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = As
|
||||||
{...extra}
|
{...extra}
|
||||||
href={getLocalePath(href)}
|
href={getLocalePath(href)}
|
||||||
class:list={[
|
class:list={[
|
||||||
'transition-bg flex items-center justify-center gap-2 rounded-xl px-6 py-4 transition-transform duration-150 hover:scale-[1.02] active:scale-[0.98]',
|
"transition-bg flex items-center justify-center gap-2 rounded-xl px-6 py-4 transition-transform duration-150 hover:scale-[1.02] active:scale-[0.98]",
|
||||||
className,
|
className,
|
||||||
isPrimary
|
isPrimary
|
||||||
? 'border-dark bg-dark text-paper shadow-lg'
|
? "border-dark bg-dark text-paper shadow-lg"
|
||||||
: isAlert
|
: isAlert
|
||||||
? 'bg-red-300 text-dark'
|
? "bg-red-300 text-dark"
|
||||||
: !isBordered
|
: !isBordered
|
||||||
? 'bg-subtle'
|
? "bg-subtle"
|
||||||
: '!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm',
|
: "!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm",
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
@ -31,15 +31,15 @@ const { class: className, isPrimary, isAlert, isBordered, href, id, extra } = As
|
||||||
id={id}
|
id={id}
|
||||||
{...extra}
|
{...extra}
|
||||||
class:list={[
|
class:list={[
|
||||||
'transition-bg flex items-center justify-center gap-2 rounded-lg px-6 py-3 transition-transform duration-150 hover:scale-[1.02]',
|
"transition-bg flex items-center justify-center gap-2 rounded-lg px-6 py-3 transition-transform duration-150 hover:scale-[1.02]",
|
||||||
className,
|
className,
|
||||||
isPrimary
|
isPrimary
|
||||||
? 'border-dark bg-dark text-paper shadow-md'
|
? "border-dark bg-dark text-paper shadow-md"
|
||||||
: isAlert
|
: isAlert
|
||||||
? 'bg-red-300 text-dark'
|
? "bg-red-300 text-dark"
|
||||||
: !isBordered
|
: !isBordered
|
||||||
? ''
|
? ""
|
||||||
: '!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm',
|
: "!transition-bg border-2 border-dark hover:bg-dark hover:text-paper hover:shadow-sm",
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -4,17 +4,14 @@ 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) => (
|
||||||
<div
|
<div
|
||||||
class:list={[
|
class:list={[
|
||||||
'absolute -translate-x-1/2 -translate-y-1/2 rounded-full',
|
"absolute -translate-x-1/2 -translate-y-1/2 rounded-full",
|
||||||
white ? 'border-paper' : 'border-coral',
|
white ? "border-paper" : "border-coral",
|
||||||
]}
|
]}
|
||||||
style={{
|
style={{
|
||||||
width: `${multiplier * sizes[i]}px`,
|
width: `${multiplier * sizes[i]}px`,
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
---
|
---
|
||||||
import Image from 'astro/components/Image.astro'
|
import Image from "astro/components/Image.astro"
|
||||||
import { motion } from 'motion/react'
|
import { motion } from "motion/react"
|
||||||
import { getTitleAnimation } from '~/animations'
|
import { getTitleAnimation } from "~/animations"
|
||||||
import ComImage from '~/assets/ComImage.png'
|
import ComImage from "~/assets/ComImage.png"
|
||||||
import Button from '~/components/Button.astro'
|
import Button from "~/components/Button.astro"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import CheckIcon from '~/icons/CheckIcon.astro'
|
import CheckIcon from "~/icons/CheckIcon.astro"
|
||||||
import GitHubIcon from '~/icons/GitHubIcon.astro'
|
import GitHubIcon from "~/icons/GitHubIcon.astro"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
|
@ -33,33 +33,21 @@ 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">
|
||||||
<motion.span client:load {...getTitleAnimation(0.8)}>
|
<motion.span client:load {...getTitleAnimation(0.8)}>
|
||||||
<Button class:list={['px-4']} href="https://github.com/zen-browser">
|
<Button class:list={["px-4"]} href="https://github.com/zen-browser">
|
||||||
<GitHubIcon class="size-4" />
|
<GitHubIcon class="size-4" />
|
||||||
<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>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
---
|
---
|
||||||
import { motion } from 'motion/react'
|
import { motion } from "motion/react"
|
||||||
import { getTitleAnimation } from '~/animations'
|
import { getTitleAnimation } from "~/animations"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
|
|
||||||
import CompactModeVideo from '~/assets/CompactMode.webm'
|
import CompactModeVideo from "~/assets/CompactMode.webm"
|
||||||
import GlanceVideo from '~/assets/Glance.webm'
|
import GlanceVideo from "~/assets/Glance.webm"
|
||||||
import SplitViewsVideo from '~/assets/SplitViews.webm'
|
import SplitViewsVideo from "~/assets/SplitViews.webm"
|
||||||
import WorkspacesVideo from '~/assets/Workspaces.webm'
|
import WorkspacesVideo from "~/assets/Workspaces.webm"
|
||||||
|
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
import Video from './Video.astro'
|
import Video from "./Video.astro"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
|
@ -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,25 +161,17 @@ 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',
|
const descriptions = descriptionEl?.dataset.descriptions?.split(",")
|
||||||
) as HTMLDivElement
|
|
||||||
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 = target?.closest(".feature, .feature-tab")
|
||||||
}: {
|
|
||||||
target: HTMLElement | undefined | null
|
|
||||||
}) {
|
|
||||||
target = target?.closest('.feature, .feature-tab')
|
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
const index = Array.from(features).indexOf(target) % 4
|
const index = Array.from(features).indexOf(target) % 4
|
||||||
|
@ -200,21 +180,19 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
// Update both mobile and desktop elements
|
// Update both mobile and desktop elements
|
||||||
features.forEach((f, i) => {
|
features.forEach((f, i) => {
|
||||||
if (i % 4 === index) {
|
if (i % 4 === index) {
|
||||||
f.setAttribute('data-active', 'true')
|
f.setAttribute("data-active", "true")
|
||||||
} else {
|
} else {
|
||||||
f.removeAttribute('data-active')
|
f.removeAttribute("data-active")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update mobile description
|
// Update mobile description
|
||||||
const descriptionEl = document.querySelector('.feature-description')
|
const descriptionEl = document.querySelector(".feature-description")
|
||||||
if (descriptionEl && descriptions) {
|
if (descriptionEl && descriptions) {
|
||||||
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
|
||||||
|
@ -222,14 +200,14 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
const rotation = (i - index) * 3
|
const rotation = (i - index) * 3
|
||||||
|
|
||||||
if (i === index) {
|
if (i === index) {
|
||||||
vid.setAttribute('data-active', 'true')
|
vid.setAttribute("data-active", "true")
|
||||||
vid.style.opacity = '1'
|
vid.style.opacity = "1"
|
||||||
vid.style.transform = `translate3d(-50%, 0, 0) scale(${scale})`
|
vid.style.transform = `translate3d(-50%, 0, 0) scale(${scale})`
|
||||||
vid.style.zIndex = '10'
|
vid.style.zIndex = "10"
|
||||||
vid.currentTime = 0
|
vid.currentTime = 0
|
||||||
vid.play()
|
vid.play()
|
||||||
} else {
|
} else {
|
||||||
vid.removeAttribute('data-active')
|
vid.removeAttribute("data-active")
|
||||||
vid.style.transform = `translate3d(-50%, ${yOffset}px, ${zOffset}px)
|
vid.style.transform = `translate3d(-50%, ${yOffset}px, ${zOffset}px)
|
||||||
rotate3d(1, 0, 0, ${rotation}deg)
|
rotate3d(1, 0, 0, ${rotation}deg)
|
||||||
scale(${scale})`
|
scale(${scale})`
|
||||||
|
@ -240,9 +218,11 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
feature.addEventListener('click', changeToFeature as any)
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
feature.addEventListener("click", changeToFeature as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
changeToFeature({ target: features[0] as any })
|
changeToFeature({ target: features[0] as any })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -251,7 +231,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
@apply w-full cursor-pointer select-none rounded-lg p-4 opacity-0 hover:bg-subtle;
|
@apply w-full cursor-pointer select-none rounded-lg p-4 opacity-0 hover:bg-subtle;
|
||||||
transition: background 0.2s ease-in-out;
|
transition: background 0.2s ease-in-out;
|
||||||
|
|
||||||
&[data-active='true'] {
|
&[data-active="true"] {
|
||||||
@apply bg-subtle;
|
@apply bg-subtle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -260,7 +240,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
@apply rounded-lg px-4 py-2 text-lg font-medium opacity-0 hover:bg-subtle;
|
@apply rounded-lg px-4 py-2 text-lg font-medium opacity-0 hover:bg-subtle;
|
||||||
transition: background 0.2s ease-in-out;
|
transition: background 0.2s ease-in-out;
|
||||||
|
|
||||||
&[data-active='true'] {
|
&[data-active="true"] {
|
||||||
@apply bg-subtle;
|
@apply bg-subtle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -299,7 +279,7 @@ const descriptions = Object.values(features.featureTabs).map((tab) => tab.descri
|
||||||
display: none;
|
display: none;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
||||||
&[data-active='true'] {
|
&[data-active="true"] {
|
||||||
display: block;
|
display: block;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
import Button from '~/components/Button.astro'
|
import Button from "~/components/Button.astro"
|
||||||
import Circles from '~/components/Circles.astro'
|
import Circles from "~/components/Circles.astro"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import SocialMediaStrip from '~/components/SocialMediaStrip.astro'
|
import SocialMediaStrip from "~/components/SocialMediaStrip.astro"
|
||||||
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
|
import ArrowRightIcon from "~/icons/ArrowRightIcon.astro"
|
||||||
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
import { getLocale, getPath, getUI } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
const getLocalePath = getPath(locale)
|
const getLocalePath = getPath(locale)
|
||||||
|
@ -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,22 +54,19 @@ 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>
|
||||||
<nav aria-label="About navigation">
|
<nav aria-label="About navigation">
|
||||||
<ul class="grid gap-2 opacity-80">
|
<ul class="grid gap-2 opacity-80">
|
||||||
<li>
|
<li>
|
||||||
<a href={getLocalePath('/about')} class="font-normal"
|
<a href={getLocalePath("/about")} class="font-normal"
|
||||||
>{footer.teamAndContributors}</a
|
>{footer.teamAndContributors}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={getLocalePath('/privacy-policy')} class="font-normal"
|
<a href={getLocalePath("/privacy-policy")} class="font-normal"
|
||||||
>{footer.privacyPolicy}</a
|
>{footer.privacyPolicy}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
@ -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>
|
||||||
|
@ -144,15 +122,11 @@ const {
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
class="flex justify-center gap-2 lg:justify-start"
|
class="flex justify-center gap-2 lg:justify-start"
|
||||||
set:html={footer.madeWith.replace('{link}', getLocalePath('/about'))}
|
set:html={footer.madeWith.replace("{link}", getLocalePath("/about"))}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<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>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
---
|
---
|
||||||
import { motion } from 'motion/react'
|
import { motion } from "motion/react"
|
||||||
import { getTitleAnimation } from '~/animations'
|
import { getTitleAnimation } from "~/animations"
|
||||||
import HomePageVideo from '~/assets/HomePageVideo.webm'
|
import HomePageVideo from "~/assets/HomePageVideo.webm"
|
||||||
import Button from '~/components/Button.astro'
|
import Button from "~/components/Button.astro"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import Title from '~/components/Title.astro'
|
import Title from "~/components/Title.astro"
|
||||||
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
|
import ArrowRightIcon from "~/icons/ArrowRightIcon.astro"
|
||||||
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
import { getLocale, getPath, getUI } from "~/utils/i18n"
|
||||||
import SocialMediaStrip from './SocialMediaStrip.astro'
|
import SocialMediaStrip from "./SocialMediaStrip.astro"
|
||||||
import Video from './Video.astro'
|
import Video from "./Video.astro"
|
||||||
|
|
||||||
let titleAnimationCounter = 0
|
let titleAnimationCounter = 0
|
||||||
function getNewAnimationDelay() {
|
function getNewAnimationDelay() {
|
||||||
|
@ -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()}>
|
||||||
|
@ -81,7 +75,7 @@ const {
|
||||||
<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>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
import { getLocale, getPath, getUI } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
const getLocalePath = getPath(locale)
|
const getLocalePath = getPath(locale)
|
||||||
|
@ -11,12 +11,7 @@ const {
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Hidden checkbox for menu toggle -->
|
<!-- Hidden checkbox for menu toggle -->
|
||||||
<input
|
<input type="checkbox" id="mobile-menu-toggle" class="peer sr-only lg:hidden" aria-hidden="true" />
|
||||||
type="checkbox"
|
|
||||||
id="mobile-menu-toggle"
|
|
||||||
class="peer sr-only lg:hidden"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Mobile Slide Menu -->
|
<!-- Mobile Slide Menu -->
|
||||||
<div
|
<div
|
||||||
|
@ -25,17 +20,16 @@ const {
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between border-b border-dark px-4 py-2">
|
<div class="flex items-center justify-between border-b border-dark px-4 py-2">
|
||||||
<div class="text-lg font-bold">{menu.menu}</div>
|
<div class="text-lg font-bold">{menu.menu}</div>
|
||||||
<label
|
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||||
for="mobile-menu-toggle"
|
<label for="mobile-menu-toggle" class="cursor-pointer p-2 text-dark">
|
||||||
class="cursor-pointer p-2 text-dark"
|
<span class="sr-only">Close menu</span>
|
||||||
aria-label="Close menu"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
|
@ -52,21 +46,18 @@ const {
|
||||||
<div class="mb-2 font-bold">{menu.gettingStarted}</div>
|
<div class="mb-2 font-bold">{menu.gettingStarted}</div>
|
||||||
<ul class="ml-4 space-y-2">
|
<ul class="ml-4 space-y-2">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath("/mods")} class="block text-dark hover:text-coral"
|
||||||
href={getLocalePath('/mods')}
|
>{menu.zenMods}</a
|
||||||
class="block text-dark hover:text-coral">{menu.zenMods}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath("/release-notes")} class="block text-dark hover:text-coral"
|
||||||
href={getLocalePath('/release-notes')}
|
>{menu.releaseNotes}</a
|
||||||
class="block text-dark hover:text-coral">{menu.releaseNotes}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="https://discord.gg/zen-browser" class="block text-dark hover:text-coral"
|
||||||
href="https://discord.gg/zen-browser"
|
>{menu.discord}</a
|
||||||
class="block text-dark hover:text-coral">{menu.discord}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -76,21 +67,18 @@ const {
|
||||||
<div class="mb-2 font-bold">{menu.usefulLinks}</div>
|
<div class="mb-2 font-bold">{menu.usefulLinks}</div>
|
||||||
<ul class="ml-4 space-y-2">
|
<ul class="ml-4 space-y-2">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath("/donate")} class="block text-dark hover:text-coral"
|
||||||
href={getLocalePath('/donate')}
|
>{menu.donate}</a
|
||||||
class="block text-dark hover:text-coral">{menu.donate}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath("/about")} class="block text-dark hover:text-coral"
|
||||||
href={getLocalePath('/about')}
|
>{menu.aboutUs}</a
|
||||||
class="block text-dark hover:text-coral">{menu.aboutUs}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="https://docs.zen-browser.app" class="block text-dark hover:text-coral"
|
||||||
href="https://docs.zen-browser.app"
|
>{menu.documentation}</a
|
||||||
class="block text-dark hover:text-coral">{menu.documentation}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -104,15 +92,13 @@ const {
|
||||||
</li>
|
</li>
|
||||||
<!-- Extra Links -->
|
<!-- Extra Links -->
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath("/mods")} class="block font-bold text-dark hover:text-coral"
|
||||||
href={getLocalePath('/mods')}
|
>{menu.mods}</a
|
||||||
class="block font-bold text-dark hover:text-coral">{menu.mods}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href={getLocalePath("/download")} class="block font-bold text-dark hover:text-coral"
|
||||||
href={getLocalePath('/download')}
|
>{menu.download}</a
|
||||||
class="block font-bold text-dark hover:text-coral">{menu.download}</a
|
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -120,6 +106,7 @@ const {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Overlay for Mobile Menu -->
|
<!-- Overlay for Mobile Menu -->
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||||
<label
|
<label
|
||||||
for="mobile-menu-toggle"
|
for="mobile-menu-toggle"
|
||||||
class="pointer-events-none fixed inset-0 z-30 bg-black opacity-0 transition-opacity duration-300 peer-checked:pointer-events-auto peer-checked:opacity-50"
|
class="pointer-events-none fixed inset-0 z-30 bg-black opacity-0 transition-opacity duration-300 peer-checked:pointer-events-auto peer-checked:opacity-50"
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
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 type { ZenTheme } from '~/mods'
|
import { useModsSearch } from "~/hooks/useModsSearch"
|
||||||
import { type Locale, getUI } from '~/utils/i18n'
|
import { type ZenTheme } from "~/mods"
|
||||||
|
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)
|
||||||
|
|
||||||
// Create icon objects
|
// Create icon objects
|
||||||
const defaultSortIcon = icon({ prefix: 'fas', iconName: 'sort' })
|
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,
|
||||||
|
@ -43,23 +44,23 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
setPageInput(page.toString())
|
setPageInput(page.toString())
|
||||||
}, [page])
|
}, [page])
|
||||||
|
|
||||||
function getSortIcon(state: 'default' | 'asc' | 'desc') {
|
function getSortIcon(state: "default" | "asc" | "desc") {
|
||||||
if (state === 'asc') return ascSortIcon
|
if (state === "asc") return ascSortIcon
|
||||||
if (state === 'desc') return descSortIcon
|
if (state === "desc") return descSortIcon
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -89,17 +90,18 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto mb-12 flex items-center justify-center gap-4 px-8">
|
<div className="mx-auto mb-12 flex items-center justify-center gap-4 px-8">
|
||||||
<button
|
<button
|
||||||
className={`px-3 py-2 ${page === 1 ? 'pointer-events-none text-gray-400' : 'text-dark hover:text-gray-600'}`}
|
className={`px-3 py-2 ${page === 1 ? "pointer-events-none text-gray-400" : "text-dark hover:text-gray-600"}`}
|
||||||
onClick={() => navigatePage(page - 1)}
|
onClick={() => navigatePage(page - 1)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<
|
<
|
||||||
</button>
|
</button>
|
||||||
<form className="flex items-center gap-2" onSubmit={handlePageSubmit}>
|
<form className="flex items-center gap-2" onSubmit={handlePageSubmit}>
|
||||||
{mods.pagination.pagination.split('{input}').map((value, index) => {
|
{mods.pagination.pagination.split("{input}").map((value, index) => {
|
||||||
if (index === 0) {
|
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,13 +112,15 @@ 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>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</form>
|
</form>
|
||||||
<button
|
<button
|
||||||
className={`px-3 py-2 ${page === totalPages ? 'pointer-events-none text-gray-400' : 'text-dark hover:text-gray-600'}`}
|
className={`px-3 py-2 ${page === totalPages ? "pointer-events-none text-gray-400" : "text-dark hover:text-gray-600"}`}
|
||||||
onClick={() => navigatePage(page + 1)}
|
onClick={() => navigatePage(page + 1)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -143,7 +147,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
<div className="grid w-full grid-cols-2 place-items-center gap-4 sm:grid-cols-3">
|
<div className="grid w-full grid-cols-2 place-items-center gap-4 sm:grid-cols-3">
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div className="flex flex-col items-start gap-2">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2 px-4 py-2 font-semibold text-md"
|
className="text-md flex items-center gap-2 px-4 py-2 font-semibold"
|
||||||
onClick={toggleCreatedSort}
|
onClick={toggleCreatedSort}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -159,7 +163,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2 px-4 py-2 font-semibold text-md"
|
className="text-md flex items-center gap-2 px-4 py-2 font-semibold"
|
||||||
onClick={toggleUpdatedSort}
|
onClick={toggleUpdatedSort}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -174,7 +178,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 px-4 py-2">
|
<div className="flex items-center gap-2 px-4 py-2">
|
||||||
<label className="font-semibold text-md" htmlFor="limit">
|
<label className="text-md font-semibold" htmlFor="limit">
|
||||||
{mods.sort.perPage}
|
{mods.sort.perPage}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
|
@ -194,7 +198,7 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
|
|
||||||
<div className="grid w-full grid-cols-1 place-items-start gap-12 py-6 md:grid-cols-2 xl:grid-cols-3">
|
<div className="grid w-full grid-cols-1 place-items-start gap-12 py-6 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{paginatedMods.length > 0 ? (
|
{paginatedMods.length > 0 ? (
|
||||||
paginatedMods.map((mod) => (
|
paginatedMods.map(mod => (
|
||||||
<a
|
<a
|
||||||
className="mod-card flex w-full flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
|
className="mod-card flex w-full flex-col gap-4 border-transparent transition-colors duration-100 hover:opacity-90"
|
||||||
href={`/mods/${mod.id}`}
|
href={`/mods/${mod.id}`}
|
||||||
|
@ -209,17 +213,17 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-bold text-lg">
|
<h2 className="text-lg font-bold">
|
||||||
{mod.name} <span className="ml-1 font-normal text-sm">by @{mod.author}</span>
|
{mod.name} <span className="ml-1 text-sm font-normal">by @{mod.author}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="font-thin text-sm">{mod.description}</p>
|
<p className="text-sm font-thin">{mod.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="col-span-4 grid place-items-center gap-4 place-self-center px-8 text-center">
|
<div className="col-span-4 grid place-items-center gap-4 place-self-center px-8 text-center">
|
||||||
<h2 className="font-bold text-lg">{mods.noResults}</h2>
|
<h2 className="text-lg font-bold">{mods.noResults}</h2>
|
||||||
<p className="font-thin text-sm">{mods.noResultsDescription}</p>
|
<p className="text-sm font-thin">{mods.noResultsDescription}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -228,3 +232,5 @@ export default function ModsList({ allMods, locale }: ModsListProps) {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default ModsList
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
---
|
---
|
||||||
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 Logo from "./Logo.astro"
|
||||||
import { getTitleAnimation } from '../animations.ts'
|
import MobileMenu from "./MobileMenu.astro"
|
||||||
import Logo from './Logo.astro'
|
import ThemeSwitch from "./ThemeSwitch.astro"
|
||||||
import MobileMenu from './MobileMenu.astro'
|
|
||||||
import ThemeSwitch from './ThemeSwitch.astro'
|
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
const getLocalePath = getPath(locale)
|
const getLocalePath = getPath(locale)
|
||||||
|
@ -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}
|
||||||
|
@ -62,7 +50,7 @@ const {
|
||||||
<ArrowRightIcon class="size-4" />
|
<ArrowRightIcon class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" href={getLocalePath('/release-notes')}>
|
<a class="dropdown-item" href={getLocalePath("/release-notes")}>
|
||||||
<div class="dropdown-title">{menu.releaseNotes}</div>
|
<div class="dropdown-title">{menu.releaseNotes}</div>
|
||||||
<div class="dropdown-description">
|
<div class="dropdown-description">
|
||||||
{menu.releaseNotesDesc}
|
{menu.releaseNotesDesc}
|
||||||
|
@ -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,18 +73,14 @@ const {
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<DropdownItems>
|
<DropdownItems>
|
||||||
<motion.div
|
<div class="navbar-dropdown !grid-cols-1 gap-1">
|
||||||
className="navbar-dropdown !grid-cols-1 gap-1"
|
<a class="dropdown-item" href={getLocalePath("/donate")}>
|
||||||
{...getTitleAnimation(0, 0.3, false)}
|
|
||||||
client:load
|
|
||||||
>
|
|
||||||
<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">
|
||||||
{menu.donateDesc}
|
{menu.donateDesc}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" href={getLocalePath('/about')}>
|
<a class="dropdown-item" href={getLocalePath("/about")}>
|
||||||
<div class="dropdown-title">{menu.aboutUs}</div>
|
<div class="dropdown-title">{menu.aboutUs}</div>
|
||||||
<div class="dropdown-description">
|
<div class="dropdown-description">
|
||||||
{menu.aboutUsDesc}
|
{menu.aboutUsDesc}
|
||||||
|
@ -108,20 +92,16 @@ 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")}>
|
||||||
<span>{menu.mods}</span>
|
<span>{menu.mods}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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"
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
import InfoIcon from '~/icons/InfoIcon.astro'
|
import InfoIcon from "~/icons/InfoIcon.astro"
|
||||||
|
|
||||||
import { releaseNotes as releaseNotesData } from '~/release-notes'
|
import { releaseNotes as releaseNotesData } from "~/release-notes"
|
||||||
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
import { getLocale, getPath, getUI } from "~/utils/i18n"
|
||||||
import { type ReleaseNote, getReleaseNoteFirefoxVersion } from '../release-notes'
|
import { type ReleaseNote, getReleaseNoteFirefoxVersion } from "../release-notes"
|
||||||
import ReleaseNoteListItem from './ReleaseNoteListItem.astro'
|
import ReleaseNoteListItem from "./ReleaseNoteListItem.astro"
|
||||||
export type Props = ReleaseNote
|
export type Props = ReleaseNote
|
||||||
const { isTwilight, ...props } = Astro.props
|
const { isTwilight, ...props } = Astro.props
|
||||||
|
|
||||||
|
@ -20,38 +20,42 @@ const {
|
||||||
|
|
||||||
let date: Date | undefined
|
let date: Date | undefined
|
||||||
if (props.date) {
|
if (props.date) {
|
||||||
const [day, month, year] = props.date.split('/')
|
const [day, month, year] = props.date.split("/")
|
||||||
date = new Date(Date.parse(`${year}-${month}-${day}`))
|
date = new Date(Date.parse(`${year}-${month}-${day}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
compareLink = `https://github.com/zen-browser/desktop/compare/${prevReleaseNote.version}...${props.version}`
|
compareLink = `https://github.com/zen-browser/desktop/compare/${prevReleaseNote.version}...${props.version}`
|
||||||
}
|
}
|
||||||
|
|
||||||
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":
|
||||||
listItems[type].push({
|
listItems[type].push({
|
||||||
type: 'feature',
|
type: "feature",
|
||||||
content: item,
|
content: item,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'fix':
|
case "fix":
|
||||||
listItems[type].push({
|
listItems[type].push({
|
||||||
type: 'fix',
|
type: "fix",
|
||||||
content: item.description ?? item,
|
content: item.description ?? item,
|
||||||
...(item.issue
|
...(item.issue
|
||||||
? {
|
? {
|
||||||
|
@ -63,86 +67,89 @@ const generateItems = (items: any, type: string) => {
|
||||||
: {}),
|
: {}),
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'security':
|
case "security":
|
||||||
listItems[type].push({
|
listItems[type].push({
|
||||||
type: 'security',
|
type: "security",
|
||||||
link: {
|
link: {
|
||||||
text: 'Various security fixes.',
|
text: "Various security fixes.",
|
||||||
href: item,
|
href: item,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'theme':
|
case "theme":
|
||||||
listItems[type].push({
|
listItems[type].push({
|
||||||
type: 'theme',
|
type: "theme",
|
||||||
content: item,
|
content: item,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'break':
|
case "break":
|
||||||
listItems[type].push({
|
listItems[type].push({
|
||||||
type: 'break',
|
type: "break",
|
||||||
content: item,
|
content: item,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
generateItems(props.security ? [props.security] : null, 'security')
|
generateItems(props.security ? [props.security] : null, "security")
|
||||||
generateItems(props.fixes, 'fix')
|
generateItems(props.fixes, "fix")
|
||||||
generateItems(props.features, 'feature')
|
generateItems(props.features, "feature")
|
||||||
generateItems(props.themeChanges, 'theme')
|
generateItems(props.themeChanges, "theme")
|
||||||
generateItems(props.breakingChanges, 'break')
|
generateItems(props.breakingChanges, "break")
|
||||||
generateItems(props.knownIssues, 'known')
|
generateItems(props.knownIssues, "known")
|
||||||
---
|
---
|
||||||
|
|
||||||
<section
|
<section
|
||||||
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
|
||||||
class="!mb-2 block w-fit rounded-full bg-coral px-3 py-1 text-xs text-paper"
|
class="!mb-2 block w-fit rounded-full bg-coral px-3 py-1 text-xs text-paper"
|
||||||
href={getLocalePath('/download?twilight')}
|
href={getLocalePath("/download?twilight")}
|
||||||
>
|
>
|
||||||
{releaseNoteItem.twilight}
|
{releaseNoteItem.twilight}
|
||||||
</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
|
||||||
{releaseNoteItem.firefoxVersion.replace('{version}', ffVersion)}
|
rel="noopener noreferrer"
|
||||||
|
class="text-xs text-coral underline decoration-wavy opacity-80"
|
||||||
|
href={`https://www.mozilla.org/en-US/firefox/${ffVersion}/releasenotes/`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{releaseNoteItem.firefoxVersion.replace("{version}", ffVersion)}
|
||||||
</a>
|
</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}`}
|
||||||
>
|
>
|
||||||
|
@ -161,12 +168,12 @@ generateItems(props.knownIssues, 'known')
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
compareLink !== '' ? (
|
compareLink !== "" ? (
|
||||||
<>
|
<>
|
||||||
<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,19 +183,19 @@ 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>
|
||||||
{
|
{
|
||||||
props.extra?.length ? (
|
props.extra?.length ? (
|
||||||
<p class="text-md text-muted-foreground extra mt-2">
|
<p class="text-md text-muted-foreground extra mt-2">
|
||||||
<Fragment set:html={props.extra.replace(/\n/g, '<br />')} />
|
<Fragment set:html={props.extra.replace(/\n/g, "<br />")} />
|
||||||
</p>
|
</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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
import { getLocale, getPath, getUI } from "~/utils/i18n"
|
||||||
|
|
||||||
const { type, content, link } = Astro.props as {
|
const { type, content, link } = Astro.props as {
|
||||||
type: 'security' | 'feature' | 'fix' | 'theme' | 'break' | 'known'
|
type: "security" | "feature" | "fix" | "theme" | "break" | "known"
|
||||||
content: string
|
content: string
|
||||||
link?: {
|
link?: {
|
||||||
text: string
|
text: string
|
||||||
|
@ -22,29 +22,25 @@ const {
|
||||||
<li class="flex gap-2">
|
<li class="flex gap-2">
|
||||||
<div
|
<div
|
||||||
class:list={[
|
class:list={[
|
||||||
(type === 'security' && 'text-[#e3401f]') ||
|
(type === "security" && "text-[#e3401f]") ||
|
||||||
(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>
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
---
|
---
|
||||||
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" })
|
||||||
const Bluesky = icon({ prefix: 'fab', iconName: 'bluesky' })
|
const Bluesky = icon({ prefix: "fab", iconName: "bluesky" })
|
||||||
const Github = icon({ prefix: 'fab', iconName: 'github' })
|
const Github = icon({ prefix: "fab", iconName: "github" })
|
||||||
const XTwitter = icon({ prefix: 'fab', iconName: 'x-twitter' })
|
const XTwitter = icon({ prefix: "fab", iconName: "x-twitter" })
|
||||||
const Reddit = icon({ prefix: 'fab', iconName: 'reddit' })
|
const Reddit = icon({ prefix: "fab", iconName: "reddit" })
|
||||||
---
|
---
|
||||||
|
|
||||||
<ul class={`flex items-center opacity-80 gap-${gap}`}>
|
<ul class={`flex items-center opacity-80 gap-${gap}`}>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
---
|
---
|
||||||
import { motion } from 'motion/react'
|
import { motion } from "motion/react"
|
||||||
import { getTitleAnimation } from '~/animations'
|
import { getTitleAnimation } from "~/animations"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
import tutaLogo from '~/assets/sponsors/tutaLogo_monochrome.svg'
|
import tutaLogo from "~/assets/sponsors/tutaLogo_monochrome.svg"
|
||||||
|
|
||||||
import Image from 'astro/components/Image.astro'
|
import Image from "astro/components/Image.astro"
|
||||||
const { showSponsors = true } = Astro.props
|
const { showSponsors = true } = Astro.props
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -18,7 +18,7 @@ const {
|
||||||
} = getUI(locale)
|
} = getUI(locale)
|
||||||
---
|
---
|
||||||
|
|
||||||
<section id="sponsors" class:list={['py-12', !showSponsors && 'hidden']}>
|
<section id="sponsors" class:list={["py-12", !showSponsors && "hidden"]}>
|
||||||
<div class="mx-auto flex flex-col text-center">
|
<div class="mx-auto flex flex-col text-center">
|
||||||
<motion.span client:load {...getTitleAnimation(0.2)}>
|
<motion.span client:load {...getTitleAnimation(0.2)}>
|
||||||
<Description class="mb-2 text-6xl font-bold">Our Sponsors</Description>
|
<Description class="mb-2 text-6xl font-bold">Our Sponsors</Description>
|
||||||
|
@ -28,10 +28,10 @@ const {
|
||||||
</motion.span>
|
</motion.span>
|
||||||
<div class="relative mt-8 flex items-center justify-center">
|
<div class="relative mt-8 flex items-center justify-center">
|
||||||
<motion.span client:load {...getTitleAnimation(0.6)}>
|
<motion.span client:load {...getTitleAnimation(0.6)}>
|
||||||
<a href={sponsors.sponsors['tuta'].url} target="_blank" class="w-fit">
|
<a href={sponsors.sponsors["tuta"].url} target="_blank" class="w-fit">
|
||||||
<Image
|
<Image
|
||||||
src={tutaLogo}
|
src={tutaLogo}
|
||||||
alt={sponsors.sponsors['tuta'].name}
|
alt={sponsors.sponsors["tuta"].name}
|
||||||
class="h-16 w-fit object-contain"
|
class="h-16 w-fit object-contain"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -4,17 +4,14 @@ interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { label, className = '' } = Astro.props
|
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"}
|
||||||
>
|
>
|
||||||
<svg class="hidden h-5 w-5 dark:block" viewBox="0 0 24 24">
|
<svg class="hidden h-5 w-5 dark:block" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
|
@ -29,26 +26,24 @@ 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")) {
|
||||||
return localStorage.getItem('theme') ?? 'light'
|
return localStorage.getItem("theme") ?? "light"
|
||||||
}
|
}
|
||||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
return 'dark'
|
return "dark"
|
||||||
}
|
}
|
||||||
return 'light'
|
return "light"
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = document.documentElement
|
const html = document.documentElement
|
||||||
|
|
||||||
themeSwitch.addEventListener('click', () => {
|
themeSwitch.addEventListener("click", () => {
|
||||||
const newTheme = resolveTheme() === 'light' ? 'dark' : 'light'
|
const newTheme = resolveTheme() === "light" ? "dark" : "light"
|
||||||
html.setAttribute('data-theme', newTheme)
|
html.setAttribute("data-theme", newTheme)
|
||||||
html.classList.toggle('dark', newTheme === 'dark')
|
html.classList.toggle("dark", newTheme === "dark")
|
||||||
localStorage.setItem('theme', newTheme)
|
localStorage.setItem("theme", newTheme)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
---
|
---
|
||||||
import { cn } from '~/utils/merge'
|
import { cn } from "~/utils/merge"
|
||||||
const { class: className } = Astro.props
|
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>
|
||||||
|
|
|
@ -1,28 +1,22 @@
|
||||||
---
|
---
|
||||||
const { src, class: className, ...rest } = Astro.props
|
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")
|
||||||
const dataSrc = video.getAttribute('data-src')
|
const dataSrc = video.getAttribute("data-src")
|
||||||
if (dataSrc && source) {
|
if (dataSrc && source) {
|
||||||
source.src = dataSrc
|
source.src = dataSrc
|
||||||
video.removeAttribute('data-src')
|
video.removeAttribute("data-src")
|
||||||
video.load()
|
video.load()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?.()
|
||||||
|
@ -109,28 +107,25 @@ const { label, href, checksum } = Astro.props
|
||||||
navigator.clipboard.writeText(checksum)
|
navigator.clipboard.writeText(checksum)
|
||||||
const btn = e.currentTarget as HTMLButtonElement
|
const btn = e.currentTarget as HTMLButtonElement
|
||||||
const original = btn.innerText
|
const original = btn.innerText
|
||||||
btn.innerText = 'Copied!'
|
btn.innerText = "Copied!"
|
||||||
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)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
// Handle platform selection
|
// Handle platform selection
|
||||||
const platformButtons = document.querySelectorAll('.platform-selector')
|
const platformButtons = document.querySelectorAll(".platform-selector")
|
||||||
const platformSections = document.querySelectorAll('.platform-section')
|
const platformSections = document.querySelectorAll(".platform-section")
|
||||||
|
|
||||||
// Function to detect OS and select appropriate platform
|
// Function to detect OS and select appropriate platform
|
||||||
function detectOS() {
|
function detectOS() {
|
||||||
const userAgent = window.navigator.userAgent
|
const userAgent = window.navigator.userAgent
|
||||||
let detectedOS = 'mac' // Default to macOS
|
let detectedOS = "mac" // Default to macOS
|
||||||
|
|
||||||
if (userAgent.indexOf('Windows') !== -1) {
|
if (userAgent.indexOf("Windows") !== -1) {
|
||||||
detectedOS = 'windows'
|
detectedOS = "windows"
|
||||||
} else if (userAgent.indexOf('Linux') !== -1) {
|
} else if (userAgent.indexOf("Linux") !== -1) {
|
||||||
detectedOS = 'linux'
|
detectedOS = "linux"
|
||||||
}
|
}
|
||||||
|
|
||||||
return detectedOS
|
return detectedOS
|
||||||
|
@ -27,28 +27,28 @@
|
||||||
async function selectPlatform(platform: string) {
|
async function selectPlatform(platform: string) {
|
||||||
// Update button styling
|
// Update button styling
|
||||||
for (const button of platformButtons) {
|
for (const button of platformButtons) {
|
||||||
const buttonPlatform = button.getAttribute('data-platform')
|
const buttonPlatform = button.getAttribute("data-platform")
|
||||||
if (buttonPlatform === platform) {
|
if (buttonPlatform === platform) {
|
||||||
button.setAttribute('data-active', 'true')
|
button.setAttribute("data-active", "true")
|
||||||
} else {
|
} else {
|
||||||
button.setAttribute('data-active', 'false')
|
button.setAttribute("data-active", "false")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide platform sections
|
// Show/hide platform sections
|
||||||
for (const section of platformSections) {
|
for (const section of platformSections) {
|
||||||
if (section.id === `${platform}-downloads`) {
|
if (section.id === `${platform}-downloads`) {
|
||||||
section.setAttribute('data-active', 'true')
|
section.setAttribute("data-active", "true")
|
||||||
} else {
|
} else {
|
||||||
section.setAttribute('data-active', 'false')
|
section.setAttribute("data-active", "false")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle platform button clicks
|
// Handle platform button clicks
|
||||||
for (const button of platformButtons) {
|
for (const button of platformButtons) {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener("click", () => {
|
||||||
const platform = button.getAttribute('data-platform') ?? ''
|
const platform = button.getAttribute("data-platform") ?? ""
|
||||||
selectPlatform(platform)
|
selectPlatform(platform)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -56,43 +56,40 @@
|
||||||
// Check for twilight mode
|
// Check for twilight mode
|
||||||
async function checkTwilightMode() {
|
async function checkTwilightMode() {
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const isTwilight = urlParams.has('twilight')
|
const isTwilight = urlParams.has("twilight")
|
||||||
|
|
||||||
if (isTwilight) {
|
if (isTwilight) {
|
||||||
const twilightInfoElem = document.getElementById('twilight-info')
|
const twilightInfoElem = document.getElementById("twilight-info")
|
||||||
twilightInfoElem?.setAttribute('data-twilight', 'true')
|
twilightInfoElem?.setAttribute("data-twilight", "true")
|
||||||
|
|
||||||
// Update UI to show twilight mode with animation
|
// Update UI to show twilight mode with animation
|
||||||
const titleElem = document.getElementById('download-title')
|
const titleElem = document.getElementById("download-title")
|
||||||
if (titleElem) {
|
if (titleElem) {
|
||||||
const zenText = titleElem.innerHTML
|
const zenText = titleElem.innerHTML
|
||||||
titleElem.innerHTML = zenText.replace('Zen', 'Twilight')
|
titleElem.innerHTML = zenText.replace("Zen", "Twilight")
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = document.querySelectorAll('.release-type-tag')
|
const tags = document.querySelectorAll(".release-type-tag")
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
tag.innerHTML = tag.innerHTML.replace('Beta', 'Twilight')
|
tag.innerHTML = tag.innerHTML.replace("Beta", "Twilight")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply twilight mode to all relevant elements
|
// 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace all download links with twilight versions
|
// Replace all download links with twilight versions
|
||||||
const downloadLinks = document.querySelectorAll('a.download-link')
|
const downloadLinks = document.querySelectorAll("a.download-link")
|
||||||
for (const link of downloadLinks) {
|
for (const link of downloadLinks) {
|
||||||
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/',
|
link.setAttribute("href", twilightHref)
|
||||||
'/download/twilight/',
|
|
||||||
)
|
|
||||||
link.setAttribute('href', twilightHref)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ interface PlatformReleases {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
platform: 'mac' | 'windows' | 'linux'
|
platform: "mac" | "windows" | "linux"
|
||||||
icon: string[]
|
icon: string[]
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
|
@ -30,13 +30,13 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { platform, icon, title, description, releases } = Astro.props
|
const { platform, icon, title, description, releases } = Astro.props
|
||||||
import { Image } from 'astro:assets'
|
import { Image } from "astro:assets"
|
||||||
import AppIconDark from '../../assets/app-icon-dark.png'
|
import AppIconDark from "../../assets/app-icon-dark.png"
|
||||||
import AppIconLight from '../../assets/app-icon-light.png'
|
import AppIconLight from "../../assets/app-icon-light.png"
|
||||||
import DownloadCard from './ButtonCard.astro'
|
import DownloadCard from "./ButtonCard.astro"
|
||||||
|
|
||||||
function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
||||||
return !!obj && typeof obj === 'object' && 'link' in obj
|
return !!obj && typeof obj === "object" && "link" in obj
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -73,22 +73,14 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
||||||
{releases.x86_64 &&
|
{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}
|
||||||
/>
|
/>
|
||||||
|
@ -99,22 +91,16 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
|
||||||
{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"
|
||||||
|
|
|
@ -6,42 +6,42 @@ export function getReleasesWithChecksums(checksums: Record<string, string>) {
|
||||||
return {
|
return {
|
||||||
macos: {
|
macos: {
|
||||||
universal: {
|
universal: {
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg',
|
link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.macos-universal.dmg",
|
||||||
label: 'Universal',
|
label: "Universal",
|
||||||
checksum: checksums['zen.macos-universal.dmg'],
|
checksum: checksums["zen.macos-universal.dmg"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
windows: {
|
windows: {
|
||||||
x86_64: {
|
x86_64: {
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe',
|
link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.installer.exe",
|
||||||
label: '64-bit (Recommended)',
|
label: "64-bit (Recommended)",
|
||||||
checksum: checksums['zen.installer.exe'],
|
checksum: checksums["zen.installer.exe"],
|
||||||
},
|
},
|
||||||
arm64: {
|
arm64: {
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe',
|
link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.installer-arm64.exe",
|
||||||
label: 'ARM64',
|
label: "ARM64",
|
||||||
checksum: checksums['zen.installer-arm64.exe'],
|
checksum: checksums["zen.installer-arm64.exe"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
x86_64: {
|
x86_64: {
|
||||||
tarball: {
|
tarball: {
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz',
|
link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-x86_64.tar.xz",
|
||||||
label: 'Tarball',
|
label: "Tarball",
|
||||||
checksum: checksums['zen.linux-x86_64.tar.xz'],
|
checksum: checksums["zen.linux-x86_64.tar.xz"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
aarch64: {
|
aarch64: {
|
||||||
tarball: {
|
tarball: {
|
||||||
link: 'https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz',
|
link: "https://github.com/zen-browser/desktop/releases/latest/download/zen.linux-aarch64.tar.xz",
|
||||||
label: 'Tarball',
|
label: "Tarball",
|
||||||
checksum: checksums['zen.linux-aarch64.tar.xz'],
|
checksum: checksums["zen.linux-aarch64.tar.xz"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
flathub: {
|
flathub: {
|
||||||
all: {
|
all: {
|
||||||
link: 'https://flathub.org/apps/app.zen_browser.zen',
|
link: "https://flathub.org/apps/app.zen_browser.zen",
|
||||||
label: 'Flathub',
|
label: "Flathub",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export const CHECKSUMS = {
|
export const CHECKSUMS = {
|
||||||
'zen.macos-universal.dmg': 'macsum',
|
"zen.macos-universal.dmg": "macsum",
|
||||||
'zen.installer.exe': 'winsum',
|
"zen.installer.exe": "winsum",
|
||||||
'zen.installer-arm64.exe': 'winarmsum',
|
"zen.installer-arm64.exe": "winarmsum",
|
||||||
'zen.linux-x86_64.tar.xz': 'linuxsum',
|
"zen.linux-x86_64.tar.xz": "linuxsum",
|
||||||
'zen.linux-aarch64.tar.xz': 'linuxarmsum',
|
"zen.linux-aarch64.tar.xz": "linuxarmsum",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const I18N = {
|
export const I18N = {
|
||||||
DEFAULT_LOCALE: 'en',
|
DEFAULT_LOCALE: "en",
|
||||||
LOCALES: [{ label: 'English', value: 'en' }],
|
LOCALES: [{ label: "English", value: "en" }],
|
||||||
} as const
|
} as const
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CHECKSUMS } from './checksum'
|
import { CHECKSUMS } from "./checksum"
|
||||||
import { I18N } from './i18n'
|
import { I18N } from "./i18n"
|
||||||
|
|
||||||
export const CONSTANT = {
|
export const CONSTANT = {
|
||||||
I18N,
|
I18N,
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from "react"
|
||||||
import type { ZenTheme } from '../mods'
|
|
||||||
|
|
||||||
type SortOrder = 'default' | 'asc' | 'desc'
|
import { type ZenTheme } from "../mods"
|
||||||
|
|
||||||
interface ModsSearchState {
|
type SortOrder = "default" | "asc" | "desc"
|
||||||
|
|
||||||
|
type ModsSearchState = {
|
||||||
search: string
|
search: string
|
||||||
createdSort: SortOrder
|
createdSort: SortOrder
|
||||||
updatedSort: SortOrder
|
updatedSort: SortOrder
|
||||||
|
@ -16,9 +17,9 @@ const DEFAULT_LIMIT = 12
|
||||||
export function useModsSearch(mods: ZenTheme[]) {
|
export function useModsSearch(mods: ZenTheme[]) {
|
||||||
const [searchParams, setSearchParams] = useState<URLSearchParams>()
|
const [searchParams, setSearchParams] = useState<URLSearchParams>()
|
||||||
const [state, setState] = useState<ModsSearchState>({
|
const [state, setState] = useState<ModsSearchState>({
|
||||||
search: '',
|
search: "",
|
||||||
createdSort: 'desc',
|
createdSort: "desc",
|
||||||
updatedSort: 'default',
|
updatedSort: "default",
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: DEFAULT_LIMIT,
|
limit: DEFAULT_LIMIT,
|
||||||
})
|
})
|
||||||
|
@ -28,11 +29,11 @@ export function useModsSearch(mods: ZenTheme[]) {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
setSearchParams(params)
|
setSearchParams(params)
|
||||||
setState({
|
setState({
|
||||||
search: params.get('q') || '',
|
search: params.get("q") || "",
|
||||||
createdSort: (params.get('created') as SortOrder) || 'desc',
|
createdSort: (params.get("created") as SortOrder) || "desc",
|
||||||
updatedSort: (params.get('updated') as SortOrder) || 'default',
|
updatedSort: (params.get("updated") as SortOrder) || "default",
|
||||||
page: Number.parseInt(params.get('page') || '1', 10),
|
page: Number.parseInt(params.get("page") || "1", 10),
|
||||||
limit: Number.parseInt(params.get('limit') || String(DEFAULT_LIMIT), 10),
|
limit: Number.parseInt(params.get("limit") || String(DEFAULT_LIMIT), 10),
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -41,41 +42,41 @@ export function useModsSearch(mods: ZenTheme[]) {
|
||||||
if (!searchParams) return
|
if (!searchParams) return
|
||||||
|
|
||||||
if (state.search) {
|
if (state.search) {
|
||||||
searchParams.set('q', state.search)
|
searchParams.set("q", state.search)
|
||||||
} else {
|
} else {
|
||||||
searchParams.delete('q')
|
searchParams.delete("q")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.createdSort !== 'default') {
|
if (state.createdSort !== "default") {
|
||||||
searchParams.set('created', state.createdSort)
|
searchParams.set("created", state.createdSort)
|
||||||
} else {
|
} else {
|
||||||
searchParams.delete('created')
|
searchParams.delete("created")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.updatedSort !== 'default') {
|
if (state.updatedSort !== "default") {
|
||||||
searchParams.set('updated', state.updatedSort)
|
searchParams.set("updated", state.updatedSort)
|
||||||
} else {
|
} else {
|
||||||
searchParams.delete('updated')
|
searchParams.delete("updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.page > 1) {
|
if (state.page > 1) {
|
||||||
searchParams.set('page', state.page.toString())
|
searchParams.set("page", state.page.toString())
|
||||||
} else {
|
} else {
|
||||||
searchParams.delete('page')
|
searchParams.delete("page")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.limit !== DEFAULT_LIMIT) {
|
if (state.limit !== DEFAULT_LIMIT) {
|
||||||
searchParams.set('limit', state.limit.toString())
|
searchParams.set("limit", state.limit.toString())
|
||||||
} else {
|
} else {
|
||||||
searchParams.delete('limit')
|
searchParams.delete("limit")
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUrl = `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`
|
const newUrl = `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`
|
||||||
|
|
||||||
if (state.page > 1) {
|
if (state.page > 1) {
|
||||||
window.history.pushState({}, '', newUrl)
|
window.history.pushState({}, "", newUrl)
|
||||||
} else {
|
} else {
|
||||||
window.history.replaceState({}, '', newUrl)
|
window.history.replaceState({}, "", newUrl)
|
||||||
}
|
}
|
||||||
}, [state, searchParams])
|
}, [state, searchParams])
|
||||||
|
|
||||||
|
@ -86,27 +87,27 @@ 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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by createdAt if chosen
|
// Sort by createdAt if chosen
|
||||||
if (state.createdSort !== 'default') {
|
if (state.createdSort !== "default") {
|
||||||
filtered.sort((a, b) => {
|
filtered.sort((a, b) => {
|
||||||
const diff = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
const diff = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
return state.createdSort === 'asc' ? diff : -diff
|
return state.createdSort === "asc" ? diff : -diff
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by updatedAt if chosen
|
// Sort by updatedAt if chosen
|
||||||
if (state.updatedSort !== 'default') {
|
if (state.updatedSort !== "default") {
|
||||||
filtered.sort((a, b) => {
|
filtered.sort((a, b) => {
|
||||||
const diff = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
const diff = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
||||||
return state.updatedSort === 'asc' ? diff : -diff
|
return state.updatedSort === "asc" ? diff : -diff
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,34 +121,36 @@ export function useModsSearch(mods: ZenTheme[]) {
|
||||||
const paginatedMods = filteredMods.slice(startIndex, endIndex)
|
const paginatedMods = filteredMods.slice(startIndex, endIndex)
|
||||||
|
|
||||||
const setSearch = (search: string) => {
|
const setSearch = (search: string) => {
|
||||||
setState((prev) => ({ ...prev, search, page: 1 })) // Reset page when search changes
|
setState(prev => ({ ...prev, search, page: 1 })) // Reset page when search changes
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleCreatedSort = () => {
|
const toggleCreatedSort = () => {
|
||||||
setState((prev) => ({
|
setState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
createdSort: prev.createdSort === 'default' ? 'asc' : prev.createdSort === 'asc' ? 'desc' : 'default',
|
createdSort:
|
||||||
|
prev.createdSort === "default" ? "asc" : prev.createdSort === "asc" ? "desc" : "default",
|
||||||
page: 1, // Reset page when sort changes
|
page: 1, // Reset page when sort changes
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleUpdatedSort = () => {
|
const toggleUpdatedSort = () => {
|
||||||
setState((prev) => ({
|
setState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
updatedSort: prev.updatedSort === 'default' ? 'asc' : prev.updatedSort === 'asc' ? 'desc' : 'default',
|
updatedSort:
|
||||||
|
prev.updatedSort === "default" ? "asc" : prev.updatedSort === "asc" ? "desc" : "default",
|
||||||
page: 1, // Reset page when sort changes
|
page: 1, // Reset page when sort changes
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPage = (page: number) => {
|
const setPage = (page: number) => {
|
||||||
setState((prev) => ({
|
setState(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
page: Math.max(1, Math.min(page, totalPages)),
|
page: Math.max(1, Math.min(page, totalPages)),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const setLimit = (limit: number) => {
|
const setLimit = (limit: number) => {
|
||||||
setState((prev) => ({ ...prev, limit, page: 1 })) // Reset page when limit changes
|
setState(prev => ({ ...prev, limit, page: 1 })) // Reset page when limit changes
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-arrow-left-icon lucide-arrow-left", className]} {...props}><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-arrow-left-icon lucide-arrow-left", className]}
|
||||||
|
{...props}><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-arrow-right-icon lucide-arrow-right", className]} {...props}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-arrow-right-icon lucide-arrow-right", className]}
|
||||||
|
{...props}><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-arrow-up-icon lucide-arrow-up", className]} {...props}><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-arrow-up-icon lucide-arrow-up", className]}
|
||||||
|
{...props}><path d="m5 12 7-7 7 7"></path><path d="M12 19V5"></path></svg
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-check-icon lucide-check", className]} {...props}><path d="M20 6 9 17l-5-5"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-check-icon lucide-check", className]}
|
||||||
|
{...props}><path d="M20 6 9 17l-5-5"></path></svg
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-chevron-down-icon lucide-chevron-down", className]} {...props}><path d="m6 9 6 6 6-6"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-chevron-down-icon lucide-chevron-down", className]}
|
||||||
|
{...props}><path d="m6 9 6 6 6-6"></path></svg
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,18 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-download-icon lucide-download", className]} {...props}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-download-icon lucide-download", className]}
|
||||||
|
{...props}
|
||||||
|
><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"
|
||||||
|
></polyline><line x1="12" x2="12" y1="15" y2="3"></line></svg
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,18 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-external-link-icon lucide-external-link", className]} {...props}><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-external-link-icon lucide-external-link", className]}
|
||||||
|
{...props}
|
||||||
|
><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path
|
||||||
|
d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,19 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-github-icon lucide-github", className]} {...props}><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-github-icon lucide-github", className]}
|
||||||
|
{...props}
|
||||||
|
><path
|
||||||
|
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
|
||||||
|
></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,18 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-info-icon lucide-info", className]} {...props}><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-info-icon lucide-info", className]}
|
||||||
|
{...props}
|
||||||
|
><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,18 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-lock-icon lucide-lock", className]} {...props}><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-lock-icon lucide-lock", className]}
|
||||||
|
{...props}
|
||||||
|
><rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
|
|
|
@ -2,4 +2,16 @@
|
||||||
const { class: className, ...props } = Astro.props
|
const { class: className, ...props } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class:list={["lucide lucide-menu-icon lucide-menu", className]} {...props}><path d="M4 12h16"/><path d="M4 18h16"/><path d="M4 6h16"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={["lucide lucide-menu-icon lucide-menu", className]}
|
||||||
|
{...props}><path d="M4 12h16"></path><path d="M4 18h16"></path><path d="M4 6h16"></path></svg
|
||||||
|
>
|
||||||
|
|
|
@ -9,57 +9,61 @@ interface Props {
|
||||||
|
|
||||||
const { title, description, ogImage, isHome, redirect } = Astro.props
|
const { title, description, ogImage, isHome, redirect } = Astro.props
|
||||||
const defaultDescription =
|
const defaultDescription =
|
||||||
'Zen Browser is built for speed, security, and true privacy. Download now to enjoy a beautifully-designed, distraction-free web experience packed with features.'
|
"Zen Browser is built for speed, security, and true privacy. Download now to enjoy a beautifully-designed, distraction-free web experience packed with features."
|
||||||
const defaultOgImage = '/share-pic.png'
|
const defaultOgImage = "/share-pic.png"
|
||||||
import '@fontsource/bricolage-grotesque/400.css'
|
import "@fontsource/bricolage-grotesque/400.css"
|
||||||
import '@fontsource/bricolage-grotesque/500.css'
|
import "@fontsource/bricolage-grotesque/500.css"
|
||||||
import '@fontsource/bricolage-grotesque/600.css'
|
import "@fontsource/bricolage-grotesque/600.css"
|
||||||
import Footer from '~/components/Footer.astro'
|
import Footer from "~/components/Footer.astro"
|
||||||
import NavBar from '~/components/NavBar.astro'
|
import NavBar from "~/components/NavBar.astro"
|
||||||
import { getLocale } from '~/utils/i18n'
|
import { getLocale } from "~/utils/i18n"
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
---
|
---
|
||||||
|
|
||||||
<script is:inline data-cfasync="false">
|
<script is:inline data-cfasync="false">
|
||||||
const theme = (() => {
|
const theme = (() => {
|
||||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
|
||||||
return localStorage.getItem('theme') ?? 'light'
|
return localStorage.getItem("theme") ?? "light"
|
||||||
}
|
}
|
||||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
return 'dark'
|
return "dark"
|
||||||
}
|
}
|
||||||
return 'light'
|
return "light"
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
if (theme === "light") {
|
||||||
if (theme === 'light') {
|
document.documentElement.setAttribute("data-theme", "light")
|
||||||
document.documentElement.setAttribute('data-theme', 'light')
|
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.setAttribute('data-theme', 'dark')
|
document.documentElement.setAttribute("data-theme", "dark")
|
||||||
}
|
}
|
||||||
</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 />
|
||||||
|
@ -106,14 +105,14 @@ const locale = getLocale(Astro)
|
||||||
</html>
|
</html>
|
||||||
<style is:global>
|
<style is:global>
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Junicode';
|
font-family: "Junicode";
|
||||||
src: url('/fonts/JunicodeVF-Roman-subset.woff2') format('woff2');
|
src: url("/fonts/JunicodeVF-Roman-subset.woff2") format("woff2");
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Junicode-Italic';
|
font-family: "Junicode-Italic";
|
||||||
src: url('/fonts/JunicodeVF-Italic-subset.woff2') format('woff2');
|
src: url("/fonts/JunicodeVF-Italic-subset.woff2") format("woff2");
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,17 +142,17 @@ const locale = getLocale(Astro)
|
||||||
|
|
||||||
body,
|
body,
|
||||||
body > * {
|
body > * {
|
||||||
font-family: 'Bricolage Grotesque', sans-serif;
|
font-family: "Bricolage Grotesque", sans-serif;
|
||||||
font-optical-sizing: auto;
|
font-optical-sizing: auto;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-variation-settings: 'wdth' 100;
|
font-variation-settings: "wdth" 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 .italic {
|
h1 .italic {
|
||||||
font-family: 'Junicode-Italic', serif;
|
font-family: "Junicode-Italic", serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-feature-settings: 'swsh' 0;
|
font-feature-settings: "swsh" 0;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
src/mods.ts
10
src/mods.ts
|
@ -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
|
||||||
|
@ -17,14 +17,14 @@ export interface ZenTheme {
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEME_API = 'https://zen-browser.github.io/theme-store/themes.json'
|
const THEME_API = "https://zen-browser.github.io/theme-store/themes.json"
|
||||||
|
|
||||||
export async function getAllMods(): Promise<ZenTheme[]> {
|
export async function getAllMods(): Promise<ZenTheme[]> {
|
||||||
try {
|
try {
|
||||||
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)
|
||||||
|
@ -37,5 +37,5 @@ export function getAuthorLink(author: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLocalizedDate(date: Date): string {
|
export function getLocalizedDate(date: Date): string {
|
||||||
return format(date, 'PP')
|
return format(date, "PP")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
import NotFound from './[...locale]/404.astro'
|
import NotFound from "./[...locale]/404.astro"
|
||||||
---
|
---
|
||||||
|
|
||||||
<NotFound />
|
<NotFound />
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
import Button from '~/components/Button.astro'
|
import Button from "~/components/Button.astro"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import Title from '~/components/Title.astro'
|
import Title from "~/components/Title.astro"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { getLocale, getPath, getUI } from '~/utils/i18n'
|
import { getLocale, getPath, getUI } from "~/utils/i18n"
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
const getLocalePath = getPath(locale)
|
const getLocalePath = getPath(locale)
|
||||||
|
@ -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}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
import { Image } from 'astro:assets'
|
import { Image } from "astro:assets"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from "~/utils/i18n"
|
||||||
import Button from '~/components/Button.astro'
|
import Button from "~/components/Button.astro"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
|
@ -14,35 +14,31 @@ 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">
|
||||||
{member.link && typeof member.link === 'string' ? (
|
{member.link && typeof member.link === "string" ? (
|
||||||
<a href={member.link}>
|
<a href={member.link}>
|
||||||
<strong class="zen-link font-bold">{member.name}</strong>
|
<strong class="zen-link font-bold">{member.name}</strong>
|
||||||
</a>
|
</a>
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
---
|
---
|
||||||
import Button from '~/components/Button.astro'
|
import Button from "~/components/Button.astro"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
|
import ArrowRightIcon from "~/icons/ArrowRightIcon.astro"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
const {
|
const {
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
---
|
---
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import DownloadScript from '~/components/download/DownloadScript.astro'
|
import DownloadScript from "~/components/download/DownloadScript.astro"
|
||||||
import PlatformDownload from '~/components/download/PlatformDownload.astro'
|
import PlatformDownload from "~/components/download/PlatformDownload.astro"
|
||||||
import { getReleasesWithChecksums } from '~/components/download/release-data'
|
import { getReleasesWithChecksums } from "~/components/download/release-data"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { getChecksums } from '~/utils/githubChecksums'
|
import { getChecksums } from "~/utils/githubChecksums"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
|
|
||||||
import { icon, library } from '@fortawesome/fontawesome-svg-core'
|
import { icon, library } from "@fortawesome/fontawesome-svg-core"
|
||||||
import { faApple, faGithub, faLinux, faWindows } from '@fortawesome/free-brands-svg-icons'
|
import { faApple, faGithub, faLinux, faWindows } from "@fortawesome/free-brands-svg-icons"
|
||||||
import ExternalLinkIcon from '~/icons/ExternalLink.astro'
|
import ExternalLinkIcon from "~/icons/ExternalLink.astro"
|
||||||
import LockIcon from '~/icons/LockIcon.astro'
|
import LockIcon from "~/icons/LockIcon.astro"
|
||||||
|
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
const {
|
const {
|
||||||
|
@ -21,10 +21,10 @@ const {
|
||||||
} = getUI(locale)
|
} = getUI(locale)
|
||||||
|
|
||||||
library.add(faWindows, faLinux, faApple, faGithub)
|
library.add(faWindows, faLinux, faApple, faGithub)
|
||||||
const windowsIcon = icon({ prefix: 'fab', iconName: 'windows' })
|
const windowsIcon = icon({ prefix: "fab", iconName: "windows" })
|
||||||
const linuxIcon = icon({ prefix: 'fab', iconName: 'linux' })
|
const linuxIcon = icon({ prefix: "fab", iconName: "linux" })
|
||||||
const appleIcon = icon({ prefix: 'fab', iconName: 'apple' })
|
const appleIcon = icon({ prefix: "fab", iconName: "apple" })
|
||||||
const githubIcon = icon({ prefix: 'fab', iconName: 'github' })
|
const githubIcon = icon({ prefix: "fab", iconName: "github" })
|
||||||
|
|
||||||
const checksums = await getChecksums()
|
const checksums = await getChecksums()
|
||||||
const releases = getReleasesWithChecksums(checksums)
|
const releases = getReleasesWithChecksums(checksums)
|
||||||
|
@ -39,9 +39,7 @@ const platformDescriptions = download.platformDescriptions
|
||||||
<main class="flex min-h-screen flex-col px-6 data-[os='windows']:bg-zen-blue">
|
<main class="flex min-h-screen flex-col px-6 data-[os='windows']:bg-zen-blue">
|
||||||
<div class="container relative mx-auto py-12">
|
<div class="container relative mx-auto py-12">
|
||||||
<div class="mb-6 mt-12 flex flex-col gap-4">
|
<div class="mb-6 mt-12 flex flex-col gap-4">
|
||||||
<Description id="download-title" class="text-6xl font-bold"
|
<Description id="download-title" class="text-6xl font-bold">{download.title}</Description>
|
||||||
>{download.title}</Description
|
|
||||||
>
|
|
||||||
<Description class="max-w-xl text-pretty">
|
<Description class="max-w-xl text-pretty">
|
||||||
{download.description}
|
{download.description}
|
||||||
</Description>
|
</Description>
|
||||||
|
@ -191,9 +189,7 @@ const platformDescriptions = download.platformDescriptions
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Security Notice -->
|
<!-- Security Notice -->
|
||||||
<div
|
<div class="bg-opaicty-10 grid grid-cols-[auto,1fr] gap-4 rounded-2xl bg-subtle p-6">
|
||||||
class="bg-opaicty-10 grid grid-cols-[auto,1fr] gap-4 rounded-2xl bg-subtle p-6"
|
|
||||||
>
|
|
||||||
<div class="h-fit rounded-xl bg-subtle p-3">
|
<div class="h-fit rounded-xl bg-subtle p-3">
|
||||||
<LockIcon class="h-5 w-5" />
|
<LockIcon class="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -202,10 +198,7 @@ const platformDescriptions = download.platformDescriptions
|
||||||
<h3 class="mb-2 text-lg font-medium">
|
<h3 class="mb-2 text-lg font-medium">
|
||||||
{download.securityNotice.title}
|
{download.securityNotice.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p class="text-muted-foreground" set:html={download.securityNotice.description} />
|
||||||
class="text-muted-foreground"
|
|
||||||
set:html={download.securityNotice.description}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
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. */
|
||||||
const RSS_ENTRY_LIMIT = 20
|
const RSS_ENTRY_LIMIT = 20
|
||||||
|
@ -12,11 +13,12 @@ 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",
|
||||||
description: 'Release Notes for the Zen Browser',
|
description: "Release Notes for the Zen Browser",
|
||||||
site: context.url,
|
site: context.url,
|
||||||
items: [],
|
items: [],
|
||||||
customData: `
|
customData: `
|
||||||
|
@ -53,9 +55,9 @@ export function GET(context: { url: URL }) {
|
||||||
* @returns The passed in date string as a Date object.
|
* @returns The passed in date string as a Date object.
|
||||||
*/
|
*/
|
||||||
function formatRssDate(dateStr: string) {
|
function formatRssDate(dateStr: string) {
|
||||||
const splitDate = dateStr.split('/')
|
const splitDate = dateStr.split("/")
|
||||||
if (splitDate.length !== 3) {
|
if (splitDate.length !== 3) {
|
||||||
throw new Error('Invalid date format')
|
throw new Error("Invalid date format")
|
||||||
}
|
}
|
||||||
|
|
||||||
const day = Number(splitDate[0])
|
const day = Number(splitDate[0])
|
||||||
|
@ -76,40 +78,43 @@ function formatReleaseNote(releaseNote: ReleaseNote) {
|
||||||
</p>`
|
</p>`
|
||||||
|
|
||||||
if (releaseNote.extra) {
|
if (releaseNote.extra) {
|
||||||
content += `<p>${releaseNote.extra.replace(/(\n)/g, '<br />')}</p>`
|
content += `<p>${releaseNote.extra.replace(/(\n)/g, "<br />")}</p>`
|
||||||
}
|
}
|
||||||
|
|
||||||
content += addReleaseNoteSection('⚠️ Breaking changes', releaseNote.breakingChanges?.map(breakingChangeToReleaseNote))
|
content += addReleaseNoteSection(
|
||||||
content += addReleaseNoteSection('✓ Fixes', releaseNote.fixes?.map(fixToReleaseNote))
|
"⚠️ Breaking changes",
|
||||||
content += addReleaseNoteSection('🖌 Theme Changes', releaseNote.themeChanges)
|
releaseNote.breakingChanges?.map(breakingChangeToReleaseNote)
|
||||||
content += addReleaseNoteSection('⭐ Features', releaseNote.features)
|
)
|
||||||
|
content += addReleaseNoteSection("✓ Fixes", releaseNote.fixes?.map(fixToReleaseNote))
|
||||||
|
content += addReleaseNoteSection("🖌 Theme Changes", releaseNote.themeChanges)
|
||||||
|
content += addReleaseNoteSection("⭐ Features", releaseNote.features)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
function addReleaseNoteSection(title: string, items?: string[]): string {
|
function addReleaseNoteSection(title: string, items?: string[]): string {
|
||||||
if (!items) {
|
if (!items) {
|
||||||
return ''
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = `<h2>${title}</h2>`
|
let content = `<h2>${title}</h2>`
|
||||||
content += '<ul>'
|
content += "<ul>"
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item && item.length > 0) {
|
if (item && item.length > 0) {
|
||||||
content += `<li>${item}</li>`
|
content += `<li>${item}</li>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content += '</ul>'
|
content += "</ul>"
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixToReleaseNote(fix?: Exclude<ReleaseNote['fixes'], undefined>[number]) {
|
function fixToReleaseNote(fix?: Exclude<ReleaseNote["fixes"], undefined>[number]) {
|
||||||
if (typeof fix === 'string') {
|
if (typeof fix === "string") {
|
||||||
return fix
|
return fix
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fix || !fix.description || fix.description.length === 0) {
|
if (!fix || !fix.description || fix.description.length === 0) {
|
||||||
return ''
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
let note = fix.description
|
let note = fix.description
|
||||||
|
@ -119,13 +124,15 @@ function fixToReleaseNote(fix?: Exclude<ReleaseNote['fixes'], undefined>[number]
|
||||||
return note
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
function breakingChangeToReleaseNote(breakingChange?: Exclude<ReleaseNote['breakingChanges'], undefined>[number]) {
|
function breakingChangeToReleaseNote(
|
||||||
if (typeof breakingChange === 'string') {
|
breakingChange?: Exclude<ReleaseNote["breakingChanges"], undefined>[number]
|
||||||
|
) {
|
||||||
|
if (typeof breakingChange === "string") {
|
||||||
return breakingChange
|
return breakingChange
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!breakingChange || !breakingChange.description || breakingChange.description.length === 0) {
|
if (!breakingChange || !breakingChange.description || breakingChange.description.length === 0) {
|
||||||
return ''
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${breakingChange.description} (<a href="${breakingChange.link}" target="_blank">Learn more</a>)`
|
return `${breakingChange.description} (<a href="${breakingChange.link}" target="_blank">Learn more</a>)`
|
||||||
|
@ -134,10 +141,10 @@ function breakingChangeToReleaseNote(breakingChange?: Exclude<ReleaseNote['break
|
||||||
function pubDate(date?: Date) {
|
function pubDate(date?: Date) {
|
||||||
const newDate = date ?? new Date()
|
const newDate = date ?? new Date()
|
||||||
|
|
||||||
const pieces = newDate.toString().split(' ')
|
const pieces = newDate.toString().split(" ")
|
||||||
const offsetTime = pieces[5].match(/[-+]\d{4}/)
|
const offsetTime = pieces[5].match(/[-+]\d{4}/)
|
||||||
const offset = offsetTime ? offsetTime : pieces[5]
|
const offset = offsetTime ? offsetTime : pieces[5]
|
||||||
const parts = [`${pieces[0]},`, pieces[2], pieces[1], pieces[3], pieces[4], offset]
|
const parts = [`${pieces[0]},`, pieces[2], pieces[1], pieces[3], pieces[4], offset]
|
||||||
|
|
||||||
return parts.join(' ')
|
return parts.join(" ")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,18 @@
|
||||||
---
|
---
|
||||||
import Community from '~/components/Community.astro'
|
import Community from "~/components/Community.astro"
|
||||||
import Features from '~/components/Features.astro'
|
import Features from "~/components/Features.astro"
|
||||||
import Hero from '~/components/Hero.astro'
|
import Hero from "~/components/Hero.astro"
|
||||||
import Sponsors from '~/components/Sponsors.astro'
|
import Sponsors from "~/components/Sponsors.astro"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
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 />
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
---
|
---
|
||||||
import BackButton from '~/components/BackButton.astro'
|
import BackButton from "~/components/BackButton.astro"
|
||||||
import Button from '~/components/Button.astro'
|
import Button from "~/components/Button.astro"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
|
import ArrowRightIcon from "~/icons/ArrowRightIcon.astro"
|
||||||
import InfoIcon from '~/icons/InfoIcon.astro'
|
import InfoIcon from "~/icons/InfoIcon.astro"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { getAllMods, getAuthorLink, getLocalizedDate } from '~/mods'
|
import { getAllMods, getAuthorLink, getLocalizedDate } from "~/mods"
|
||||||
import { getUI } from '~/utils/i18n'
|
import { getUI } from "~/utils/i18n"
|
||||||
import { getLocale, getOtherLocales } from '~/utils/i18n'
|
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,
|
||||||
|
@ -54,8 +54,8 @@ const {
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title={slug.title.replace('{name}', mod.name)}
|
title={slug.title.replace("{name}", mod.name)}
|
||||||
description={slug.description.replace('{name}', mod.name)}
|
description={slug.description.replace("{name}", mod.name)}
|
||||||
ogImage={mod.image}
|
ogImage={mod.image}
|
||||||
>
|
>
|
||||||
<main class="mt-6 2xl:mt-0">
|
<main class="mt-6 2xl:mt-0">
|
||||||
|
@ -93,49 +93,32 @@ const {
|
||||||
<div class="flex flex-shrink-0 flex-col gap-2 font-normal">
|
<div class="flex flex-shrink-0 flex-col gap-2 font-normal">
|
||||||
<p
|
<p
|
||||||
set:html={slug.createdBy
|
set:html={slug.createdBy
|
||||||
.replace('{author}', mod.author)
|
.replace("{author}", mod.author)
|
||||||
.replace('{version}', mod.version)
|
.replace("{version}", mod.version)
|
||||||
.replace('{link}', getAuthorLink(mod.author))}
|
.replace("{link}", getAuthorLink(mod.author))}
|
||||||
/>
|
|
||||||
<p
|
|
||||||
set:html={slug.creationDate.replace('{createdAt}', dates.createdAt)}
|
|
||||||
/>
|
/>
|
||||||
|
<p set:html={slug.creationDate.replace("{createdAt}", dates.createdAt)} />
|
||||||
{
|
{
|
||||||
dates.createdAt !== dates.updatedAt && (
|
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
|
||||||
class="hidden"
|
class="hidden"
|
||||||
id="install-theme-uninstall"
|
id="install-theme-uninstall"
|
||||||
extra={{ 'zen-theme-id': mod.id }}
|
extra={{ "zen-theme-id": mod.id }}
|
||||||
isPrimary
|
isPrimary
|
||||||
>
|
>
|
||||||
{slug.uninstallMod}
|
{slug.uninstallMod}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
---
|
---
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import ModsList from '~/components/ModsList'
|
import ModsList from "~/components/ModsList"
|
||||||
import { CONSTANT } from '~/constants'
|
import { CONSTANT } from "~/constants"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { getAllMods } from '~/mods'
|
import { getAllMods } from "~/mods"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
import Title from '~/components/Title.astro'
|
import Title from "~/components/Title.astro"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { releaseNotes } from '~/release-notes'
|
import { releaseNotes } from "~/release-notes"
|
||||||
import { getStaticPaths as getI18nPaths, getLocale, getUI } from '~/utils/i18n'
|
import { getStaticPaths as getI18nPaths, getLocale, getUI } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
|
@ -15,12 +15,12 @@ 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 },
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
params: { slug: 'latest', locale },
|
params: { slug: "latest", locale },
|
||||||
props: { ...releaseNotes[0] },
|
props: { ...releaseNotes[0] },
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
@ -31,6 +31,6 @@ const release = Astro.props
|
||||||
|
|
||||||
<Layout title={slug.title} redirect={`/release-notes#${release.version}`}>
|
<Layout title={slug.title} redirect={`/release-notes#${release.version}`}>
|
||||||
<main class="flex flex-col items-center pb-52 pt-36">
|
<main class="flex flex-col items-center pb-52 pt-36">
|
||||||
{slug.redirect.replaceAll('{version}', release.version)}
|
{slug.redirect.replaceAll("{version}", release.version)}
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
---
|
---
|
||||||
import { Modal, ModalBody, ModalHeader } from 'free-astro-components'
|
import { Modal, ModalBody, ModalHeader } from "free-astro-components"
|
||||||
import Button from '~/components/Button.astro'
|
import Button from "~/components/Button.astro"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import ReleaseNoteItem from '~/components/ReleaseNoteItem.astro'
|
import ReleaseNoteItem from "~/components/ReleaseNoteItem.astro"
|
||||||
import ArrowUpIcon from '~/icons/ArrowUp.astro'
|
import ArrowUpIcon from "~/icons/ArrowUp.astro"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { releaseNotes as releaseNotesData, releaseNotesTwilight } from '~/release-notes'
|
import { releaseNotes as releaseNotesData, releaseNotesTwilight } from "~/release-notes"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
|
@ -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,41 +75,39 @@ 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")
|
||||||
const container = document.getElementById('release-notes')
|
const container = document.getElementById("release-notes")
|
||||||
const modal = document.getElementById('version-modal')
|
const modal = document.getElementById("version-modal")
|
||||||
const versionList = document.getElementById('version-list')
|
const versionList = document.getElementById("version-list")
|
||||||
|
|
||||||
const toggleScrollButton = () => {
|
const toggleScrollButton = () => {
|
||||||
if (!scrollTopButton || !versionButton) return
|
if (!scrollTopButton || !versionButton) return
|
||||||
|
|
||||||
const descriptionPosition = versionButton.getBoundingClientRect().bottom
|
const descriptionPosition = versionButton.getBoundingClientRect().bottom
|
||||||
if (descriptionPosition < 0) {
|
if (descriptionPosition < 0) {
|
||||||
scrollTopButton.classList.remove('hidden')
|
scrollTopButton.classList.remove("hidden")
|
||||||
scrollTopButton.classList.add('block')
|
scrollTopButton.classList.add("block")
|
||||||
} else {
|
} else {
|
||||||
scrollTopButton.classList.remove('block')
|
scrollTopButton.classList.remove("block")
|
||||||
scrollTopButton.classList.add('hidden')
|
scrollTopButton.classList.add("hidden")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigateToVersion = (e: MouseEvent) => {
|
const navigateToVersion = (e: MouseEvent) => {
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
if (!container || !target?.closest('[data-version]')) return
|
if (!container || !target?.closest("[data-version]")) return
|
||||||
|
|
||||||
const version = target.getAttribute('data-version')
|
const version = target.getAttribute("data-version")
|
||||||
if (!version) return
|
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", "")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,21 +119,21 @@ const {
|
||||||
openModal(modal)
|
openModal(modal)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('scroll', toggleScrollButton)
|
window.addEventListener("scroll", toggleScrollButton)
|
||||||
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
|
||||||
document.body.style.overflow = ''
|
document.body.style.overflow = ""
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style is:global>
|
<style is:global>
|
||||||
#version-modal > * {
|
#version-modal > * {
|
||||||
font-family: 'Bricolage Grotesque', sans-serif !important;
|
font-family: "Bricolage Grotesque", sans-serif !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
import Features from '~/components/Features.astro'
|
import Features from "~/components/Features.astro"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from "~/utils/i18n"
|
||||||
|
|
||||||
const locale = getLocale(Astro)
|
const locale = getLocale(Astro)
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
---
|
---
|
||||||
import Button from '~/components/Button.astro'
|
import Button from "~/components/Button.astro"
|
||||||
import Description from '~/components/Description.astro'
|
import Description from "~/components/Description.astro"
|
||||||
import SocialMediaStrip from '~/components/SocialMediaStrip.astro'
|
import SocialMediaStrip from "~/components/SocialMediaStrip.astro"
|
||||||
import ArrowRightIcon from '~/icons/ArrowRightIcon.astro'
|
import ArrowRightIcon from "~/icons/ArrowRightIcon.astro"
|
||||||
import Layout from '~/layouts/Layout.astro'
|
import Layout from "~/layouts/Layout.astro"
|
||||||
|
|
||||||
import whatsNewVideo from '~/assets/whats-new.mp4'
|
import whatsNewVideo from "~/assets/whats-new.mp4"
|
||||||
import Video from '~/components/Video.astro'
|
import Video from "~/components/Video.astro"
|
||||||
import { releaseNotes } from '~/release-notes'
|
import { releaseNotes } from "~/release-notes"
|
||||||
import whatsNewText from '~/release-notes/whats-new.json'
|
import whatsNewText from "~/release-notes/whats-new.json"
|
||||||
import { getLocale, getUI } from '~/utils/i18n'
|
import { getLocale, getUI } from "~/utils/i18n"
|
||||||
export { getStaticPaths } from '~/utils/i18n'
|
export { getStaticPaths } from "~/utils/i18n"
|
||||||
|
|
||||||
const latestVersion = releaseNotes[0]
|
const latestVersion = releaseNotes[0]
|
||||||
|
|
||||||
|
@ -22,40 +22,27 @@ const {
|
||||||
} = getUI(locale)
|
} = getUI(locale)
|
||||||
|
|
||||||
// Just redirect to the release notes if we are in a patch version
|
// Just redirect to the release notes if we are in a patch version
|
||||||
if (latestVersion.version.split('.').length > 2 && whatsNewText[1] !== latestVersion.version) {
|
if (latestVersion.version.split(".").length > 2 && whatsNewText[1] !== latestVersion.version) {
|
||||||
return Astro.redirect(`/release-notes#${latestVersion.version}`)
|
return Astro.redirect(`/release-notes#${latestVersion.version}`)
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<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>
|
||||||
<div>
|
<div>
|
||||||
<Fragment set:html={whatsNewText[0].replace(/\n/g, '<br>')} />
|
<Fragment set:html={whatsNewText[0].replace(/\n/g, "<br>")} />
|
||||||
</div>
|
</div>
|
||||||
<ul class="hidden list-disc flex-col gap-2 xl:container xl:flex">
|
<ul class="hidden list-disc flex-col gap-2 xl:container xl:flex">
|
||||||
<a
|
<a href="https://github.com/zen-browser/desktop/issues/new/choose" target="_blank">
|
||||||
href="https://github.com/zen-browser/desktop/issues/new/choose"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<Description class="text-base font-bold">
|
<Description class="text-base font-bold">
|
||||||
{whatsNew.reportIssue}
|
{whatsNew.reportIssue}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import releaseNotesStable from './release-notes/stable.json'
|
import releaseNotesStable from "./release-notes/stable.json"
|
||||||
|
|
||||||
interface FixWithIssue {
|
type FixWithIssue = {
|
||||||
description: string
|
description: string
|
||||||
issue?: number
|
issue?: number
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ type Fix = string | FixWithIssue
|
||||||
|
|
||||||
export type BreakingChange = string | { description: string; link: string }
|
export type BreakingChange = string | { description: string; link: string }
|
||||||
|
|
||||||
export interface ReleaseNote {
|
export type ReleaseNote = {
|
||||||
version: string
|
version: string
|
||||||
date?: string // optional for twilight
|
date?: string // optional for twilight
|
||||||
extra?: string
|
extra?: string
|
||||||
|
@ -25,12 +25,12 @@ export interface ReleaseNote {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const releaseNotes: ReleaseNote[] = releaseNotesStable.reverse()
|
export const releaseNotes: ReleaseNote[] = releaseNotesStable.reverse()
|
||||||
export { default as releaseNotesTwilight } from './release-notes/twilight.json'
|
export { default as releaseNotesTwilight } from "./release-notes/twilight.json"
|
||||||
|
|
||||||
export function getReleaseNoteFirefoxVersion(releaseNote: ReleaseNote): string | null {
|
export function getReleaseNoteFirefoxVersion(releaseNote: ReleaseNote): string | null {
|
||||||
// Check if "firefox" is on the feature list
|
// Check if "firefox" is on the feature list
|
||||||
for (const feature of releaseNote.features || []) {
|
for (const feature of releaseNote.features || []) {
|
||||||
if (feature.toLowerCase().includes('firefox')) {
|
if (feature.toLowerCase().includes("firefox")) {
|
||||||
// may be X or X.X or X.X.X
|
// may be X or X.X or X.X.X
|
||||||
const match = feature.match(/(\d+(\.\d+){0,2})/)
|
const match = feature.match(/(\d+(\.\d+){0,2})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
|
|
|
@ -601,7 +601,10 @@
|
||||||
"version": "1.0.0-a.30",
|
"version": "1.0.0-a.30",
|
||||||
"date": "26/08/2024",
|
"date": "26/08/2024",
|
||||||
"extra": "This release is the thirtieth alpha release of the 1.0.0-alpha series.",
|
"extra": "This release is the thirtieth alpha release of the 1.0.0-alpha series.",
|
||||||
"features": ["Added support for 24 more languages!", "Update installed mods from the browser settings"],
|
"features": [
|
||||||
|
"Added support for 24 more languages!",
|
||||||
|
"Update installed mods from the browser settings"
|
||||||
|
],
|
||||||
"fixes": [
|
"fixes": [
|
||||||
{
|
{
|
||||||
"description": "Letterboxing option is missing",
|
"description": "Letterboxing option is missing",
|
||||||
|
@ -924,7 +927,11 @@
|
||||||
"date": "24/09/2024",
|
"date": "24/09/2024",
|
||||||
"workflowId": 11020784612,
|
"workflowId": 11020784612,
|
||||||
"extra": "This update is a small patch to fix some issues that weren't addressed in the previous release!",
|
"extra": "This update is a small patch to fix some issues that weren't addressed in the previous release!",
|
||||||
"features": ["Moved application menu button to the right", "Added new shortcuts", "Collapsed tab sidebar is now smaller"],
|
"features": [
|
||||||
|
"Moved application menu button to the right",
|
||||||
|
"Added new shortcuts",
|
||||||
|
"Collapsed tab sidebar is now smaller"
|
||||||
|
],
|
||||||
"fixes": [
|
"fixes": [
|
||||||
{
|
{
|
||||||
"description": "Fixed issue with hovering over window control buttons (macOS)"
|
"description": "Fixed issue with hovering over window control buttons (macOS)"
|
||||||
|
@ -949,7 +956,9 @@
|
||||||
"Improved Expand Tabs on Hover layout"
|
"Improved Expand Tabs on Hover layout"
|
||||||
],
|
],
|
||||||
"themeChanges": ["Toggle inputs will not use the themed tertiary color"],
|
"themeChanges": ["Toggle inputs will not use the themed tertiary color"],
|
||||||
"breakingChanges": ["The keyboard shortcuts will be overriden by the defaults ones in this update"],
|
"breakingChanges": [
|
||||||
|
"The keyboard shortcuts will be overriden by the defaults ones in this update"
|
||||||
|
],
|
||||||
"fixes": [
|
"fixes": [
|
||||||
{
|
{
|
||||||
"description": "Fixed Firefox add-ons not updating",
|
"description": "Fixed Firefox add-ons not updating",
|
||||||
|
@ -1121,7 +1130,10 @@
|
||||||
"description": "Fixed about page linking 'global Community' to a Mozilla page"
|
"description": "Fixed about page linking 'global Community' to a Mozilla page"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"features": ["About page will now display the Firefox version used", "Disabled forcing container grouping for workspaces"]
|
"features": [
|
||||||
|
"About page will now display the Firefox version used",
|
||||||
|
"Disabled forcing container grouping for workspaces"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"version": "1.0.1-a.11",
|
"version": "1.0.1-a.11",
|
||||||
|
@ -1258,7 +1270,9 @@
|
||||||
"description": "Fixed sidebar webpanels being in a darker contrast"
|
"description": "Fixed sidebar webpanels being in a darker contrast"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"features": ["Added a confirmation dialog when the gradient generator has successfully saved the gradient"]
|
"features": [
|
||||||
|
"Added a confirmation dialog when the gradient generator has successfully saved the gradient"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"version": "1.0.1-a.15",
|
"version": "1.0.1-a.15",
|
||||||
|
@ -2097,7 +2111,10 @@
|
||||||
"date": "30/01/2025",
|
"date": "30/01/2025",
|
||||||
"workflowId": 13062083313,
|
"workflowId": 13062083313,
|
||||||
"extra": "Quick fix for a critical bug that was introduced in the previous release.",
|
"extra": "Quick fix for a critical bug that was introduced in the previous release.",
|
||||||
"fixes": ["Fixed the browser not opening when having multiple windows", "Fixed macos fullscreen having a weird shadow"]
|
"fixes": [
|
||||||
|
"Fixed the browser not opening when having multiple windows",
|
||||||
|
"Fixed macos fullscreen having a weird shadow"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"version": "1.7.5b",
|
"version": "1.7.5b",
|
||||||
|
@ -2154,7 +2171,9 @@
|
||||||
"Fixed opening glance tabs on essentials messing up the sidebar",
|
"Fixed opening glance tabs on essentials messing up the sidebar",
|
||||||
"Fixed pinned tabs appearing on normal container after a restart"
|
"Fixed pinned tabs appearing on normal container after a restart"
|
||||||
],
|
],
|
||||||
"features": ["Tabs can now be dragged into pinned tabs by dragging them into the workspace indicator"],
|
"features": [
|
||||||
|
"Tabs can now be dragged into pinned tabs by dragging them into the workspace indicator"
|
||||||
|
],
|
||||||
"workflowId": 13209591935,
|
"workflowId": 13209591935,
|
||||||
"date": "08/02/2025"
|
"date": "08/02/2025"
|
||||||
},
|
},
|
||||||
|
@ -2684,7 +2703,10 @@
|
||||||
"'All tabs' menu not showing any text when collapsed toolbar is enabled."
|
"'All tabs' menu not showing any text when collapsed toolbar is enabled."
|
||||||
],
|
],
|
||||||
"security": "https://www.mozilla.org/en-US/security/advisories/mfsa2025-36/",
|
"security": "https://www.mozilla.org/en-US/security/advisories/mfsa2025-36/",
|
||||||
"features": ["Updated to Firefox 138.0.4", "Better compact mode support for multiple toolbars."],
|
"features": [
|
||||||
|
"Updated to Firefox 138.0.4",
|
||||||
|
"Better compact mode support for multiple toolbars."
|
||||||
|
],
|
||||||
"knownIssues": ["Selecting a tab on private mode doesn't scroll to make the tab visible."],
|
"knownIssues": ["Selecting a tab on private mode doesn't scroll to make the tab visible."],
|
||||||
"themeChanges": [
|
"themeChanges": [
|
||||||
"Changed the layout of workspaces and their icons internally to provide a more stable layout that doesn't require floating elements. We finally managed to get it to how we wanted it to be, so it will change less in the future."
|
"Changed the layout of workspaces and their icons internally to provide a more stable layout that doesn't require floating elements. We finally managed to get it to how we wanted it to be, so it will change less in the future."
|
||||||
|
|
|
@ -1,100 +1,101 @@
|
||||||
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'
|
|
||||||
|
|
||||||
describe('<Button />', () => {
|
import Button from "~/components/Button.astro"
|
||||||
|
|
||||||
|
describe("<Button />", () => {
|
||||||
let container: Awaited<ReturnType<typeof AstroContainer.create>>
|
let container: Awaited<ReturnType<typeof AstroContainer.create>>
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
container = await AstroContainer.create()
|
container = await AstroContainer.create()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as <button>', () => {
|
describe("as <button>", () => {
|
||||||
it('renders default <button> with slot', async () => {
|
it("renders default <button> with slot", async () => {
|
||||||
const result = await container.renderToString(Button, {
|
const result = await container.renderToString(Button, {
|
||||||
props: {},
|
props: {},
|
||||||
slots: { default: 'Click me' },
|
slots: { default: "Click me" },
|
||||||
})
|
})
|
||||||
expect(result).toContain('<button')
|
expect(result).toContain("<button")
|
||||||
expect(result).toContain('Click me')
|
expect(result).toContain("Click me")
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['isPrimary', { isPrimary: true }, 'bg-dark'],
|
["isPrimary", { isPrimary: true }, "bg-dark"],
|
||||||
['isAlert', { isAlert: true }, 'bg-red-300'],
|
["isAlert", { isAlert: true }, "bg-red-300"],
|
||||||
['isBordered', { isBordered: true }, 'border-2'],
|
["isBordered", { isBordered: true }, "border-2"],
|
||||||
])('applies %s style', async (_label, propObj, expectedClass) => {
|
])("applies %s style", async (_label, propObj, expectedClass) => {
|
||||||
const result = await container.renderToString(Button, {
|
const result = await container.renderToString(Button, {
|
||||||
props: { ...propObj },
|
props: { ...propObj },
|
||||||
slots: { default: 'Test' },
|
slots: { default: "Test" },
|
||||||
})
|
})
|
||||||
expect(result).toContain('<button')
|
expect(result).toContain("<button")
|
||||||
expect(result).toContain(expectedClass)
|
expect(result).toContain(expectedClass)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies id and extra props', async () => {
|
it("applies id and extra props", async () => {
|
||||||
const result = await container.renderToString(Button, {
|
const result = await container.renderToString(Button, {
|
||||||
props: {
|
props: {
|
||||||
id: 'my-btn',
|
id: "my-btn",
|
||||||
extra: { 'data-test': 'foo' },
|
extra: { "data-test": "foo" },
|
||||||
},
|
},
|
||||||
slots: { default: 'Test' },
|
slots: { default: "Test" },
|
||||||
})
|
})
|
||||||
expect(result).toContain('id="my-btn"')
|
expect(result).toContain('id="my-btn"')
|
||||||
expect(result).toContain('data-test="foo"')
|
expect(result).toContain('data-test="foo"')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('as <a>', () => {
|
describe("as <a>", () => {
|
||||||
it('renders <a> with slot and href', async () => {
|
it("renders <a> with slot and href", async () => {
|
||||||
const result = await container.renderToString(Button, {
|
const result = await container.renderToString(Button, {
|
||||||
props: { href: '/link' },
|
props: { href: "/link" },
|
||||||
slots: { default: 'Go' },
|
slots: { default: "Go" },
|
||||||
})
|
})
|
||||||
expect(result).toContain('<a')
|
expect(result).toContain("<a")
|
||||||
expect(result).toContain('Go')
|
expect(result).toContain("Go")
|
||||||
expect(result).toContain('href="/en/link"')
|
expect(result).toContain('href="/en/link"')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['isPrimary', { isPrimary: true }, 'bg-dark'],
|
["isPrimary", { isPrimary: true }, "bg-dark"],
|
||||||
['isAlert', { isAlert: true }, 'bg-red-300'],
|
["isAlert", { isAlert: true }, "bg-red-300"],
|
||||||
['isBordered', { isBordered: true }, 'border-2'],
|
["isBordered", { isBordered: true }, "border-2"],
|
||||||
])('applies %s style', async (_label, propObj, expectedClass) => {
|
])("applies %s style", async (_label, propObj, expectedClass) => {
|
||||||
const result = await container.renderToString(Button, {
|
const result = await container.renderToString(Button, {
|
||||||
props: { href: '/link', ...propObj },
|
props: { href: "/link", ...propObj },
|
||||||
slots: { default: 'Test' },
|
slots: { default: "Test" },
|
||||||
})
|
})
|
||||||
expect(result).toContain('<a')
|
expect(result).toContain("<a")
|
||||||
expect(result).toContain(expectedClass)
|
expect(result).toContain(expectedClass)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies id and extra props', async () => {
|
it("applies id and extra props", async () => {
|
||||||
const result = await container.renderToString(Button, {
|
const result = await container.renderToString(Button, {
|
||||||
props: {
|
props: {
|
||||||
href: '/link',
|
href: "/link",
|
||||||
id: 'my-link',
|
id: "my-link",
|
||||||
extra: { 'data-test': 'bar' },
|
extra: { "data-test": "bar" },
|
||||||
},
|
},
|
||||||
slots: { default: 'Test' },
|
slots: { default: "Test" },
|
||||||
})
|
})
|
||||||
expect(result).toContain('id="my-link"')
|
expect(result).toContain('id="my-link"')
|
||||||
expect(result).toContain('data-test="bar"')
|
expect(result).toContain('data-test="bar"')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies custom className', async () => {
|
it("applies custom className", async () => {
|
||||||
const result = await container.renderToString(Button, {
|
const result = await container.renderToString(Button, {
|
||||||
props: { class: 'custom-class' },
|
props: { class: "custom-class" },
|
||||||
slots: { default: 'Test' },
|
slots: { default: "Test" },
|
||||||
})
|
})
|
||||||
expect(result).toContain('custom-class')
|
expect(result).toContain("custom-class")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses locale path for href', async () => {
|
it("uses locale path for href", async () => {
|
||||||
const result = await container.renderToString(Button, {
|
const result = await container.renderToString(Button, {
|
||||||
props: { href: '/foo' },
|
props: { href: "/foo" },
|
||||||
slots: { default: 'Test' },
|
slots: { default: "Test" },
|
||||||
})
|
})
|
||||||
expect(result).toContain('href="/en/foo"')
|
expect(result).toContain('href="/en/foo"')
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,47 +1,48 @@
|
||||||
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'
|
|
||||||
|
|
||||||
describe('<ButtonCard />', () => {
|
import ButtonCard from "~/components/download/ButtonCard.astro"
|
||||||
|
|
||||||
|
describe("<ButtonCard />", () => {
|
||||||
let container: Awaited<ReturnType<typeof AstroContainer.create>>
|
let container: Awaited<ReturnType<typeof AstroContainer.create>>
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
container = await AstroContainer.create()
|
container = await AstroContainer.create()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders with required props', async () => {
|
it("renders with required props", async () => {
|
||||||
const result = await container.renderToString(ButtonCard, {
|
const result = await container.renderToString(ButtonCard, {
|
||||||
props: {
|
props: {
|
||||||
label: 'Download',
|
label: "Download",
|
||||||
href: '/download',
|
href: "/download",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(result).toContain('Download')
|
expect(result).toContain("Download")
|
||||||
expect(result).toContain('href="/download"')
|
expect(result).toContain('href="/download"')
|
||||||
expect(result).not.toContain('Show SHA-256')
|
expect(result).not.toContain("Show SHA-256")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders with checksum', async () => {
|
it("renders with checksum", async () => {
|
||||||
const result = await container.renderToString(ButtonCard, {
|
const result = await container.renderToString(ButtonCard, {
|
||||||
props: {
|
props: {
|
||||||
label: 'Download',
|
label: "Download",
|
||||||
href: '/download',
|
href: "/download",
|
||||||
checksum: 'sha256sum',
|
checksum: "sha256sum",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(result).toContain('Show SHA-256')
|
expect(result).toContain("Show SHA-256")
|
||||||
expect(result).toContain('sha256sum')
|
expect(result).toContain("sha256sum")
|
||||||
expect(result).toContain('Copy')
|
expect(result).toContain("Copy")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders with variant', async () => {
|
it("renders with variant", async () => {
|
||||||
const result = await container.renderToString(ButtonCard, {
|
const result = await container.renderToString(ButtonCard, {
|
||||||
props: {
|
props: {
|
||||||
label: 'Download',
|
label: "Download",
|
||||||
href: '/download',
|
href: "/download",
|
||||||
variant: 'flathub',
|
variant: "flathub",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(result).toContain('Download')
|
expect(result).toContain("Download")
|
||||||
expect(result).toContain('Beta')
|
expect(result).toContain("Beta")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,125 +1,126 @@
|
||||||
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'
|
|
||||||
|
|
||||||
const mockIcon = ['<svg></svg>']
|
import PlatformDownload from "~/components/download/PlatformDownload.astro"
|
||||||
|
|
||||||
|
const mockIcon = ["<svg></svg>"]
|
||||||
const mockReleases = {
|
const mockReleases = {
|
||||||
universal: { label: 'Universal', link: '/universal', checksum: 'abc123' },
|
universal: { label: "Universal", link: "/universal", checksum: "abc123" },
|
||||||
x86_64: { label: 'x86_64', link: '/x86_64', checksum: 'def456' },
|
x86_64: { label: "x86_64", link: "/x86_64", checksum: "def456" },
|
||||||
arm64: { label: 'ARM64', link: '/arm64', checksum: 'ghi789' },
|
arm64: { label: "ARM64", link: "/arm64", checksum: "ghi789" },
|
||||||
flathub: { all: { label: 'Flathub', link: '/flathub' } },
|
flathub: { all: { label: "Flathub", link: "/flathub" } },
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('<PlatformDownload />', () => {
|
describe("<PlatformDownload />", () => {
|
||||||
let container: Awaited<ReturnType<typeof AstroContainer.create>>
|
let container: Awaited<ReturnType<typeof AstroContainer.create>>
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
container = await AstroContainer.create()
|
container = await AstroContainer.create()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders mac platform', async () => {
|
it("renders mac platform", async () => {
|
||||||
const result = await container.renderToString(PlatformDownload, {
|
const result = await container.renderToString(PlatformDownload, {
|
||||||
props: {
|
props: {
|
||||||
platform: 'mac',
|
platform: "mac",
|
||||||
icon: mockIcon,
|
icon: mockIcon,
|
||||||
title: 'Mac Title',
|
title: "Mac Title",
|
||||||
description: 'Mac Desc',
|
description: "Mac Desc",
|
||||||
releases: mockReleases,
|
releases: mockReleases,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(result).toContain('Mac Title')
|
expect(result).toContain("Mac Title")
|
||||||
expect(result).toContain('Mac Desc')
|
expect(result).toContain("Mac Desc")
|
||||||
expect(result).toContain('Universal')
|
expect(result).toContain("Universal")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders windows platform', async () => {
|
it("renders windows platform", async () => {
|
||||||
const result = await container.renderToString(PlatformDownload, {
|
const result = await container.renderToString(PlatformDownload, {
|
||||||
props: {
|
props: {
|
||||||
platform: 'windows',
|
platform: "windows",
|
||||||
icon: mockIcon,
|
icon: mockIcon,
|
||||||
title: 'Win Title',
|
title: "Win Title",
|
||||||
description: 'Win Desc',
|
description: "Win Desc",
|
||||||
releases: mockReleases,
|
releases: mockReleases,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(result).toContain('Win Title')
|
expect(result).toContain("Win Title")
|
||||||
expect(result).toContain('Win Desc')
|
expect(result).toContain("Win Desc")
|
||||||
expect(result).toContain('x86_64')
|
expect(result).toContain("x86_64")
|
||||||
expect(result).toContain('ARM64')
|
expect(result).toContain("ARM64")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders linux platform with flathub and tarball', async () => {
|
it("renders linux platform with flathub and tarball", async () => {
|
||||||
const linuxReleases = {
|
const linuxReleases = {
|
||||||
flathub: { all: { label: 'Flathub', link: '/flathub' } },
|
flathub: { all: { label: "Flathub", link: "/flathub" } },
|
||||||
x86_64: {
|
x86_64: {
|
||||||
tarball: {
|
tarball: {
|
||||||
label: 'Tarball x86_64',
|
label: "Tarball x86_64",
|
||||||
link: '/tarball-x86_64',
|
link: "/tarball-x86_64",
|
||||||
checksum: 'sha256',
|
checksum: "sha256",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const result = await container.renderToString(PlatformDownload, {
|
const result = await container.renderToString(PlatformDownload, {
|
||||||
props: {
|
props: {
|
||||||
platform: 'linux',
|
platform: "linux",
|
||||||
icon: mockIcon,
|
icon: mockIcon,
|
||||||
title: 'Linux Title',
|
title: "Linux Title",
|
||||||
description: 'Linux Desc',
|
description: "Linux Desc",
|
||||||
releases: linuxReleases,
|
releases: linuxReleases,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(result).toContain('Linux Title')
|
expect(result).toContain("Linux Title")
|
||||||
expect(result).toContain('Linux Desc')
|
expect(result).toContain("Linux Desc")
|
||||||
expect(result).toContain('Flathub')
|
expect(result).toContain("Flathub")
|
||||||
expect(result).toContain('Tarball')
|
expect(result).toContain("Tarball")
|
||||||
expect(result).toContain('x86_64')
|
expect(result).toContain("x86_64")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders linux platform with all branches', async () => {
|
it("renders linux platform with all branches", async () => {
|
||||||
const linuxReleases = {
|
const linuxReleases = {
|
||||||
flathub: { all: { label: 'Flathub', link: '/flathub' } },
|
flathub: { all: { label: "Flathub", link: "/flathub" } },
|
||||||
x86_64: {
|
x86_64: {
|
||||||
tarball: {
|
tarball: {
|
||||||
label: 'Tarball x86_64',
|
label: "Tarball x86_64",
|
||||||
link: '/tarball-x86_64',
|
link: "/tarball-x86_64",
|
||||||
checksum: 'sha256',
|
checksum: "sha256",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
aarch64: {
|
aarch64: {
|
||||||
tarball: {
|
tarball: {
|
||||||
label: 'Tarball ARM64',
|
label: "Tarball ARM64",
|
||||||
link: '/tarball-arm64',
|
link: "/tarball-arm64",
|
||||||
checksum: 'sha256-arm64',
|
checksum: "sha256-arm64",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const result = await container.renderToString(PlatformDownload, {
|
const result = await container.renderToString(PlatformDownload, {
|
||||||
props: {
|
props: {
|
||||||
platform: 'linux',
|
platform: "linux",
|
||||||
icon: mockIcon,
|
icon: mockIcon,
|
||||||
title: 'Linux Title',
|
title: "Linux Title",
|
||||||
description: 'Linux Desc',
|
description: "Linux Desc",
|
||||||
releases: linuxReleases,
|
releases: linuxReleases,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test basic content
|
// Test basic content
|
||||||
expect(result).toContain('Linux Title')
|
expect(result).toContain("Linux Title")
|
||||||
expect(result).toContain('Linux Desc')
|
expect(result).toContain("Linux Desc")
|
||||||
|
|
||||||
// Test Flathub section
|
// Test Flathub section
|
||||||
expect(result).toContain('Flathub')
|
expect(result).toContain("Flathub")
|
||||||
expect(result).toContain('/flathub')
|
expect(result).toContain("/flathub")
|
||||||
|
|
||||||
// Test x86_64 section
|
// Test x86_64 section
|
||||||
expect(result).toContain('x86_64')
|
expect(result).toContain("x86_64")
|
||||||
expect(result).toContain('Tarball x86_64')
|
expect(result).toContain("Tarball x86_64")
|
||||||
expect(result).toContain('/tarball-x86_64')
|
expect(result).toContain("/tarball-x86_64")
|
||||||
expect(result).toContain('sha256')
|
expect(result).toContain("sha256")
|
||||||
|
|
||||||
// Test ARM64 section
|
// Test ARM64 section
|
||||||
expect(result).toContain('ARM64')
|
expect(result).toContain("ARM64")
|
||||||
expect(result).toContain('Tarball ARM64')
|
expect(result).toContain("Tarball ARM64")
|
||||||
expect(result).toContain('/tarball-arm64')
|
expect(result).toContain("/tarball-arm64")
|
||||||
expect(result).toContain('sha256-arm64')
|
expect(result).toContain("sha256-arm64")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from "vitest"
|
||||||
import { getReleasesWithChecksums } from '~/components/download/release-data'
|
|
||||||
|
|
||||||
describe('getReleasesWithChecksums', () => {
|
import { getReleasesWithChecksums } from "~/components/download/release-data"
|
||||||
it('returns correct structure with checksums', () => {
|
|
||||||
|
describe("getReleasesWithChecksums", () => {
|
||||||
|
it("returns correct structure with checksums", () => {
|
||||||
const checksums = {
|
const checksums = {
|
||||||
'zen.macos-universal.dmg': 'macsum',
|
"zen.macos-universal.dmg": "macsum",
|
||||||
'zen.installer.exe': 'winsum',
|
"zen.installer.exe": "winsum",
|
||||||
'zen.installer-arm64.exe': 'winarmsum',
|
"zen.installer-arm64.exe": "winarmsum",
|
||||||
'zen.linux-x86_64.tar.xz': 'linx86sum',
|
"zen.linux-x86_64.tar.xz": "linx86sum",
|
||||||
'zen.linux-aarch64.tar.xz': 'linaarchsum',
|
"zen.linux-aarch64.tar.xz": "linaarchsum",
|
||||||
}
|
}
|
||||||
const releases = getReleasesWithChecksums(checksums)
|
const releases = getReleasesWithChecksums(checksums)
|
||||||
expect(releases.macos.universal.checksum).toBe('macsum')
|
expect(releases.macos.universal.checksum).toBe("macsum")
|
||||||
expect(releases.windows.x86_64.checksum).toBe('winsum')
|
expect(releases.windows.x86_64.checksum).toBe("winsum")
|
||||||
expect(releases.windows.arm64.checksum).toBe('winarmsum')
|
expect(releases.windows.arm64.checksum).toBe("winarmsum")
|
||||||
expect(releases.linux.x86_64.tarball.checksum).toBe('linx86sum')
|
expect(releases.linux.x86_64.tarball.checksum).toBe("linx86sum")
|
||||||
expect(releases.linux.aarch64.tarball.checksum).toBe('linaarchsum')
|
expect(releases.linux.aarch64.tarball.checksum).toBe("linaarchsum")
|
||||||
expect(releases.linux.flathub.all.label).toBe('Flathub')
|
expect(releases.linux.flathub.all.label).toBe("Flathub")
|
||||||
expect(releases.linux.flathub.all.link).toBe('https://flathub.org/apps/app.zen_browser.zen')
|
expect(releases.linux.flathub.all.link).toBe("https://flathub.org/apps/app.zen_browser.zen")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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"
|
||||||
|
|
||||||
// Helper to get the platform section by id
|
// Helper to get the platform section by id
|
||||||
const getPlatformSection = (page: Page, platform: string) =>
|
const getPlatformSection = (page: Page, platform: string) =>
|
||||||
|
@ -12,90 +12,100 @@ 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 }[] = [
|
||||||
{
|
{
|
||||||
name: 'windows',
|
name: "windows",
|
||||||
userAgent:
|
userAgent:
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
platform: 'Win32',
|
platform: "Win32",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'mac',
|
name: "mac",
|
||||||
userAgent:
|
userAgent:
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15',
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15",
|
||||||
platform: 'MacIntel',
|
platform: "MacIntel",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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:
|
||||||
platform: 'Linux x86_64',
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
platform: "Linux x86_64",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
test.describe('Download page default tab per platform', () => {
|
test.describe("Download page default tab per platform", () => {
|
||||||
for (const { name, userAgent, platform } of platformConfigs) {
|
for (const { name, userAgent, platform } of platformConfigs) {
|
||||||
test(`shows correct default tab for ${name} platform`, async ({ browser }) => {
|
test(`shows correct default tab for ${name} platform`, async ({ browser }) => {
|
||||||
const context = await browser.newContext({
|
const context = await browser.newContext({
|
||||||
userAgent,
|
userAgent,
|
||||||
locale: 'en-US',
|
locale: "en-US",
|
||||||
platform,
|
platform,
|
||||||
} as BrowserContextOptions)
|
} as BrowserContextOptions)
|
||||||
const page = await context.newPage()
|
const page = await context.newPage()
|
||||||
await page.goto('/download')
|
await page.goto("/download")
|
||||||
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")
|
||||||
}
|
}
|
||||||
await context.close()
|
await context.close()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Download page platform detection and tab switching', () => {
|
test.describe("Download page platform detection and tab switching", () => {
|
||||||
test('shows correct platform section and tab when switching platforms', async ({ page }) => {
|
test("shows correct platform section and tab when switching platforms", async ({ page }) => {
|
||||||
await page.goto('/download')
|
await page.goto("/download")
|
||||||
const platforms = ['windows', 'mac', 'linux']
|
const platforms = ["windows", "mac", "linux"]
|
||||||
for (const platform of platforms) {
|
for (const platform of platforms) {
|
||||||
await getPlatformButton(page, platform).click()
|
await getPlatformButton(page, platform).click()
|
||||||
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"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Download page download links', () => {
|
test.describe("Download page download links", () => {
|
||||||
const releases = getReleasesWithChecksums(CONSTANT.CHECKSUMS)
|
const releases = getReleasesWithChecksums(CONSTANT.CHECKSUMS)
|
||||||
|
|
||||||
function getPlatformLinks(releases: ReturnType<typeof getReleasesWithChecksums>) {
|
function getPlatformLinks(releases: ReturnType<typeof getReleasesWithChecksums>) {
|
||||||
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,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('all platform download links are correct', async ({ page }) => {
|
test("all platform download links are correct", async ({ page }) => {
|
||||||
const platforms = ['windows', 'mac', 'linux']
|
const platforms = ["windows", "mac", "linux"]
|
||||||
const platformLinkSelectors = getPlatformLinks(releases)
|
const platformLinkSelectors = getPlatformLinks(releases)
|
||||||
await page.goto('/download')
|
await page.goto("/download")
|
||||||
await page.waitForLoadState('domcontentloaded')
|
await page.waitForLoadState("domcontentloaded")
|
||||||
for (const platform of platforms) {
|
for (const platform of platforms) {
|
||||||
await getPlatformButton(page, platform).click()
|
await getPlatformButton(page, platform).click()
|
||||||
for (const { label, link } of platformLinkSelectors[platform as keyof typeof platformLinkSelectors]) {
|
for (const { label, link } of platformLinkSelectors[
|
||||||
|
platform as keyof typeof platformLinkSelectors
|
||||||
|
]) {
|
||||||
const downloadLink = page.locator(`#${platform}-downloads .download-link[href="${link}"]`)
|
const downloadLink = page.locator(`#${platform}-downloads .download-link[href="${link}"]`)
|
||||||
await expect(downloadLink).toContainText(label)
|
await expect(downloadLink).toContainText(label)
|
||||||
await expect(downloadLink).toHaveAttribute('href', link)
|
await expect(downloadLink).toHaveAttribute("href", link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from "@playwright/test"
|
||||||
|
|
||||||
test('clicking back button navigates to previous page', async ({ page }) => {
|
test("clicking back button navigates to previous page", async ({ page }) => {
|
||||||
await page.goto('/mods?created=asc')
|
await page.goto("/mods?created=asc")
|
||||||
const currentUrl = page.url()
|
const currentUrl = page.url()
|
||||||
const modCards = await page.locator('.mod-card').all()
|
const modCards = await page.locator(".mod-card").all()
|
||||||
await modCards[0].click()
|
await modCards[0].click()
|
||||||
await page.getByRole('button', { name: 'Back' }).click()
|
await page.getByRole("button", { name: "Back" }).click()
|
||||||
await page.waitForURL(currentUrl)
|
await page.waitForURL(currentUrl)
|
||||||
expect(page.url()).toStrictEqual(currentUrl)
|
expect(page.url()).toStrictEqual(currentUrl)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from "@playwright/test"
|
||||||
|
|
||||||
test('all routes do not return 404', async ({ page }) => {
|
test("all routes do not return 404", async ({ page }) => {
|
||||||
const routes = ['/', '/welcome', '/about', '/privacy-policy', '/download', '/donate', '/whatsnew']
|
const routes = ["/", "/welcome", "/about", "/privacy-policy", "/download", "/donate", "/whatsnew"]
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
const response = await page.goto(route)
|
const response = await page.goto(route)
|
||||||
expect(response?.status()).not.toBe(404)
|
expect(response?.status()).not.toBe(404)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { vi } from 'vitest'
|
import { vi } from "vitest"
|
||||||
import translation from '~/i18n/en/translation.json'
|
|
||||||
|
|
||||||
vi.mock('~/utils/i18n', () => ({
|
import translation from "~/i18n/en/translation.json"
|
||||||
getLocale: () => 'en',
|
|
||||||
|
vi.mock("~/utils/i18n", () => ({
|
||||||
|
getLocale: () => "en",
|
||||||
getPath: () => (href: string) => `/en${href}`,
|
getPath: () => (href: string) => `/en${href}`,
|
||||||
getUI: () => translation,
|
getUI: () => translation,
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CONSTANT } from '~/constants'
|
import { CONSTANT } from "~/constants"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the latest release notes from GitHub and parses the SHA-256 checksums.
|
* Fetches the latest release notes from GitHub and parses the SHA-256 checksums.
|
||||||
|
@ -8,11 +8,11 @@ export async function getChecksums() {
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
return CONSTANT.CHECKSUMS
|
return CONSTANT.CHECKSUMS
|
||||||
}
|
}
|
||||||
const res = await fetch('https://api.github.com/repos/zen-browser/desktop/releases/latest', {
|
const res = await fetch("https://api.github.com/repos/zen-browser/desktop/releases/latest", {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/vnd.github+json',
|
Accept: "application/vnd.github+json",
|
||||||
'X-GitHub-Api-Version': '2022-11-28',
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
'User-Agent': 'zen-browser-checksum-fetcher',
|
"User-Agent": "zen-browser-checksum-fetcher",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error(`Failed to fetch GitHub release: ${res.statusText}`)
|
if (!res.ok) throw new Error(`Failed to fetch GitHub release: ${res.statusText}`)
|
||||||
|
@ -23,7 +23,7 @@ export async function getChecksums() {
|
||||||
const match = body.match(/File Checksums \(SHA-256\)[\s\S]*?```([\s\S]*?)```/)
|
const match = body.match(/File Checksums \(SHA-256\)[\s\S]*?```([\s\S]*?)```/)
|
||||||
const checksums: Record<string, string> = {}
|
const checksums: Record<string, string> = {}
|
||||||
if (match?.[1]) {
|
if (match?.[1]) {
|
||||||
for (const line of match[1].split('\n')) {
|
for (const line of match[1].split("\n")) {
|
||||||
const [hash, filename] = line.trim().split(/\s+/, 2)
|
const [hash, filename] = line.trim().split(/\s+/, 2)
|
||||||
if (hash && filename) checksums[filename] = hash
|
if (hash && filename) checksums[filename] = hash
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { GetStaticPaths } from 'astro'
|
import { type GetStaticPaths } from "astro"
|
||||||
import { CONSTANT } from '~/constants'
|
|
||||||
import UI_EN from '~/i18n/en/translation.json'
|
import { CONSTANT } from "~/constants"
|
||||||
|
import UI_EN from "~/i18n/en/translation.json"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the available locales in the application
|
* Represents the available locales in the application
|
||||||
|
@ -15,7 +16,7 @@ export type Locale = (typeof locales)[number]
|
||||||
*/
|
*/
|
||||||
export const getPath = (locale?: Locale) => (path: string) => {
|
export const getPath = (locale?: Locale) => (path: string) => {
|
||||||
if (locale && locale !== CONSTANT.I18N.DEFAULT_LOCALE && !path.startsWith(`/${locale}`)) {
|
if (locale && locale !== CONSTANT.I18N.DEFAULT_LOCALE && !path.startsWith(`/${locale}`)) {
|
||||||
return `/${locale}${path.startsWith('/') ? '' : '/'}${path}`
|
return `/${locale}${path.startsWith("/") ? "" : "/"}${path}`
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -83,10 +86,10 @@ export const getUI = (locale?: Locale | string): UI => {
|
||||||
*/
|
*/
|
||||||
function deepMerge<T extends object>(defaultObj: T, overrideObj: Partial<T>): T {
|
function deepMerge<T extends object>(defaultObj: T, overrideObj: Partial<T>): T {
|
||||||
// Handle non-object cases
|
// Handle non-object cases
|
||||||
if (typeof defaultObj !== 'object' || defaultObj === null) {
|
if (typeof defaultObj !== "object" || defaultObj === null) {
|
||||||
return (overrideObj ?? defaultObj) as T
|
return (overrideObj ?? defaultObj) as T
|
||||||
}
|
}
|
||||||
if (typeof overrideObj !== 'object' || overrideObj === null) {
|
if (typeof overrideObj !== "object" || overrideObj === null) {
|
||||||
return (overrideObj ?? defaultObj) as T
|
return (overrideObj ?? defaultObj) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -102,11 +105,14 @@ export const getUI = (locale?: Locale | string): UI => {
|
||||||
if (
|
if (
|
||||||
defaultValue !== null &&
|
defaultValue !== null &&
|
||||||
overrideValue !== null &&
|
overrideValue !== null &&
|
||||||
typeof defaultValue === 'object' &&
|
typeof defaultValue === "object" &&
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { type ClassValue, clsx } from 'clsx'
|
import { type ClassValue, clsx } from "clsx"
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export const cn = (...inputs: ClassValue[]) => {
|
export const cn = (...inputs: ClassValue[]) => {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"extends": "astro/tsconfigs/strict",
|
"extends": "astro/tsconfigs/strict",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "preact",
|
"jsxImportSource": "react",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue