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-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"@vercel/postgres": "^0.9.0", "@vercel/postgres": "^0.9.0",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cobe": "^0.6.3", "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": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "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-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"@vercel/postgres": "^0.9.0", "@vercel/postgres": "^0.9.0",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cobe": "^0.6.3", "cobe": "^0.6.3",

View File

@@ -1,9 +1,9 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 0 0% 3.9%; --foreground: 0 0% 3.9%;
@@ -33,10 +33,23 @@
--ring: 0 0% 3.9%; --ring: 0 0% 3.9%;
--radius: 0.5rem; --radius: 0.5rem;
}
.dark { /* Custom properties */
--background: 0 0% 3.9%; --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%; --foreground: 0 0% 98%;
--card: 0 0% 3.9%; --card: 0 0% 3.9%;
@@ -63,14 +76,15 @@
--border: 0 0% 14.9%; --border: 0 0% 14.9%;
--input: 0 0% 14.9%; --input: 0 0% 14.9%;
--ring: 0 0% 83.1%; --ring: 0 0% 83.1%;
} --color-one: #6aa8e2;
} }
}
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

View File

@@ -1,15 +1,15 @@
"use client"; "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 { 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"; const BASE_URL = "https://github.com/zen-browser/desktop/releases/latest/download";
@@ -19,12 +19,10 @@ function getDefaultPlatformBasedOnUserAgent() {
userAgent = window.navigator.userAgent; userAgent = window.navigator.userAgent;
} }
if (userAgent.includes("Win")) { if (userAgent.includes("Win")) {
return "WindowsInstaller"; return "Windows";
} }
if (userAgent.includes("Mac")) { if (userAgent.includes("Mac")) {
// TODO: return "MacOS";
// return "MacOS";
return "";
} }
if (userAgent.includes("Linux")) { if (userAgent.includes("Linux")) {
return "Linux"; return "Linux";
@@ -32,110 +30,350 @@ function getDefaultPlatformBasedOnUserAgent() {
return ""; return "";
} }
const formSchema = z.object({ const field_enter = keyframes`
platform: z.string().nonempty(), 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() { export default function DownloadPage() {
const form = useForm<z.infer<typeof formSchema>>({ const [platform, setPlatform] = useState<string | null>(null);
resolver: zodResolver(formSchema), const [architecture, setArchitecture] = useState<string | null>(null);
defaultValues: { const [windowsDownloadType, setWindowsDownloadType] = useState<string | null>(null);
platform: getDefaultPlatformBasedOnUserAgent(), const [linuxDownloadType, setLinuxDownloadType] = useState<string | null>(null);
},
});
const watchRelease = form.watch("platform");
const onSubmit = async (data: any) => { const [selectedPlatform, setSelectedPlatform] = useState(getDefaultPlatformBasedOnUserAgent());
const platform = data.platform; const [selectedArchitecture, setSelectedArchitecture] = useState("64-bit");
addDownload(platform); const [selectedWindowsDownloadType, setSelectedWindowsDownloadType] = useState("installer");
console.log("Data: ", data) const [selectedLinuxDownloadType, setSelectedLinuxDownloadType] = useState("portable");
console.log("Platform: ", platform)
console.log("Releases: ", releases) const [hasDownloaded, setHasDownloaded] = useState(false);
const releasesForPlatform = releases[platform];
console.log("Releases for platform: ", releasesForPlatform) const [flowIndex, setFlowIndex] = useState(0);
const url = `${BASE_URL}/${releasesForPlatform}`;
console.log("URL: ", url) const throwConfetti = () => {
window.open(url, "_blank"); 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 ( 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"> <link rel="stylesheet" type='text/css' href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css" />
<GridPattern <link rel="stylesheet" type='text/css' href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css" />
numSquares={30} <link rel="stylesheet" type='text/css' href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css" />
maxOpacity={0.5}
height={50} <div className="w-full overflow-hidden relative h-screen flex items-center justify-center flex-col lg:flex-row">
width={50} <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">
duration={3} {hasDownloaded && (
repeatDelay={1} <div className="flex items-center justify-center flex-col">
x={-1} <h1 className="text-6xl font-bold">Downloaded! </h1>
y={-1} <p className="text-muted-foreground mt-3">Zen Browser has been downloaded successfully. Enjoy browsing the web with Zen!</p>
strokeDasharray="4 2" <div className="flex font-bold mt-5 items-center justify-between mx-auto">
className={ny( <a href="https://github.com/zen-browser">Source Code</a>
'[mask-image:radial-gradient(350px_circle_at_center,white,transparent)]', <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>
'w-full z-0', <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>
</>
)} )}
/> <div className="relative w-full">
<h1 className="!text-4xl font-bold tracking-[-0.02em] text-black dark:text-white md:text-7xl md:leading-[5rem]"> {platform === null && (
Download Zen Browser <FormField
</h1> enter={platform === null}
<p className="!text-md text-muted-foreground !font-medium"> out={platform !== null}
Get started with Zen Browser today. Get back to browsing the web with peace of mind. >
</p> <FieldTitle>Platform</FieldTitle>
</div> <FieldDescription>Choose the platform you want to download Zen for.</FieldDescription>
<div className="w-full lg:w-1/2 relative flex flex-col relative items-cetner justify-start"> <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" : "")}>
<div className="w-full lg:w-2/3 relative flex flex-col items-center mx-auto mt-10 lg:mt-0"> <Checkbox checked={selectedPlatform === "Windows"} />
<Form {...form}> <i className="devicon-windows8-original ml-3 p-2 border border-blue-400 rounded-lg"></i>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6"> <div className="ml-2">Windows</div>
<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> </div>
)} <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" : "")}>
{watchRelease === "MacOS" && ( <Checkbox checked={selectedPlatform === "Linux"} />
<div className="mt-20 rounded border bg-muted p-4 text-muted-foreground"> <i className="devicon-linux-plain ml-3 p-2 border border-yellow-400 rounded-lg"></i>
<p> <div className="ml-2">Linux</div>
<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> </div>
)} <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" : "")}>
</form> <Checkbox checked={selectedPlatform === "MacOS"} />
</Form> <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> </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 { Button } from "./ui/button";
import TextReveal from "./ui/text-reveal"; import TextReveal from "./ui/text-reveal";
import styled, { css, keyframes } from "styled-components"; import styled, { css, keyframes } from "styled-components";
import BlurFade from "./ui/blur-fade";
const profileColors = [ const profileColors = [
"#e8cd7d", "#e8cd7d",
@@ -70,31 +71,37 @@ export default function Features() {
}, []); }, []);
return ( return (
<div> <div>
<TextReveal text="Zen will change the way you browse the web. 🌟" /> {/*<TextReveal text="Zen will change the way you browse the web. 🌟" />*/}
<Feature <BlurFade delay={0.35} inView>
title="Split Views" <Feature
description="View multiple tabs at once. Divide your screen into multiple views and browse multiple websites at the same time." title="Split Views"
color="#EEDBF9"> description="View multiple tabs at once. Divide your screen into multiple views and browse multiple websites at the same time."
<img src="/split-view.png" alt="Split Views" className="w-64 h-64 absolute left-1/2 top-1/2" style={{ color="#EEDBF9">
transform: "translate(-50%, -50%)" <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 </Feature>
title="Sidebar" </BlurFade>
description="Access websites with ease. The sidebar allows you to quickly access your favorite websites without disrupting your browsing experience." <BlurFade delay={0.35} inView>
color="#F5ED97"> <Feature
<img src="/sidebar.png" alt="Split Views" className="absolute left-1/2 top-1/2 w-4/5 rounded-lg overflow-hidden" style={{ title="Sidebar"
transform: "translate(-50%, -50%)" description="Access websites with ease. The sidebar allows you to quickly access your favorite websites without disrupting your browsing experience."
}} /> color="#F5ED97">
</Feature> <img src="/sidebar.png" alt="Split Views" className="absolute left-1/2 top-1/2 w-4/5 rounded-lg overflow-hidden" style={{
<Feature transform: "translate(-50%, -50%)"
title="Profiles" }} />
description="Switch between profiles with ease. Create multiple profiles to keep your work and personal browsing separate." </Feature>
color={currentProfileColor}> </BlurFade>
<ProfileImage enter={profile1Enter} src="/profile-1.png" alt="Profiles" id="profile-1" className="absolute left-1/2 w-3/4 top-1/2" /> <BlurFade delay={0.35} inView>
<ProfileImage enter={profile2Enter} src="/profile-2.png" alt="Profiles" id="profile-2" className="absolute left-1/2 w-3/4 top-1/2" /> <Feature
<ProfileImage enter={profile3Enter} src="/profile-3.png" alt="Profiles" id="profile-3" className="absolute left-1/2 w-3/4 top-1/2" /> title="Profiles"
</Feature> 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"> <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> <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> <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 { ArrowRightIcon } from '@radix-ui/react-icons'
import AnimatedGradientText from "./ui/animated-gradient-text"; import { useInView } from 'framer-motion'
import { ChevronDown, ChevronRight } from "lucide-react"; import { useRef } from 'react'
import WordPullUp from "./ui/word-pull-up"; import AnimatedGradientText from './ui/animated-gradient-text'
import ShinyButton from "./ui/shiny-button"; import { Button } from './ui/button'
import GridPattern from "./ui/grid-pattern"; import { BorderBeam } from './ui/border-beam'
import BlurIn from "./ui/blur-in"; import { ny } from '@/lib/utils'
import { FadeText } from "./ui/fade-text"; import { ChevronRight } from 'lucide-react'
import styled, { keyframes } from "styled-components"; import Particles from './ui/particles'
const HeaderElement = styled.div`
background: light-dark(white, rgba(0, 0, 0, 0.5));
`;
export default function Header() { 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"> <section
<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')} /> id="hero"
<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')} /> className="relative mx-auto mt-40 max-w-7xl px-6 text-center md:px-8"
</div> >
<HeaderElement className="w-full relative flex h-screen justify-center flex-col align-center border-b"> <a href="/download">
<GridPattern <AnimatedGradientText>
numSquares={30} 🎉
maxOpacity={0.5} {' '}
height={50} <hr className="mx-2 h-4 w-[1px] shrink-0 bg-gray-300" />
width={50} {' '}
duration={3} <span
repeatDelay={1} className={ny(
x={-1} `inline animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
y={-1} )}
strokeDasharray="4 2" >
className={ny( Introducing Zen Alpha
'[mask-image:radial-gradient(350px_circle_at_center,white,transparent)]', </span>
'w-full z-0', <ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
)} </AnimatedGradientText>
/>
<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>
</a> </a>
</div> <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">
<WordPullUp Zen is the best way
className="text-3xl font-bold tracking-[-0.02em] text-black dark:text-white md:text-7xl md:leading-[5rem]" <br className="hidden md:block" />
words="Make It Yours" {' '}
/> to browse the web.
<BlurIn </h1>
word="Firefox based browser with a focus on privacy and customization." <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">
className="!text-xl text-muted-foreground !font-medium" Beautifully designed, privacy-focused, and packed with features.
/> <br className="hidden md:block" />
<div className="max-w-1/4 mt-10 flex items-center justify-center"> {' '}
<a href="/release-notes" className="mr-5"> We care about your experience, not your data.
<FadeText </p>
className="text-md font-medium text-black dark:text-white" <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'}>
direction="up" <span>Download Zen Now </span>
framerProps={{ <ArrowRightIcon className="ml-1 size-4 transition-transform duration-300 ease-in-out group-hover:translate-x-1" />
show: { transition: { delay: 0.2 } }, </Button>
}} <div
text="Release Notes" 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)]"
</a> >
<a href="/download"> <div
<ShinyButton text="Download now" /> 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)] ${
</a> inView ? 'before:animate-image-glow' : ''
</div> }`}
</HeaderElement> >
<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 = { export const releases: any = {
WindowsInstaller: "zen.installer.exe", WindowsInstaller: "zen.installer-x64.exe",
WindowsStubInstaller: "zen.installer.pretty.exe", WindowsInstaller32: "zen.installer-x32.exe",
WindowsZip: "zen.win64.zip",
MacOS: "zen.macos.dmg", WindowsZip: "zen.win-x64.zip",
Linux: "zen.linux.tar.bz2", 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 type { Config } from "tailwindcss"
import { fontFamily } from 'tailwindcss/defaultTheme'
const config = { const config = {
darkMode: ["class"], darkMode: ["class"],
@@ -12,81 +13,117 @@ const config = {
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem", padding: '2rem',
screens: { screens: {
"2xl": "1400px", '2xl': '1400px',
},
},
extend: {
fontFamily: {
sans: ['var(--font-sans)', ...fontFamily.sans],
}, },
},
extend: {
colors: { colors: {
border: "hsl(var(--border))", border: 'hsl(var(--border))',
input: "hsl(var(--input))", input: 'hsl(var(--input))',
ring: "hsl(var(--ring))", ring: 'hsl(var(--ring))',
background: "hsl(var(--background))", background: 'hsl(var(--background))',
foreground: "hsl(var(--foreground))", foreground: 'hsl(var(--foreground))',
primary: { primary: {
DEFAULT: "hsl(var(--primary))", DEFAULT: 'hsl(var(--primary))',
foreground: "hsl(var(--primary-foreground))", foreground: 'hsl(var(--primary-foreground))',
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: 'hsl(var(--secondary))',
foreground: "hsl(var(--secondary-foreground))", foreground: 'hsl(var(--secondary-foreground))',
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive))", DEFAULT: 'hsl(var(--destructive))',
foreground: "hsl(var(--destructive-foreground))", foreground: 'hsl(var(--destructive-foreground))',
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: 'hsl(var(--muted))',
foreground: "hsl(var(--muted-foreground))", foreground: 'hsl(var(--muted-foreground))',
}, },
accent: { accent: {
DEFAULT: "hsl(var(--accent))", DEFAULT: 'hsl(var(--accent))',
foreground: "hsl(var(--accent-foreground))", foreground: 'hsl(var(--accent-foreground))',
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover))", DEFAULT: 'hsl(var(--popover))',
foreground: "hsl(var(--popover-foreground))", foreground: 'hsl(var(--popover-foreground))',
}, },
card: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: 'hsl(var(--card))',
foreground: "hsl(var(--card-foreground))", foreground: 'hsl(var(--card-foreground))',
}, },
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: 'var(--radius)',
md: "calc(var(--radius) - 2px)", md: 'calc(var(--radius) - 2px)',
sm: "calc(var(--radius) - 4px)", sm: 'calc(var(--radius) - 4px)',
}, },
keyframes: { keyframes: {
"accordion-down": { 'accordion-down': {
from: { height: "0" }, from: { height: '0' },
to: { height: "var(--radix-accordion-content-height)" }, to: { height: 'var(--radix-accordion-content-height)' },
}, },
"accordion-up": { 'accordion-up': {
from: { height: "var(--radix-accordion-content-height)" }, from: { height: 'var(--radix-accordion-content-height)' },
to: { height: "0" }, to: { height: '0' },
}, },
grid: { 'border-beam': {
"0%": { transform: "translateY(-50%)" }, '100%': {
"100%": { transform: "translateY(0)" }, 'offset-distance': '100%',
}, },
"shine-pulse": { },
"0%": { 'image-glow': {
"background-position": "0% 0%", '0%': {
}, 'opacity': '0',
"50%": { 'animation-timing-function': 'cubic-bezier(0.74, 0.25, 0.76, 1)',
"background-position": "100% 100%", },
}, '10%': {
to: { 'opacity': '0.7',
"background-position": "0% 0%", '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: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", 'accordion-down': 'accordion-down 0.2s ease-out',
"accordion-up": "accordion-up 0.2s ease-out", 'accordion-up': 'accordion-up 0.2s ease-out',
grid: "grid 15s linear infinite", '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',
}, },
}, },
}, },