Redesigned the website!

This commit is contained in:
Mauro Balades
2024-08-01 20:15:50 +02:00
parent c6a979d585
commit ac79dea1f7
12 changed files with 1161 additions and 295 deletions

10
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@vercel/postgres": "^0.9.0",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cobe": "^0.6.3",
@@ -5465,6 +5466,15 @@
}
]
},
"node_modules/canvas-confetti": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz",
"integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==",
"funding": {
"type": "donate",
"url": "https://www.paypal.me/kirilvatev"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",

View File

@@ -21,6 +21,7 @@
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@vercel/postgres": "^0.9.0",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cobe": "^0.6.3",

View File

@@ -1,9 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
@@ -33,10 +33,23 @@
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
/* Custom properties */
--navigation-height: 3.5rem;
--color-one: #ffbd7a;
--color-two: #fe8bbb;
--color-three: #9e7aff;
/*
--color-one: #37ecba;
--color-two: #72afd3;
--color-three: #ff2e63;
*/
}
.dark {
--background: 0 0% 0%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
@@ -63,14 +76,15 @@
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
}
--color-one: #6aa8e2;
}
}
@layer base {
* {
@layer base {
* {
@apply border-border;
}
body {
}
body {
@apply bg-background text-foreground;
}
}
}
}

View File

@@ -1,15 +1,15 @@
"use client";
import { ny } from "@/lib/utils";
import GridPattern from "./ui/grid-pattern";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "./ui/select";
import { Button } from "./ui/button";
import { Form, FormField, FormItem, FormLabel } from "./ui/form";
import { useForm } from "react-hook-form";
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'
import { releases } from "@/lib/releases";
import { addDownload } from "@/lib/db";
import { useState } from "react";
import styled, { keyframes } from "styled-components";
import { ny } from "@/lib/utils";
import { Checkbox } from "./ui/checkbox";
import { ChevronLeft } from "lucide-react";
import { Button } from "./ui/button";
import Particles from "./ui/particles";
import confetti from 'canvas-confetti';
import { releases, releaseTree } from "@/lib/releases";
const BASE_URL = "https://github.com/zen-browser/desktop/releases/latest/download";
@@ -19,12 +19,10 @@ function getDefaultPlatformBasedOnUserAgent() {
userAgent = window.navigator.userAgent;
}
if (userAgent.includes("Win")) {
return "WindowsInstaller";
return "Windows";
}
if (userAgent.includes("Mac")) {
// TODO:
// return "MacOS";
return "";
return "MacOS";
}
if (userAgent.includes("Linux")) {
return "Linux";
@@ -32,110 +30,350 @@ function getDefaultPlatformBasedOnUserAgent() {
return "";
}
const formSchema = z.object({
platform: z.string().nonempty(),
});
const field_enter = keyframes`
0% {
opacity: 0;
transform: scale(0.9);
filter: blur(10px);
}
1% {
max-height: 100%;
}
100% {
opacity: 1;
transform: scale(1);
filter: blur(0);
}
`;
const field_exit = keyframes`
from {
display: flex;
opacity: 1;
transform: scale(1);
filter: blur(0);
}
99% {
opacity: 0;
transform: scale(0.9);
filter: blur(10px);
}
100% {
display: none;
}
`;
const FormField = styled.div<{ enter: boolean, out: boolean }>`
overflow: hidden;
max-height: 0;
flex-direction: column;
margin-top: 3rem;
opacity: 0;
width: 100%;
animation: 0.2s ease-in-out forwards ${({ enter, out }) => enter ? field_enter : out ? field_exit : ""} !important;
animation-delay: ${({ enter }) => enter ? "0.4s" : "0s"};
`;
const FieldTitle = styled.div`
font-size: 1.35rem;
font-weight: 500;
`;
const FieldDescription = styled.div`
font-size: 1rem;
color: #666;
margin-bottom: 1rem;
`;
export default function DownloadPage() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
platform: getDefaultPlatformBasedOnUserAgent(),
},
});
const watchRelease = form.watch("platform");
const [platform, setPlatform] = useState<string | null>(null);
const [architecture, setArchitecture] = useState<string | null>(null);
const [windowsDownloadType, setWindowsDownloadType] = useState<string | null>(null);
const [linuxDownloadType, setLinuxDownloadType] = useState<string | null>(null);
const onSubmit = async (data: any) => {
const platform = data.platform;
addDownload(platform);
console.log("Data: ", data)
console.log("Platform: ", platform)
console.log("Releases: ", releases)
const releasesForPlatform = releases[platform];
console.log("Releases for platform: ", releasesForPlatform)
const url = `${BASE_URL}/${releasesForPlatform}`;
console.log("URL: ", url)
window.open(url, "_blank");
const [selectedPlatform, setSelectedPlatform] = useState(getDefaultPlatformBasedOnUserAgent());
const [selectedArchitecture, setSelectedArchitecture] = useState("64-bit");
const [selectedWindowsDownloadType, setSelectedWindowsDownloadType] = useState("installer");
const [selectedLinuxDownloadType, setSelectedLinuxDownloadType] = useState("portable");
const [hasDownloaded, setHasDownloaded] = useState(false);
const [flowIndex, setFlowIndex] = useState(0);
const throwConfetti = () => {
const end = Date.now() + 3 * 1000 // 3 seconds
const colors = ['#a786ff', '#fd8bbc', '#eca184', '#f8deb1']
const frame = () => {
if (Date.now() > end)
return
confetti({
particleCount: 2,
angle: 60,
spread: 55,
startVelocity: 60,
origin: { x: 0, y: 0.5 },
colors,
})
confetti({
particleCount: 2,
angle: 120,
spread: 55,
startVelocity: 60,
origin: { x: 1, y: 0.5 },
colors,
})
requestAnimationFrame(frame)
}
frame()
}
const startDownload = () => {
const platform = releaseTree[selectedPlatform.toLowerCase()];
let arch: string;
if (selectedArchitecture === "64-bit") {
arch = "x64";
} else if (selectedArchitecture === "aarch64") {
arch = "arm";
} else {
arch = "x32";
}
let releaseTarget: string;
if (selectedPlatform === "MacOS") {
releaseTarget = platform[arch];
} else {
releaseTarget = platform[arch][selectedPlatform === "Windows"
? windowsDownloadType as string
: linuxDownloadType as string];
}
console.log("Downloading: ");
console.log("platform: ", selectedPlatform);
console.log("arch: ", arch);
setHasDownloaded(true);
addDownload(releaseTarget);
//window.location.replace(`${BASE_URL}/${releases[releaseTarget]}`);
throwConfetti();
};
const continueFlow = () => {
if (flowIndex === 0)
setPlatform(selectedPlatform);
if (flowIndex === 1)
setArchitecture(selectedArchitecture);
if (flowIndex === 2 || (flowIndex === 1 && platform === "MacOS")) {
setWindowsDownloadType(selectedWindowsDownloadType);
setLinuxDownloadType(selectedLinuxDownloadType);
startDownload();
}
setFlowIndex(flowIndex + 1);
};
const goBackFlow = () => {
if (flowIndex === 1) {
setPlatform(null);
} else if (flowIndex === 2) {
setArchitecture(null);
} else if (flowIndex === 3) {
setWindowsDownloadType(null);
setSelectedWindowsDownloadType("installer");
setLinuxDownloadType(null);
setSelectedLinuxDownloadType("portable");
}
if (flowIndex > 0)
setFlowIndex(flowIndex - 1);
}
const changeToFlatpak = () => {
if (selectedArchitecture === "64-bit") {
setSelectedLinuxDownloadType("flatpak");
}
}
return (
<div className="w-full relative h-screen flex items-center justify-center flex-col lg:flex-row">
<div className="w-full lg:w-1/2 relative h-full px-12 lg:px-24 xl:px-32 2xl:px-64 text-center flex items-cetner justify-center flex-col">
<GridPattern
numSquares={30}
maxOpacity={0.5}
height={50}
width={50}
duration={3}
repeatDelay={1}
x={-1}
y={-1}
strokeDasharray="4 2"
className={ny(
'[mask-image:radial-gradient(350px_circle_at_center,white,transparent)]',
'w-full z-0',
<>
<link rel="stylesheet" type='text/css' href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css" />
<link rel="stylesheet" type='text/css' href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css" />
<link rel="stylesheet" type='text/css' href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css" />
<div className="w-full overflow-hidden relative h-screen flex items-center justify-center flex-col lg:flex-row">
<div className="flex flex-col justify-center w-full p-10 md:p-0 md:p-20 lg:p-0 lg:w-1/2 2xl:w-1/3 mx-auto">
{hasDownloaded && (
<div className="flex items-center justify-center flex-col">
<h1 className="text-6xl font-bold">Downloaded! </h1>
<p className="text-muted-foreground mt-3">Zen Browser has been downloaded successfully. Enjoy browsing the web with Zen!</p>
<div className="flex font-bold mt-5 items-center justify-between mx-auto">
<a href="https://github.com/zen-browser">Source Code</a>
<a className="ml-5" href="https://patreon.com/zen_browser?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink">Donate</a>
<a className="ml-5" href="/release-notes/latest">Release Notes</a>
</div>
{selectedPlatform === "MacOS" && (
<div className="mt-12 flex flex-col items-start border justify-between rounded-md bg-background p-5">
<h3 className="text-xl font-semibold">Installation Instructions</h3>
<p className="text-muted-foreground text-sm ">To install Zen on MacOS, the process is a bit different. Please follow the instructions below:</p>
<Button className="mt-5" onClick={() => window.location.href = "https://github.com/zen-browser/desktop/issues/53"}>Download Zen for MacOS</Button>
</div>
)}
</div>
) || (
<>
<h1 className="text-6xl font-bold">Download Zen</h1>
<p className="text-muted-foreground mt-3">We are so excited for you to try Zen Browser. But first, we need to know what kind of device you are using. It will be fast, we promise.</p>
</>
)}
/>
<h1 className="!text-4xl font-bold tracking-[-0.02em] text-black dark:text-white md:text-7xl md:leading-[5rem]">
Download Zen Browser
</h1>
<p className="!text-md text-muted-foreground !font-medium">
Get started with Zen Browser today. Get back to browsing the web with peace of mind.
</p>
</div>
<div className="w-full lg:w-1/2 relative flex flex-col relative items-cetner justify-start">
<div className="w-full lg:w-2/3 relative flex flex-col items-center mx-auto mt-10 lg:mt-0">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
<FormField
control={form.control}
name="platform"
render={({ field }) => (
<FormItem>
<FormLabel>Select your operating system</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger className="w-full mb-5">
<SelectValue placeholder="Operating System" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Operating System</SelectLabel>
<SelectItem value="WindowsInstaller">Windows Installer</SelectItem>
<SelectItem value="WindowsZip">Windows (Zip)</SelectItem>
<SelectItem value="MacOS">MacOS</SelectItem>
<SelectItem value="Linux">Linux</SelectItem>
<SelectItem value="WindowsStubInstaller" disabled>Windows Pretty Installer</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
<Button type="submit">Download Zen 🎉</Button>
{watchRelease === "Linux" && (
<div className="mt-20 rounded border bg-muted p-4 text-muted-foreground">
<p>
<strong>Linux user?</strong><br/>
We{"'"}ve recently added support for flatpak! You can download the flatpak version by running the following command:
<pre className="bg-white dark:bg-black p-2 rounded mt-4 overflow-auto">flatpak install flathub io.github.zen_browser.zen</pre>
</p>
<div className="relative w-full">
{platform === null && (
<FormField
enter={platform === null}
out={platform !== null}
>
<FieldTitle>Platform</FieldTitle>
<FieldDescription>Choose the platform you want to download Zen for.</FieldDescription>
<div onClick={() => setSelectedPlatform("Windows")} className={ny("select-none mb-2 px-4 py-3 flex items-center rounded-lg bg-background cursor-pointer border", selectedPlatform === "Windows" ? "border-blue-400" : "")}>
<Checkbox checked={selectedPlatform === "Windows"} />
<i className="devicon-windows8-original ml-3 p-2 border border-blue-400 rounded-lg"></i>
<div className="ml-2">Windows</div>
</div>
)}
{watchRelease === "MacOS" && (
<div className="mt-20 rounded border bg-muted p-4 text-muted-foreground">
<p>
<strong>Mac user?</strong><br/>
Setting up the MacOS version of Zen Browser is a bit more involved. We{"'"}re working on making this easier, but for now you can follow these steps:
<a href="https://github.com/zen-browser/desktop/issues/53" target="_blank" rel="noreferrer">
<Button className="mt-5" type="button">View MacOS instructions</Button>
</a>
</p>
<div onClick={() => setSelectedPlatform("Linux")} className={ny("select-none mb-2 px-4 py-3 flex items-center rounded-lg bg-background cursor-pointer border", selectedPlatform === "Linux" ? "border-yellow-400" : "")}>
<Checkbox checked={selectedPlatform === "Linux"} />
<i className="devicon-linux-plain ml-3 p-2 border border-yellow-400 rounded-lg"></i>
<div className="ml-2">Linux</div>
</div>
)}
</form>
</Form>
<div onClick={() => setSelectedPlatform("MacOS")} className={ny("select-none mb-2 px-4 py-3 flex items-center rounded-lg bg-background cursor-pointer border", selectedPlatform === "MacOS" ? "border-purple-400" : "")}>
<Checkbox checked={selectedPlatform === "MacOS"} />
<i className="devicon-apple-original p-2 border border-purple-400 ml-3 rounded-lg"></i>
<div className="ml-2 font-bold">MacOS</div>
</div>
</FormField>
)}
{/* Architecture */}
{((platform === "Windows" || platform === "Linux") && flowIndex === 1) && (
<FormField
enter={platform === "Windows" || platform === "Linux" && flowIndex === 1}
out={platform !== "Windows" && platform !== "Linux" && flowIndex >= 1}
>
<FieldTitle>Select Architecture</FieldTitle>
<FieldDescription>Choose the architecture of your device, either 32-bit or 64-bit.</FieldDescription>
<div className="flex items-center justify-center">
<div onClick={() => setSelectedArchitecture("64-bit")} className={ny("select-none w-full h-full mb-2 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedArchitecture === "64-bit" ? "border-blue-400" : "")}>
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">🚀</h1>
<h1 className="text-2xl font-semibold my-2">64 Bits</h1>
<p className="text-muted-foreground mx-auto text-center">Blazing fast and compatible with modern devices</p>
</div>
<div onClick={() => setSelectedArchitecture("32-bit")} className={ny("select-none w-full h-full mb-2 ml-10 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedArchitecture === "32-bit" ? "border-blue-400" : "")}>
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">👴</h1>
<h1 className="text-2xl font-semibold my-2">32 Bits</h1>
<p className="text-muted-foreground mx-auto text-center">Slow but compatible with older devices.</p>
</div>
</div>
</FormField>
)}
{(platform === "MacOS" && flowIndex === 1) && (
<FormField
enter={platform === "MacOS"}
out={platform !== "MacOS"}
>
<FieldTitle>Download Zen for MacOS</FieldTitle>
<FieldDescription>Click the button below to download Zen for MacOS.</FieldDescription>
<div className="flex items-center justify-center">
<div onClick={() => setSelectedArchitecture("aarch64")} className={ny("select-none w-full h-full mb-2 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedArchitecture === "aarch64" ? "border-blue-400" : "")}>
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">🍏</h1>
<h1 className="text-2xl font-semibold my-2">aarch64</h1>
<p className="text-muted-foreground mx-auto text-center">64-bit ARM architecture, for Apple's M1 or M2 chips</p>
</div>
<div onClick={() => setSelectedArchitecture("64-bit")} className={ny("select-none w-full h-full mb-2 ml-10 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedArchitecture === "64-bit" ? "border-blue-400" : "")}>
<h1 className="text-5xl font-bold my-2 opacity-40 dark:opacity-20">x64</h1>
<h1 className="text-2xl font-semibold my-2">Intel</h1>
<p className="text-muted-foreground mx-auto text-center">64-bit Intel architecture, for older Macs</p>
</div>
</div>
</FormField>
)}
{flowIndex === 2 && (platform === "Windows") && (
<FormField
enter={platform === "Windows" && flowIndex === 2}
out={platform !== "Windows" && flowIndex >= 2}
>
<FieldTitle
className="text-2xl"
>Download Zen for Windows {selectedArchitecture}</FieldTitle>
<FieldDescription>Choose the type of download you want for Zen for Windows.</FieldDescription>
<div className="flex items-center justify-center">
<div onClick={() => setSelectedWindowsDownloadType("installer")} className={ny("select-none w-full h-full mb-2 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedWindowsDownloadType === "installer" ? "border-blue-400" : "")}>
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">🚀</h1>
<h1 className="text-2xl font-semibold my-2">Installer</h1>
<p className="text-muted-foreground mx-auto text-center">Install Zen with a setup wizard</p>
</div>
<div onClick={() => setSelectedWindowsDownloadType("portable")} className={ny("select-none w-full h-full mb-2 ml-10 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedWindowsDownloadType === "portable" ? "border-blue-400" : "")}>
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">📦</h1>
<h1 className="text-2xl font-semibold my-2">Portable</h1>
<p className="text-muted-foreground mx-auto text-center">Download Zen as a ZIP file</p>
</div>
</div>
</FormField>
)}
{flowIndex === 2 && (platform === "Linux") && (
<FormField
enter={platform === "Linux" && flowIndex === 2}
out={platform !== "Linux" && flowIndex >= 2}
>
<FieldTitle
className="text-2xl"
>Download Zen for Linux {selectedArchitecture}</FieldTitle>
<FieldDescription>Choose the type of download you want for Zen for Linux.</FieldDescription>
<div className="flex items-center justify-center">
<div onClick={() => setSelectedLinuxDownloadType("AppImage")} className={ny("select-none w-full h-full mb-2 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedLinuxDownloadType === "AppImage" ? "border-blue-400" : "")}>
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">🚀</h1>
<h1 className="text-2xl font-semibold my-2">AppImage</h1>
<p className="text-muted-foreground mx-auto text-center">Install Zen with a setup wizard</p>
</div>
<div onClick={() => setSelectedLinuxDownloadType("portable")} className={ny("select-none w-full h-full mb-2 ml-5 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedLinuxDownloadType === "portable" ? "border-blue-400" : "")}>
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">📦</h1>
<h1 className="text-2xl font-semibold my-2">Portable</h1>
<p className="text-muted-foreground mx-auto text-center">Download Zen as a ZIP file</p>
</div>
<div onClick={() => changeToFlatpak()} className={ny("select-none w-full h-full mb-2 ml-5 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border", selectedLinuxDownloadType === "flatpak" ? "border-blue-400" : "",
selectedArchitecture === "32-bit" ? "opacity-50 cursor-not-allowed" : "")}>
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">🧑💻</h1>
<h1 className="text-2xl font-semibold my-2">Flatpak</h1>
<p className="text-muted-foreground mx-auto text-center">
Install Zen from the Flatpak repository.
</p>
</div>
</div>
</FormField>
)}
</div>
{!hasDownloaded && (
<div className="mt-5 flex items-center justify-between">
<Button variant="ghost" onClick={() => goBackFlow()} className={ny("opacity-70", platform === null ? "invisible" : "")}>
<ChevronLeft className="size-4" />
Back
</Button>
<Button onClick={() => continueFlow()} disabled={
(selectedPlatform === null)
}>
{((flowIndex === 1 && platform === "MacOS") || flowIndex === 2) ? "Download 🥳" : "Continue"}
</Button>
</div>
)}
</div>
</div>
</div>
<Particles
className="absolute inset-0 -z-10 hidden dark:block"
quantity={20}
ease={70}
size={0.05}
staticity={70}
color="#ffffff"
/>
<Particles
className="absolute inset-0 -z-10 block dark:hidden"
quantity={20}
ease={70}
size={0.05}
staticity={70}
color="#000000"
/>
</>
);
}

View File

@@ -4,6 +4,7 @@ import Feature, { FeatureCard } from "./feature";
import { Button } from "./ui/button";
import TextReveal from "./ui/text-reveal";
import styled, { css, keyframes } from "styled-components";
import BlurFade from "./ui/blur-fade";
const profileColors = [
"#e8cd7d",
@@ -70,31 +71,37 @@ export default function Features() {
}, []);
return (
<div>
<TextReveal text="Zen will change the way you browse the web. 🌟" />
<Feature
title="Split Views"
description="View multiple tabs at once. Divide your screen into multiple views and browse multiple websites at the same time."
color="#EEDBF9">
<img src="/split-view.png" alt="Split Views" className="w-64 h-64 absolute left-1/2 top-1/2" style={{
transform: "translate(-50%, -50%)"
}} />
</Feature>
<Feature
title="Sidebar"
description="Access websites with ease. The sidebar allows you to quickly access your favorite websites without disrupting your browsing experience."
color="#F5ED97">
<img src="/sidebar.png" alt="Split Views" className="absolute left-1/2 top-1/2 w-4/5 rounded-lg overflow-hidden" style={{
transform: "translate(-50%, -50%)"
}} />
</Feature>
<Feature
title="Profiles"
description="Switch between profiles with ease. Create multiple profiles to keep your work and personal browsing separate."
color={currentProfileColor}>
<ProfileImage enter={profile1Enter} src="/profile-1.png" alt="Profiles" id="profile-1" className="absolute left-1/2 w-3/4 top-1/2" />
<ProfileImage enter={profile2Enter} src="/profile-2.png" alt="Profiles" id="profile-2" className="absolute left-1/2 w-3/4 top-1/2" />
<ProfileImage enter={profile3Enter} src="/profile-3.png" alt="Profiles" id="profile-3" className="absolute left-1/2 w-3/4 top-1/2" />
</Feature>
{/*<TextReveal text="Zen will change the way you browse the web. 🌟" />*/}
<BlurFade delay={0.35} inView>
<Feature
title="Split Views"
description="View multiple tabs at once. Divide your screen into multiple views and browse multiple websites at the same time."
color="#EEDBF9">
<img src="/split-view.png" alt="Split Views" className="w-64 h-64 absolute left-1/2 top-1/2" style={{
transform: "translate(-50%, -50%)"
}} />
</Feature>
</BlurFade>
<BlurFade delay={0.35} inView>
<Feature
title="Sidebar"
description="Access websites with ease. The sidebar allows you to quickly access your favorite websites without disrupting your browsing experience."
color="#F5ED97">
<img src="/sidebar.png" alt="Split Views" className="absolute left-1/2 top-1/2 w-4/5 rounded-lg overflow-hidden" style={{
transform: "translate(-50%, -50%)"
}} />
</Feature>
</BlurFade>
<BlurFade delay={0.35} inView>
<Feature
title="Profiles"
description="Switch between profiles with ease. Create multiple profiles to keep your work and personal browsing separate."
color={currentProfileColor}>
<ProfileImage enter={profile1Enter} src="/profile-1.png" alt="Profiles" id="profile-1" className="absolute left-1/2 w-3/4 top-1/2" />
<ProfileImage enter={profile2Enter} src="/profile-2.png" alt="Profiles" id="profile-2" className="absolute left-1/2 w-3/4 top-1/2" />
<ProfileImage enter={profile3Enter} src="/profile-3.png" alt="Profiles" id="profile-3" className="absolute left-1/2 w-3/4 top-1/2" />
</Feature>
</BlurFade>
<div className="my-40 w-full flex items-center justify-center flex-col">
<h1 className="text-5xl text-center font-bold w-1/2">Want more?</h1>
<p className="text-muted-foreground text-center mt-3 w-1/2">Zen Browser is packed with features that will change the way you browse the web. Download it today and experience a new way to browse the web.</p>

View File

@@ -1,83 +1,103 @@
'use client'
import { ny } from "@/lib/utils";
import AnimatedGradientText from "./ui/animated-gradient-text";
import { ChevronDown, ChevronRight } from "lucide-react";
import WordPullUp from "./ui/word-pull-up";
import ShinyButton from "./ui/shiny-button";
import GridPattern from "./ui/grid-pattern";
import BlurIn from "./ui/blur-in";
import { FadeText } from "./ui/fade-text";
import styled, { keyframes } from "styled-components";
import { ArrowRightIcon } from '@radix-ui/react-icons'
import { useInView } from 'framer-motion'
import { useRef } from 'react'
import AnimatedGradientText from './ui/animated-gradient-text'
import { Button } from './ui/button'
import { BorderBeam } from './ui/border-beam'
import { ny } from '@/lib/utils'
import { ChevronRight } from 'lucide-react'
import Particles from './ui/particles'
const HeaderElement = styled.div`
background: light-dark(white, rgba(0, 0, 0, 0.5));
`;
export default function Header() {
return (
const ref = useRef(null)
const inView = useInView(ref, { once: true, margin: '-100px' })
return (
<>
<div className="absolute top-3/4 z-10">
<img src="/browser-dark.png" className={ny('hidden dark:block mx-auto shadow-sm border mt-24 z-0 w-4/5 rounded-xl overflow-hidden')} />
<img src="/browser-light.png" className={ny('dark:hidden mx-auto shadow-sm border mt-24 z-0 w-4/5 rounded-xl overflow-hidden')} />
</div>
<HeaderElement className="w-full relative flex h-screen justify-center flex-col align-center border-b">
<GridPattern
numSquares={30}
maxOpacity={0.5}
height={50}
width={50}
duration={3}
repeatDelay={1}
x={-1}
y={-1}
strokeDasharray="4 2"
className={ny(
'[mask-image:radial-gradient(350px_circle_at_center,white,transparent)]',
'w-full z-0',
)}
/>
<div className="z-10 flex mb-10 items-center justify-center">
<a href="/download">
<AnimatedGradientText>
🎉
{' '}
<hr className="mx-2 h-4 w-[1px] shrink-0 bg-gray-300" />
{' '}
<span
className={ny(
`inline animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
)}
>
Introducing Zen Alpha
</span>
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
</AnimatedGradientText>
<section
id="hero"
className="relative mx-auto mt-40 max-w-7xl px-6 text-center md:px-8"
>
<a href="/download">
<AnimatedGradientText>
🎉
{' '}
<hr className="mx-2 h-4 w-[1px] shrink-0 bg-gray-300" />
{' '}
<span
className={ny(
`inline animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
)}
>
Introducing Zen Alpha
</span>
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
</AnimatedGradientText>
</a>
</div>
<WordPullUp
className="text-3xl font-bold tracking-[-0.02em] text-black dark:text-white md:text-7xl md:leading-[5rem]"
words="Make It Yours"
/>
<BlurIn
word="Firefox based browser with a focus on privacy and customization."
className="!text-xl text-muted-foreground !font-medium"
/>
<div className="max-w-1/4 mt-10 flex items-center justify-center">
<a href="/release-notes" className="mr-5">
<FadeText
className="text-md font-medium text-black dark:text-white"
direction="up"
framerProps={{
show: { transition: { delay: 0.2 } },
}}
text="Release Notes"
/>
</a>
<a href="/download">
<ShinyButton text="Download now" />
</a>
</div>
</HeaderElement>
<h1 className="animate-fade-in -translate-y-4 text-balance bg-gradient-to-br from-black from-30% to-black/40 bg-clip-text py-6 text-5xl font-semibold leading-none tracking-tighter text-transparent opacity-0 [--animation-delay:200ms] sm:text-6xl md:text-7xl lg:text-8xl dark:from-white dark:to-white/40">
Zen is the best way
<br className="hidden md:block" />
{' '}
to browse the web.
</h1>
<p className="animate-fade-in mb-12 -translate-y-4 text-balance text-lg tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms] md:text-xl">
Beautifully designed, privacy-focused, and packed with features.
<br className="hidden md:block" />
{' '}
We care about your experience, not your data.
</p>
<Button className="animate-fade-in -translate-y-4 gap-1 rounded-lg text-white opacity-0 ease-in-out [--animation-delay:600ms] dark:text-black" onClick={() => window.location.href = '/download'}>
<span>Download Zen Now </span>
<ArrowRightIcon className="ml-1 size-4 transition-transform duration-300 ease-in-out group-hover:translate-x-1" />
</Button>
<div
ref={ref}
className="animate-fade-up relative mt-32 opacity-0 [--animation-delay:400ms] [perspective:2000px] after:absolute after:inset-0 after:z-50 after:[background:linear-gradient(to_top,hsl(var(--background))_30%,transparent)]"
>
<div
className={`rounded-xl border border-white/10 bg-white bg-opacity-[0.01] before:absolute before:bottom-1/2 before:left-0 before:top-0 before:size-full before:opacity-0 before:[background-image:linear-gradient(to_bottom,var(--color-one),var(--color-one),transparent_40%)] before:[filter:blur(180px)] ${
inView ? 'before:animate-image-glow' : ''
}`}
>
<BorderBeam
size={200}
duration={12}
delay={11}
colorFrom="var(--color-one)"
colorTo="var(--color-two)"
/>
<img
src="/browser-dark.png"
alt="browser Image"
className="relative hidden size-full rounded-[inherit] border object-contain dark:block"
/>
<img
src="/browser-light.png"
alt="browser Image"
className="relative block size-full rounded-[inherit] border object-contain dark:hidden"
/>
</div>
</div>
</section>
<Particles
className="absolute inset-0 -z-10 hidden dark:block"
quantity={50}
ease={70}
size={0.05}
staticity={40}
color="#ffffff"
/>
<Particles
className="absolute inset-0 -z-10 block dark:hidden"
quantity={50}
ease={70}
size={0.05}
staticity={40}
color="#000000"
/>
</>
)
)
}

View File

@@ -0,0 +1,60 @@
'use client'
import { useRef } from 'react'
import type { Variants } from 'framer-motion'
import { AnimatePresence, motion, useInView } from 'framer-motion'
interface BlurFadeProps {
children: React.ReactNode
className?: string
variant?: {
hidden: { y: number }
visible: { y: number }
}
duration?: number
delay?: number
yOffset?: number
inView?: boolean
inViewMargin?: string
blur?: string
}
export default function BlurFade({
children,
className,
variant,
duration = 0.4,
delay = 0,
yOffset = 6,
inView = false,
inViewMargin = '-50px',
blur = '6px',
}: BlurFadeProps) {
const ref = useRef(null)
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
const isInView = !inView || inViewResult
const defaultVariants: Variants = {
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
}
const combinedVariants = variant || defaultVariants
return (
<AnimatePresence>
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
exit="hidden"
variants={combinedVariants}
transition={{
delay: 0.04 + delay,
duration,
ease: 'easeOut',
}}
className={className}
>
{children}
</motion.div>
</AnimatePresence>
)
}

View File

@@ -0,0 +1,49 @@
import { ny } from '@/lib/utils'
interface BorderBeamProps {
className?: string
size?: number
duration?: number
borderWidth?: number
anchor?: number
colorFrom?: string
colorTo?: string
delay?: number
}
export function BorderBeam({
className,
size = 200,
duration = 15,
anchor = 90,
borderWidth = 1.5,
colorFrom = '#ffaa40',
colorTo = '#9c40ff',
delay = 0,
}: BorderBeamProps) {
return (
<div
style={
{
'--size': size,
'--duration': duration,
'--anchor': anchor,
'--border-width': borderWidth,
'--color-from': colorFrom,
'--color-to': colorTo,
'--delay': `-${delay}s`,
} as React.CSSProperties
}
className={ny(
'pointer-events-none absolute inset-0 rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]',
// mask styles
'![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]',
// pseudo styles
'after:animate-border-beam after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]',
className,
)}
/>
)
}

View File

@@ -0,0 +1,120 @@
'use client'
import confetti from 'canvas-confetti'
import type { ReactNode } from 'react'
import React, { createContext, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
import type {
GlobalOptions as ConfettiGlobalOptions,
CreateTypes as ConfettiInstance,
Options as ConfettiOptions,
} from 'canvas-confetti'
import { Button } from '~/components/ui/button'
import type { ButtonProps } from '~/components/ui/button'
interface Api {
fire: (options?: ConfettiOptions) => void
}
type Props = React.ComponentPropsWithRef<'canvas'> & {
options?: ConfettiOptions
globalOptions?: ConfettiGlobalOptions
manualstart?: boolean
children?: ReactNode
}
export type ConfettiRef = Api | null
const ConfettiContext = createContext<Api>({} as Api)
const Confetti = forwardRef<ConfettiRef, Props>((props, ref) => {
const {
options,
globalOptions = { resize: true, useWorker: true },
manualstart = false,
children,
...rest
} = props
const instanceRef = useRef<ConfettiInstance | null>(null) // confetti instance
const canvasRef = useCallback(
// https://react.dev/reference/react-dom/components/common#ref-callback
// https://reactjs.org/docs/refs-and-the-dom.html#callback-refs
(node: HTMLCanvasElement) => {
if (node !== null) {
// <canvas> is mounted => create the confetti instance
if (instanceRef.current)
return // if not already created
instanceRef.current = confetti.create(node, {
...globalOptions,
resize: true,
})
}
else {
// <canvas> is unmounted => reset and destroy instanceRef
if (instanceRef.current) {
instanceRef.current.reset()
instanceRef.current = null
}
}
},
[globalOptions],
)
// `fire` is a function that calls the instance() with `opts` merged with `options`
const fire = useCallback(
(opts = {}) => instanceRef.current?.({ ...options, ...opts }),
[options],
)
const api = useMemo(
() => ({
fire,
}),
[fire],
)
useImperativeHandle(ref, () => api, [api])
useEffect(() => {
if (!manualstart)
fire()
}, [manualstart, fire])
return (
<ConfettiContext.Provider value={api}>
<canvas ref={canvasRef} {...rest} />
{children}
</ConfettiContext.Provider>
)
})
interface ConfettiButtonProps extends ButtonProps {
options?: ConfettiOptions &
ConfettiGlobalOptions & { canvas?: HTMLCanvasElement }
children?: React.ReactNode
}
function ConfettiButton({ options, children, ...props }: ConfettiButtonProps) {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
const rect = event.currentTarget.getBoundingClientRect()
const x = rect.left + rect.width / 2
const y = rect.top + rect.height / 2
confetti({
...options,
origin: {
x: x / window.innerWidth,
y: y / window.innerHeight,
},
})
}
return (
<Button onClick={handleClick} {...props}>
{children}
</Button>
)
}
export { Confetti, ConfettiButton }
export default Confetti

View File

@@ -0,0 +1,271 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
interface MousePosition {
x: number
y: number
}
function MousePosition(): MousePosition {
const [mousePosition, setMousePosition] = useState<MousePosition>({
x: 0,
y: 0,
})
useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
setMousePosition({ x: event.clientX, y: event.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
}
}, [])
return mousePosition
}
interface ParticlesProps {
className?: string
quantity?: number
staticity?: number
ease?: number
size?: number
refresh?: boolean
color?: string
vx?: number
vy?: number
}
function hexToRgb(hex: string): number[] {
hex = hex.replace('#', '')
const hexInt = Number.parseInt(hex, 16)
const red = (hexInt >> 16) & 255
const green = (hexInt >> 8) & 255
const blue = hexInt & 255
return [red, green, blue]
}
const Particles: React.FC<ParticlesProps> = ({
className = '',
quantity = 100,
staticity = 50,
ease = 50,
size = 0.4,
refresh = false,
color = '#ffffff',
vx = 0,
vy = 0,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const canvasContainerRef = useRef<HTMLDivElement>(null)
const context = useRef<CanvasRenderingContext2D | null>(null)
const circles = useRef<any[]>([])
const mousePosition = MousePosition()
const mouse = useRef<{ x: number, y: number }>({ x: 0, y: 0 })
const canvasSize = useRef<{ w: number, h: number }>({ w: 0, h: 0 })
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1
useEffect(() => {
if (canvasRef.current) {
context.current = canvasRef.current.getContext('2d')
}
initCanvas()
animate()
window.addEventListener('resize', initCanvas)
return () => {
window.removeEventListener('resize', initCanvas)
}
}, [color])
useEffect(() => {
onMouseMove()
}, [mousePosition.x, mousePosition.y])
useEffect(() => {
initCanvas()
}, [refresh])
const initCanvas = () => {
resizeCanvas()
drawParticles()
}
const onMouseMove = () => {
if (canvasRef.current) {
const rect = canvasRef.current.getBoundingClientRect()
const { w, h } = canvasSize.current
const x = mousePosition.x - rect.left - w / 2
const y = mousePosition.y - rect.top - h / 2
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
if (inside) {
mouse.current.x = x
mouse.current.y = y
}
}
}
interface Circle {
x: number
y: number
translateX: number
translateY: number
size: number
alpha: number
targetAlpha: number
dx: number
dy: number
magnetism: number
}
const resizeCanvas = () => {
if (canvasContainerRef.current && canvasRef.current && context.current) {
circles.current.length = 0
canvasSize.current.w = canvasContainerRef.current.offsetWidth
canvasSize.current.h = canvasContainerRef.current.offsetHeight
canvasRef.current.width = canvasSize.current.w * dpr
canvasRef.current.height = canvasSize.current.h * dpr
canvasRef.current.style.width = `${canvasSize.current.w}px`
canvasRef.current.style.height = `${canvasSize.current.h}px`
context.current.scale(dpr, dpr)
}
}
const circleParams = (): Circle => {
const x = Math.floor(Math.random() * canvasSize.current.w)
const y = Math.floor(Math.random() * canvasSize.current.h)
const translateX = 0
const translateY = 0
const pSize = Math.floor(Math.random() * 2) + size
const alpha = 0
const targetAlpha = Number.parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
const dx = (Math.random() - 0.5) * 0.1
const dy = (Math.random() - 0.5) * 0.1
const magnetism = 0.1 + Math.random() * 4
return {
x,
y,
translateX,
translateY,
size: pSize,
alpha,
targetAlpha,
dx,
dy,
magnetism,
}
}
const rgb = hexToRgb(color)
const drawCircle = (circle: Circle, update = false) => {
if (context.current) {
const { x, y, translateX, translateY, size, alpha } = circle
context.current.translate(translateX, translateY)
context.current.beginPath()
context.current.arc(x, y, size, 0, 2 * Math.PI)
context.current.fillStyle = `rgba(${rgb.join(', ')}, ${alpha})`
context.current.fill()
context.current.setTransform(dpr, 0, 0, dpr, 0, 0)
if (!update) {
circles.current.push(circle)
}
}
}
const clearContext = () => {
if (context.current) {
context.current.clearRect(
0,
0,
canvasSize.current.w,
canvasSize.current.h,
)
}
}
const drawParticles = () => {
clearContext()
const particleCount = quantity
for (let i = 0; i < particleCount; i++) {
const circle = circleParams()
drawCircle(circle)
}
}
const remapValue = (
value: number,
start1: number,
end1: number,
start2: number,
end2: number,
): number => {
const remapped
= ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
return remapped > 0 ? remapped : 0
}
const animate = () => {
clearContext()
circles.current.forEach((circle: Circle, i: number) => {
// Handle the alpha value
const edge = [
circle.x + circle.translateX - circle.size, // distance from left edge
canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
circle.y + circle.translateY - circle.size, // distance from top edge
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
]
const closestEdge = edge.reduce((a, b) => Math.min(a, b))
const remapClosestEdge = Number.parseFloat(
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
)
if (remapClosestEdge > 1) {
circle.alpha += 0.02
if (circle.alpha > circle.targetAlpha) {
circle.alpha = circle.targetAlpha
}
}
else {
circle.alpha = circle.targetAlpha * remapClosestEdge
}
circle.x += circle.dx + vx
circle.y += circle.dy + vy
circle.translateX
+= (mouse.current.x / (staticity / circle.magnetism) - circle.translateX)
/ ease
circle.translateY
+= (mouse.current.y / (staticity / circle.magnetism) - circle.translateY)
/ ease
drawCircle(circle, true)
// circle gets out of the canvas
if (
circle.x < -circle.size
|| circle.x > canvasSize.current.w + circle.size
|| circle.y < -circle.size
|| circle.y > canvasSize.current.h + circle.size
) {
// remove the circle from the array
circles.current.splice(i, 1)
// create a new circle
const newCircle = circleParams()
drawCircle(newCircle)
// update the circle position
}
})
window.requestAnimationFrame(animate)
}
return (
<div className={className} ref={canvasContainerRef} aria-hidden="true">
<canvas ref={canvasRef} className="size-full" />
</div>
)
}
export default Particles

View File

@@ -1,7 +1,46 @@
export const releases: any = {
WindowsInstaller: "zen.installer.exe",
WindowsStubInstaller: "zen.installer.pretty.exe",
WindowsZip: "zen.win64.zip",
MacOS: "zen.macos.dmg",
Linux: "zen.linux.tar.bz2",
};
export const releases: any = {
WindowsInstaller: "zen.installer-x64.exe",
WindowsInstaller32: "zen.installer-x32.exe",
WindowsZip: "zen.win-x64.zip",
WindowsZip32: "zen.win-x32.zip",
MacOS: "zen.macos-aarch64.dmg",
MacOSIntel: "zen.macos-x64.dmg",
Linux: "zen.linux-x64.tar.bz2",
Linux32: "zen.linux-x32.tar.bz2",
LinuxAppImage: "zen-x64.AppImage",
LinuxAppImage32: "zen-x32.AppImage",
};
// platform
// -> arch
// -> file
export const releaseTree: any = {
windows: {
x64: {
installer: "WindowsInstaller",
portable: "WindowsZip",
},
x32: {
installer: "WindowsInstaller32",
portable: "WindowsZip32",
},
},
macos: {
x64: "MacOSIntel",
arm: "MacOS",
},
linux: {
x64: {
portable: "Linux",
appimage: "LinuxAppImage",
},
x32: {
portable: "Linux32",
appimage: "LinuxAppImage32",
},
},
};

View File

@@ -1,4 +1,5 @@
import type { Config } from "tailwindcss"
import { fontFamily } from 'tailwindcss/defaultTheme'
const config = {
darkMode: ["class"],
@@ -12,81 +13,117 @@ const config = {
theme: {
container: {
center: true,
padding: "2rem",
padding: '2rem',
screens: {
"2xl": "1400px",
'2xl': '1400px',
},
},
extend: {
fontFamily: {
sans: ['var(--font-sans)', ...fontFamily.sans],
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
grid: {
"0%": { transform: "translateY(-50%)" },
"100%": { transform: "translateY(0)" },
},
"shine-pulse": {
"0%": {
"background-position": "0% 0%",
},
"50%": {
"background-position": "100% 100%",
},
to: {
"background-position": "0% 0%",
},
},
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
'border-beam': {
'100%': {
'offset-distance': '100%',
},
},
'image-glow': {
'0%': {
'opacity': '0',
'animation-timing-function': 'cubic-bezier(0.74, 0.25, 0.76, 1)',
},
'10%': {
'opacity': '0.7',
'animation-timing-function': 'cubic-bezier(0.12, 0.01, 0.08, 0.99)',
},
'100%': {
opacity: '0.4',
},
},
'fade-in': {
from: { opacity: '0', transform: 'translateY(-10px)' },
to: { opacity: '1', transform: 'none' },
},
'fade-up': {
from: { opacity: '0', transform: 'translateY(20px)' },
to: { opacity: '1', transform: 'none' },
},
'shimmer': {
'0%, 90%, 100%': {
'background-position': 'calc(-100% - var(--shimmer-width)) 0',
},
'30%, 60%': {
'background-position': 'calc(100% + var(--shimmer-width)) 0',
},
},
'marquee': {
from: { transform: 'translateX(0)' },
to: { transform: 'translateX(calc(-100% - var(--gap)))' },
},
'marquee-vertical': {
from: { transform: 'translateY(0)' },
to: { transform: 'translateY(calc(-100% - var(--gap)))' },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
grid: "grid 15s linear infinite",
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear',
'image-glow': 'image-glow 4100ms 600ms ease-out forwards',
'fade-in': 'fade-in 1000ms var(--animation-delay, 0ms) ease forwards',
'fade-up': 'fade-up 1000ms var(--animation-delay, 0ms) ease forwards',
'shimmer': 'shimmer 8s infinite',
'marquee': 'marquee var(--duration) infinite linear',
'marquee-vertical': 'marquee-vertical var(--duration) linear infinite',
},
},
},