chore(prettier): update prettier config

This commit is contained in:
Shintaro Jokagi 2025-05-28 13:50:37 +12:00
parent ceef83d609
commit a77c141d35
No known key found for this signature in database
GPG key ID: 0DDF8FA44C9A0DA8
66 changed files with 709 additions and 709 deletions

View file

@ -7,7 +7,7 @@ export default {
tabWidth: 2, tabWidth: 2,
useTabs: false, useTabs: false,
semi: false, semi: false,
singleQuote: false, singleQuote: true,
quoteProps: "as-needed", quoteProps: "as-needed",
trailingComma: "es5", trailingComma: "es5",
bracketSpacing: true, bracketSpacing: true,

View file

@ -1,10 +1,10 @@
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 }, viewport: { once },

View file

@ -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)

View file

@ -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 />

View file

@ -4,14 +4,14 @@ const sizes = [216, 396, 576, 756]
const borderWidths = [20, 30, 40, 50] const borderWidths = [20, 30, 40, 50]
--- ---
<div id="circles" class:list={["pointer-events-none inset-0 overflow-hidden", classList]}> <div id="circles" class:list={['pointer-events-none inset-0 overflow-hidden', classList]}>
<div class="mx-auto opacity-10 lg:opacity-100"> <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`,

View file

@ -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)
@ -38,7 +38,7 @@ const {
</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>

View file

@ -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)
@ -161,17 +161,17 @@ const descriptions = Object.values(features.featureTabs).map(tab => tab.descript
</section> </section>
<script> <script>
const features = document.querySelectorAll(".feature, .feature-tab") as NodeListOf<HTMLElement> const features = document.querySelectorAll('.feature, .feature-tab') as NodeListOf<HTMLElement>
// Set initial description // Set initial description
const descriptionEl = document.querySelector(".feature-description") as HTMLDivElement const descriptionEl = document.querySelector('.feature-description') as HTMLDivElement
const descriptions = descriptionEl?.dataset.descriptions?.split(",") const descriptions = descriptionEl?.dataset.descriptions?.split(',')
if (descriptionEl && descriptions) { if (descriptionEl && descriptions) {
descriptionEl.textContent = descriptions[0] descriptionEl.textContent = descriptions[0]
} }
function changeToFeature({ target }: { target: HTMLElement | undefined | null }) { function changeToFeature({ target }: { target: HTMLElement | undefined | null }) {
target = target?.closest(".feature, .feature-tab") 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
@ -180,19 +180,19 @@ const descriptions = Object.values(features.featureTabs).map(tab => tab.descript
// 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(".feature-video") as NodeListOf<HTMLVideoElement> const videos = document.querySelectorAll('.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
@ -200,14 +200,14 @@ const descriptions = Object.values(features.featureTabs).map(tab => tab.descript
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})`
@ -219,7 +219,7 @@ const descriptions = Object.values(features.featureTabs).map(tab => tab.descript
for (const feature of features) { for (const feature of features) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
feature.addEventListener("click", changeToFeature as any) feature.addEventListener('click', changeToFeature as any)
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -231,7 +231,7 @@ const descriptions = Object.values(features.featureTabs).map(tab => tab.descript
@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;
} }
} }
@ -240,7 +240,7 @@ const descriptions = Object.values(features.featureTabs).map(tab => tab.descript
@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;
} }
} }
@ -279,7 +279,7 @@ const descriptions = Object.values(features.featureTabs).map(tab => tab.descript
display: none; display: none;
object-fit: cover; object-fit: cover;
&[data-active="true"] { &[data-active='true'] {
display: block; display: block;
opacity: 1; opacity: 1;
} }

View file

@ -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)
@ -61,12 +61,12 @@ const {
<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>
@ -83,14 +83,14 @@ const {
<a href="https://docs.zen-browser.app/" class="font-normal">{footer.documentation}</a> <a href="https://docs.zen-browser.app/" class="font-normal">{footer.documentation}</a>
</li> </li>
<li> <li>
<a href={getLocalePath("/mods")} class="font-normal">{footer.zenMods}</a> <a href={getLocalePath('/mods')} class="font-normal">{footer.zenMods}</a>
</li> </li>
<li> <li>
<a href={getLocalePath("/release-notes")} class="font-normal">{footer.releaseNotes}</a <a href={getLocalePath('/release-notes')} class="font-normal">{footer.releaseNotes}</a
> >
</li> </li>
<li> <li>
<a href={getLocalePath("/download?twilight")} class="font-normal">{footer.twilight}</a <a href={getLocalePath('/download?twilight')} class="font-normal">{footer.twilight}</a
> >
</li> </li>
</ul> </ul>
@ -122,7 +122,7 @@ 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">

View file

@ -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() {
@ -63,13 +63,13 @@ const {
</motion.span> </motion.span>
<div class="mt-6 flex w-2/3 flex-col gap-3 sm:gap-6 md:w-fit md:flex-row"> <div class="mt-6 flex w-2/3 flex-col gap-3 sm:gap-6 md:w-fit md:flex-row">
<motion.span client:load {...getHeroTitleAnimation()}> <motion.span client:load {...getHeroTitleAnimation()}>
<Button class="w-full" href={getLocalePath("/download")} isPrimary> <Button class="w-full" href={getLocalePath('/download')} isPrimary>
{hero.buttons.beta} {hero.buttons.beta}
<ArrowRightIcon class="size-4" /> <ArrowRightIcon class="size-4" />
</Button> </Button>
</motion.span> </motion.span>
<motion.span client:load {...getHeroTitleAnimation()}> <motion.span client:load {...getHeroTitleAnimation()}>
<Button href={getLocalePath("/donate")}>{hero.buttons.support}</Button> <Button href={getLocalePath('/donate')}>{hero.buttons.support}</Button>
</motion.span> </motion.span>
</div> </div>
<motion.span <motion.span

View file

@ -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)
@ -46,12 +46,12 @@ 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 href={getLocalePath("/mods")} class="block text-dark hover:text-coral" <a href={getLocalePath('/mods')} class="block text-dark hover:text-coral"
>{menu.zenMods}</a >{menu.zenMods}</a
> >
</li> </li>
<li> <li>
<a href={getLocalePath("/release-notes")} class="block text-dark hover:text-coral" <a href={getLocalePath('/release-notes')} class="block text-dark hover:text-coral"
>{menu.releaseNotes}</a >{menu.releaseNotes}</a
> >
</li> </li>
@ -67,12 +67,12 @@ 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 href={getLocalePath("/donate")} class="block text-dark hover:text-coral" <a href={getLocalePath('/donate')} class="block text-dark hover:text-coral"
>{menu.donate}</a >{menu.donate}</a
> >
</li> </li>
<li> <li>
<a href={getLocalePath("/about")} class="block text-dark hover:text-coral" <a href={getLocalePath('/about')} class="block text-dark hover:text-coral"
>{menu.aboutUs}</a >{menu.aboutUs}</a
> >
</li> </li>
@ -92,12 +92,12 @@ const {
</li> </li>
<!-- Extra Links --> <!-- Extra Links -->
<li> <li>
<a href={getLocalePath("/mods")} class="block font-bold text-dark hover:text-coral" <a href={getLocalePath('/mods')} class="block font-bold text-dark hover:text-coral"
>{menu.mods}</a >{menu.mods}</a
> >
</li> </li>
<li> <li>
<a href={getLocalePath("/download")} class="block font-bold text-dark hover:text-coral" <a href={getLocalePath('/download')} class="block font-bold text-dark hover:text-coral"
>{menu.download}</a >{menu.download}</a
> >
</li> </li>

View file

@ -1,18 +1,18 @@
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, type FormEvent } from "react" import { useEffect, useState, type FormEvent } from 'react'
import { useModsSearch } from "~/hooks/useModsSearch" import { useModsSearch } from '~/hooks/useModsSearch'
import { type ZenTheme } from "~/mods" import { type ZenTheme } from '~/mods'
import { getUI, type Locale } from "~/utils/i18n" import { getUI, type Locale } from '~/utils/i18n'
// Add icons to the library // Add icons to the library
library.add(faSort, faSortUp, faSortDown) library.add(faSort, faSortUp, faSortDown)
// 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' })
type ModsListProps = { type ModsListProps = {
allMods: ZenTheme[] allMods: ZenTheme[]
@ -44,9 +44,9 @@ const 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
} }
@ -90,14 +90,14 @@ const 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"
> >
&lt; &lt;
</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
@ -113,14 +113,14 @@ const ModsList = ({ allMods, locale }: ModsListProps) => {
return ( return (
<span className="text-sm" key={value}> <span className="text-sm" key={value}>
{value {value
.replace("{totalPages}", totalPages.toString()) .replace('{totalPages}', totalPages.toString())
.replace("{totalItems}", totalItems.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"
> >

View file

@ -1,14 +1,14 @@
--- ---
import { Astronav, Dropdown, DropdownItems, MenuItems } from "astro-navbar" import { Astronav, Dropdown, DropdownItems, MenuItems } from 'astro-navbar'
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 Logo from './Logo.astro'
import MobileMenu from "./MobileMenu.astro" import MobileMenu from './MobileMenu.astro'
import ThemeSwitch from "./ThemeSwitch.astro" import ThemeSwitch from './ThemeSwitch.astro'
const locale = getLocale(Astro) const locale = getLocale(Astro)
const getLocalePath = getPath(locale) const getLocalePath = getPath(locale)
@ -24,7 +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 class="flex items-center gap-2 text-lg font-bold" href={getLocalePath("/")}> <a class="flex items-center gap-2 text-lg font-bold" href={getLocalePath('/')}>
<Logo class="text-coral" /> <Logo class="text-coral" />
<span>{brand}</span> <span>{brand}</span>
</a> </a>
@ -40,7 +40,7 @@ const {
</button> </button>
<DropdownItems> <DropdownItems>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<a class="dropdown-item bg-dark/5 row-span-2" href={getLocalePath("/mods")}> <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}
@ -50,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,13 +74,13 @@ const {
</button> </button>
<DropdownItems> <DropdownItems>
<div class="navbar-dropdown !grid-cols-1 gap-1"> <div class="navbar-dropdown !grid-cols-1 gap-1">
<a class="dropdown-item" href={getLocalePath("/donate")}> <a class="dropdown-item" href={getLocalePath('/donate')}>
<div class="dropdown-title">{menu.donate}</div> <div class="dropdown-title">{menu.donate}</div>
<div class="dropdown-description"> <div class="dropdown-description">
{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}
@ -101,7 +101,7 @@ const {
</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>

View file

@ -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,7 +20,7 @@ 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}`))
} }
@ -29,7 +29,7 @@ const currentReleaseIndex = releaseNotesData.findIndex(
(releaseNote: ReleaseNote) => releaseNote.version === props.version (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}`
} }
@ -47,15 +47,15 @@ const generateItems = (items: any, type: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // 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
? { ? {
@ -67,36 +67,36 @@ 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
@ -108,7 +108,7 @@ generateItems(props.knownIssues, "known")
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>
@ -121,11 +121,11 @@ generateItems(props.knownIssues, "known")
{ {
isTwilight ? ( isTwilight ? (
<> <>
{releaseNoteItem.twilightChanges}{" "} {releaseNoteItem.twilightChanges}{' '}
{props.version.replaceAll("{version}", props.version)} {props.version.replaceAll('{version}', props.version)}
</> </>
) : ( ) : (
<>{releaseNoteItem.releaseChanges.replaceAll("{version}", props.version)}</> <>{releaseNoteItem.releaseChanges.replaceAll('{version}', props.version)}</>
) )
} }
{ {
@ -139,7 +139,7 @@ generateItems(props.knownIssues, "known")
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{releaseNoteItem.firefoxVersion.replace("{version}", ffVersion)} {releaseNoteItem.firefoxVersion.replace('{version}', ffVersion)}
</a> </a>
</> </>
) : null ) : null
@ -149,7 +149,7 @@ generateItems(props.knownIssues, "known")
rel="noopener noreferrer" rel="noopener noreferrer"
class="zen-link whitespace-nowrap text-xs !no-underline 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
> >
{ {
@ -168,7 +168,7 @@ 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
@ -184,13 +184,13 @@ generateItems(props.knownIssues, "known")
} }
</div> </div>
<div class="text-xs font-bold opacity-80"> <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
} }

View file

@ -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,13 +22,13 @@ 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]') ||
"", '',
"min-w-16 font-bold opacity-80", 'min-w-16 font-bold opacity-80',
]} ]}
> >
{itemType[type]} {itemType[type]}

View file

@ -1,21 +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 { import {
faBluesky, faBluesky,
faGithub, faGithub,
faMastodon, faMastodon,
faReddit, faReddit,
faXTwitter, faXTwitter,
} from "@fortawesome/free-brands-svg-icons" } 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}`}>

View file

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

View file

@ -4,14 +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={["inline-flex h-8 w-8 cursor-pointer items-center justify-center", className]} class:list={['inline-flex h-8 w-8 cursor-pointer items-center justify-center', className]}
id="theme-switcher" 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
@ -26,24 +26,24 @@ const { label, className = "" } = Astro.props
</button> </button>
<script> <script>
const themeSwitch = document.getElementById("theme-switcher") as HTMLButtonElement const themeSwitch = document.getElementById('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>

View file

@ -1,10 +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("mb-[0.4rem] font-junicode text-5xl font-semibold leading-[0.9] text-dark", className)} class={cn('mb-[0.4rem] font-junicode text-5xl font-semibold leading-[0.9] text-dark', className)}
> >
<slot /> <slot />
</h1> </h1>

View file

@ -1,22 +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'
--- ---
{/* eslint-disable-next-line jsx-a11y/media-has-caption */} {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video class:list={["w-fit", className]} data-src={src} preload="none" {...rest}> <video class:list={['w-fit', className]} data-src={src} preload="none" {...rest}>
<source src="" type={`video/${type}`} /> <source src="" type={`video/${type}`} />
</video> </video>
<script> <script>
const videos = document.querySelectorAll("video[data-src]") as NodeListOf<HTMLVideoElement> const videos = document.querySelectorAll('video[data-src]') as NodeListOf<HTMLVideoElement>
const loadVideo = (video: HTMLVideoElement) => { const 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()
} }
} }

View file

@ -89,12 +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(".copy-btn") as NodeListOf<HTMLButtonElement> const copyButtons = document.querySelectorAll('.copy-btn') as NodeListOf<HTMLButtonElement>
function stopEvent(e: Event) { function stopEvent(e: Event) {
e.preventDefault?.() e.preventDefault?.()
@ -107,25 +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.closest('.checksum-tooltip')?.querySelector('.font-mono') as HTMLSpanElement)
?.innerText ?.innerText
) )
) )
btn.addEventListener("mousedown", stopEvent) btn.addEventListener('mousedown', stopEvent)
}) })
</script> </script>

View file

@ -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,40 +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("/latest/download/", "/download/twilight/") const twilightHref = href.replace('/latest/download/', '/download/twilight/')
link.setAttribute("href", twilightHref) link.setAttribute('href', twilightHref)
} }
} }
} }

View file

@ -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,19 +30,19 @@ 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
} }
--- ---
<div <div
id={`${platform}-downloads`} id={`${platform}-downloads`}
data-active={platform === "mac"} data-active={platform === 'mac'}
class="platform-section data-[active='false']:hidden" class="platform-section data-[active='false']:hidden"
> >
<div class="items-center gap-8 md:flex"> <div class="items-center gap-8 md:flex">
@ -56,7 +56,7 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
<p class="text-muted-foreground mb-6" set:html={description} /> <p class="text-muted-foreground mb-6" set:html={description} />
<div class="space-y-6"> <div class="space-y-6">
{ {
platform === "linux" ? ( platform === 'linux' ? (
<> <>
{releases.flathub && releases.flathub.all.label && ( {releases.flathub && releases.flathub.all.label && (
<article class="flathub-download data-[twilight='true']:hidden"> <article class="flathub-download data-[twilight='true']:hidden">
@ -71,16 +71,16 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
</article> </article>
)} )}
{releases.x86_64 && {releases.x86_64 &&
typeof releases.x86_64 === "object" && typeof releases.x86_64 === 'object' &&
"tarball" in releases.x86_64 && 'tarball' in releases.x86_64 &&
releases.x86_64.tarball && ( releases.x86_64.tarball && (
<article> <article>
<h4 class="mb-3 text-lg font-medium">x86_64</h4> <h4 class="mb-3 text-lg font-medium">x86_64</h4>
<div class=""> <div class="">
{releases.x86_64.tarball && ( {releases.x86_64.tarball && (
<DownloadCard <DownloadCard
label={releases.x86_64.tarball.label ? releases.x86_64.tarball.label : ""} label={releases.x86_64.tarball.label ? releases.x86_64.tarball.label : ''}
href={releases.x86_64.tarball.link ? releases.x86_64.tarball.link : ""} 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}
/> />
@ -89,8 +89,8 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
</article> </article>
)} )}
{releases.aarch64 && {releases.aarch64 &&
typeof releases.aarch64 === "object" && typeof releases.aarch64 === 'object' &&
"tarball" in releases.aarch64 && 'tarball' in releases.aarch64 &&
releases.aarch64.tarball && ( releases.aarch64.tarball && (
<article> <article>
<h4 class="mb-3 text-lg font-medium">ARM64</h4> <h4 class="mb-3 text-lg font-medium">ARM64</h4>
@ -98,9 +98,9 @@ function isFlatReleaseInfo(obj: unknown): obj is ReleaseInfo {
{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}
/> />

View file

@ -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',
}, },
}, },
}, },

View file

@ -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',
} }

View file

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

View file

@ -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,

View file

@ -1,8 +1,8 @@
import { useEffect, useState } from "react" import { useEffect, useState } from 'react'
import { type ZenTheme } from "../mods" import { type ZenTheme } from '../mods'
type SortOrder = "default" | "asc" | "desc" type SortOrder = 'default' | 'asc' | 'desc'
type ModsSearchState = { type ModsSearchState = {
search: string search: string
@ -17,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,
}) })
@ -29,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),
}) })
}, []) }, [])
@ -42,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])
@ -96,18 +96,18 @@ export function useModsSearch(mods: ZenTheme[]) {
} }
// 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
}) })
} }
@ -128,7 +128,7 @@ export function useModsSearch(mods: ZenTheme[]) {
setState(prev => ({ setState(prev => ({
...prev, ...prev,
createdSort: createdSort:
prev.createdSort === "default" ? "asc" : prev.createdSort === "asc" ? "desc" : "default", prev.createdSort === 'default' ? 'asc' : prev.createdSort === 'asc' ? 'desc' : 'default',
page: 1, // Reset page when sort changes page: 1, // Reset page when sort changes
})) }))
} }
@ -137,7 +137,7 @@ export function useModsSearch(mods: ZenTheme[]) {
setState(prev => ({ setState(prev => ({
...prev, ...prev,
updatedSort: updatedSort:
prev.updatedSort === "default" ? "asc" : prev.updatedSort === "asc" ? "desc" : "default", prev.updatedSort === 'default' ? 'asc' : prev.updatedSort === 'asc' ? 'desc' : 'default',
page: 1, // Reset page when sort changes page: 1, // Reset page when sort changes
})) }))
} }

View file

@ -12,6 +12,6 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-arrow-left-icon lucide-arrow-left", className]} 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 {...props}><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg
> >

View file

@ -12,6 +12,6 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-arrow-right-icon lucide-arrow-right", className]} 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 {...props}><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg
> >

View file

@ -12,6 +12,6 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-arrow-up-icon lucide-arrow-up", className]} 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 {...props}><path d="m5 12 7-7 7 7"></path><path d="M12 19V5"></path></svg
> >

View file

@ -12,6 +12,6 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-check-icon lucide-check", className]} class:list={['lucide lucide-check-icon lucide-check', className]}
{...props}><path d="M20 6 9 17l-5-5"></path></svg {...props}><path d="M20 6 9 17l-5-5"></path></svg
> >

View file

@ -12,6 +12,6 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-chevron-down-icon lucide-chevron-down", className]} class:list={['lucide lucide-chevron-down-icon lucide-chevron-down', className]}
{...props}><path d="m6 9 6 6 6-6"></path></svg {...props}><path d="m6 9 6 6 6-6"></path></svg
> >

View file

@ -12,7 +12,7 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-download-icon lucide-download", className]} class:list={['lucide lucide-download-icon lucide-download', className]}
{...props} {...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" ><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 ></polyline><line x1="12" x2="12" y1="15" y2="3"></line></svg

View file

@ -12,7 +12,7 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-external-link-icon lucide-external-link", className]} class:list={['lucide lucide-external-link-icon lucide-external-link', className]}
{...props} {...props}
><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path ><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 d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg

View file

@ -12,7 +12,7 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-github-icon lucide-github", className]} class:list={['lucide lucide-github-icon lucide-github', className]}
{...props} {...props}
><path ><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" 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"

View file

@ -12,7 +12,7 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-info-icon lucide-info", className]} class:list={['lucide lucide-info-icon lucide-info', className]}
{...props} {...props}
><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01" ><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"
></path></svg ></path></svg

View file

@ -12,7 +12,7 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-lock-icon lucide-lock", className]} class:list={['lucide lucide-lock-icon lucide-lock', className]}
{...props} {...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" ><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 ></path></svg

View file

@ -12,6 +12,6 @@ const { class: className, ...props } = Astro.props
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class:list={["lucide lucide-menu-icon lucide-menu", className]} 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 {...props}><path d="M4 12h16"></path><path d="M4 18h16"></path><path d="M4 6h16"></path></svg
> >

View file

@ -9,32 +9,32 @@ 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>
@ -55,10 +55,10 @@ const locale = getLocale(Astro)
is:inline is:inline
type="application/ld+json" type="application/ld+json"
set:html={JSON.stringify({ set:html={JSON.stringify({
"@context": "https://schema.org", '@context': 'https://schema.org',
"@type": "WebSite", '@type': 'WebSite',
name: "Zen Browser", name: 'Zen Browser',
url: "https://zen-browser.app/", url: 'https://zen-browser.app/',
})} })}
/> />
</> </>
@ -91,7 +91,7 @@ const locale = getLocale(Astro)
<script> <script>
// eslint-disable-next-line no-console // 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;'
) )
@ -105,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;
} }
@ -128,7 +128,7 @@ const locale = getLocale(Astro)
--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);
@ -142,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;
} }

View file

@ -1,4 +1,4 @@
import { format } from "date-fns" import { format } from 'date-fns'
export type ZenTheme = { export type ZenTheme = {
name: string name: string
@ -17,7 +17,7 @@ export type 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 {
@ -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')
} }

View file

@ -1,5 +1,5 @@
--- ---
import NotFound from "./[...locale]/404.astro" import NotFound from './[...locale]/404.astro'
--- ---
<NotFound /> <NotFound />

View file

@ -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)
@ -25,7 +25,7 @@ const {
<p class="max-w-xl text-lg text-gray-500 dark:text-gray-400"> <p class="max-w-xl text-lg text-gray-500 dark:text-gray-400">
{notFound.description} {notFound.description}
</p> </p>
<Button href={getLocalePath("/")} isPrimary class="w-fit"> <Button href={getLocalePath('/')} isPrimary class="w-fit">
{notFound.button} {notFound.button}
</Button> </Button>
</div> </div>

View file

@ -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)
@ -38,7 +38,7 @@ const {
<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>

View file

@ -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 {

View file

@ -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)

View file

@ -1,8 +1,8 @@
import rss, { type RSSOptions } from "@astrojs/rss" import rss, { type RSSOptions } from '@astrojs/rss'
import { releaseNotes, 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
@ -17,8 +17,8 @@ export function GET(context: { url: URL }) {
releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date as string) : new Date() 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: `
@ -55,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])
@ -78,43 +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( content += addReleaseNoteSection(
"⚠️ Breaking changes", '⚠️ Breaking changes',
releaseNote.breakingChanges?.map(breakingChangeToReleaseNote) releaseNote.breakingChanges?.map(breakingChangeToReleaseNote)
) )
content += addReleaseNoteSection("✓ Fixes", releaseNote.fixes?.map(fixToReleaseNote)) content += addReleaseNoteSection('✓ Fixes', releaseNote.fixes?.map(fixToReleaseNote))
content += addReleaseNoteSection("🖌 Theme Changes", releaseNote.themeChanges) content += addReleaseNoteSection('🖌 Theme Changes', releaseNote.themeChanges)
content += addReleaseNoteSection("⭐ Features", releaseNote.features) content += addReleaseNoteSection('⭐ Features', releaseNote.features)
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
@ -125,14 +125,14 @@ function fixToReleaseNote(fix?: Exclude<ReleaseNote["fixes"], undefined>[number]
} }
function breakingChangeToReleaseNote( function breakingChangeToReleaseNote(
breakingChange?: Exclude<ReleaseNote["breakingChanges"], undefined>[number] breakingChange?: Exclude<ReleaseNote['breakingChanges'], undefined>[number]
) { ) {
if (typeof breakingChange === "string") { 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>)`
@ -141,10 +141,10 @@ function breakingChangeToReleaseNote(
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(' ')
} }

View file

@ -1,11 +1,11 @@
--- ---
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)

View file

@ -1,13 +1,13 @@
--- ---
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()
@ -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,14 +93,14 @@ 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 set:html={slug.latestUpdate.replace("{updatedAt}", dates.updatedAt)} /> <p set:html={slug.latestUpdate.replace('{updatedAt}', dates.updatedAt)} />
) )
} }
{ {
@ -112,13 +112,13 @@ const {
} }
</div> </div>
<div class="flex flex-col sm:items-end"> <div class="flex flex-col sm:items-end">
<Button class="hidden" id="install-theme" extra={{ "zen-theme-id": mod.id }} isPrimary> <Button class="hidden" id="install-theme" extra={{ 'zen-theme-id': mod.id }} isPrimary>
{slug.installMod} {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}

View file

@ -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)

View file

@ -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)

View file

@ -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)
@ -20,7 +20,7 @@ export async function getStaticPaths() {
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>

View file

@ -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)
@ -26,7 +26,7 @@ const {
<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
)} )}
/> />
@ -75,39 +75,39 @@ const {
</ModalBody> </ModalBody>
</Modal> </Modal>
<script> <script>
import { closeModal, openModal } 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.getElementById(version)?.getElementsByTagName("details") const versionDetails = document.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', '')
}) })
} }
@ -119,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>

View file

@ -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)

View file

@ -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,24 +22,24 @@ 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 title={layout.whatsNew.title.replace("{latestVersion.version}", latestVersion.version)}> <Layout 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 href="https://github.com/zen-browser/desktop/issues/new/choose" target="_blank"> <a href="https://github.com/zen-browser/desktop/issues/new/choose" target="_blank">

View file

@ -1,4 +1,4 @@
import releaseNotesStable from "./release-notes/stable.json" import releaseNotesStable from './release-notes/stable.json'
type FixWithIssue = { type FixWithIssue = {
description: string description: string
@ -25,12 +25,12 @@ export type 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) {

View file

@ -1,101 +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" import Button from '~/components/Button.astro'
describe("<Button />", () => { 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"')
}) })

View file

@ -1,48 +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" import ButtonCard from '~/components/download/ButtonCard.astro'
describe("<ButtonCard />", () => { 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')
}) })
}) })

View file

@ -1,126 +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" import PlatformDownload from '~/components/download/PlatformDownload.astro'
const mockIcon = ["<svg></svg>"] 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')
}) })
}) })

View file

@ -1,23 +1,23 @@
import { describe, expect, it } from "vitest" import { describe, expect, it } from 'vitest'
import { getReleasesWithChecksums } from "~/components/download/release-data" import { getReleasesWithChecksums } from '~/components/download/release-data'
describe("getReleasesWithChecksums", () => { describe('getReleasesWithChecksums', () => {
it("returns correct structure with checksums", () => { 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')
}) })
}) })

View file

@ -1,7 +1,7 @@
import { expect, test, type BrowserContextOptions, type Page } from "@playwright/test" import { expect, test, type BrowserContextOptions, type 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) =>
@ -17,68 +17,68 @@ const _ = (page: Page, platform: string, label: string) =>
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: userAgent:
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
platform: "Linux x86_64", platform: 'Linux x86_64',
}, },
] ]
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( await expect(getPlatformButton(page, otherPlatform)).not.toHaveAttribute(
"data-active", 'data-active',
"true" '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>) {
@ -93,11 +93,11 @@ test.describe("Download page download links", () => {
} }
} }
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[ for (const { label, link } of platformLinkSelectors[
@ -105,7 +105,7 @@ test.describe("Download page download links", () => {
]) { ]) {
const downloadLink = page.locator(`#${platform}-downloads .download-link[href="${link}"]`) const downloadLink = page.locator(`#${platform}-downloads .download-link[href="${link}"]`)
await expect(downloadLink).toContainText(label) await expect(downloadLink).toContainText(label)
await expect(downloadLink).toHaveAttribute("href", link) await expect(downloadLink).toHaveAttribute('href', link)
} }
} }
}) })

View file

@ -1,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)
}) })

View file

@ -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)

View file

@ -1,9 +1,9 @@
import { vi } from "vitest" import { vi } from 'vitest'
import translation from "~/i18n/en/translation.json" import translation from '~/i18n/en/translation.json'
vi.mock("~/utils/i18n", () => ({ vi.mock('~/utils/i18n', () => ({
getLocale: () => "en", getLocale: () => 'en',
getPath: () => (href: string) => `/en${href}`, getPath: () => (href: string) => `/en${href}`,
getUI: () => translation, getUI: () => translation,
})) }))

View file

@ -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
} }

View file

@ -1,7 +1,7 @@
import { type GetStaticPaths } from "astro" import { type GetStaticPaths } from 'astro'
import { CONSTANT } from "~/constants" import { CONSTANT } from '~/constants'
import UI_EN from "~/i18n/en/translation.json" import UI_EN from '~/i18n/en/translation.json'
/** /**
* Represents the available locales in the application * Represents the available locales in the application
@ -16,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
} }
@ -86,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
} }
@ -105,8 +105,8 @@ 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( ;(result as Record<keyof T, unknown>)[key] = deepMerge(

View file

@ -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))