Redesigned the website!
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -33,10 +33,23 @@
|
|||||||
--ring: 0 0% 3.9%;
|
--ring: 0 0% 3.9%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
/* 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 {
|
.dark {
|
||||||
--background: 0 0% 3.9%;
|
--background: 0 0% 0%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
--card: 0 0% 3.9%;
|
--card: 0 0% 3.9%;
|
||||||
@@ -63,6 +76,7 @@
|
|||||||
--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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<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>
|
||||||
<div className="w-full lg:w-1/2 relative flex flex-col relative items-cetner justify-start">
|
{selectedPlatform === "MacOS" && (
|
||||||
<div className="w-full lg:w-2/3 relative flex flex-col items-center mx-auto mt-10 lg:mt-0">
|
<div className="mt-12 flex flex-col items-start border justify-between rounded-md bg-background p-5">
|
||||||
<Form {...form}>
|
<h3 className="text-xl font-semibold">Installation Instructions</h3>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
<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">
|
||||||
|
{platform === null && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
enter={platform === null}
|
||||||
name="platform"
|
out={platform !== null}
|
||||||
render={({ field }) => (
|
>
|
||||||
<FormItem>
|
<FieldTitle>Platform</FieldTitle>
|
||||||
<FormLabel>Select your operating system</FormLabel>
|
<FieldDescription>Choose the platform you want to download Zen for.</FieldDescription>
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
<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" : "")}>
|
||||||
<SelectTrigger className="w-full mb-5">
|
<Checkbox checked={selectedPlatform === "Windows"} />
|
||||||
<SelectValue placeholder="Operating System" />
|
<i className="devicon-windows8-original ml-3 p-2 border border-blue-400 rounded-lg"></i>
|
||||||
</SelectTrigger>
|
<div className="ml-2">Windows</div>
|
||||||
<SelectContent>
|
</div>
|
||||||
<SelectGroup>
|
<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" : "")}>
|
||||||
<SelectLabel>Operating System</SelectLabel>
|
<Checkbox checked={selectedPlatform === "Linux"} />
|
||||||
<SelectItem value="WindowsInstaller">Windows Installer</SelectItem>
|
<i className="devicon-linux-plain ml-3 p-2 border border-yellow-400 rounded-lg"></i>
|
||||||
<SelectItem value="WindowsZip">Windows (Zip)</SelectItem>
|
<div className="ml-2">Linux</div>
|
||||||
<SelectItem value="MacOS">MacOS</SelectItem>
|
</div>
|
||||||
<SelectItem value="Linux">Linux</SelectItem>
|
<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" : "")}>
|
||||||
<SelectItem value="WindowsStubInstaller" disabled>Windows Pretty Installer</SelectItem>
|
<Checkbox checked={selectedPlatform === "MacOS"} />
|
||||||
</SelectGroup>
|
<i className="devicon-apple-original p-2 border border-purple-400 ml-3 rounded-lg"></i>
|
||||||
</SelectContent>
|
<div className="ml-2 font-bold">MacOS</div>
|
||||||
</Select>
|
</div>
|
||||||
</FormItem>
|
</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>
|
||||||
|
<Particles
|
||||||
|
className="absolute inset-0 -z-10 hidden dark:block"
|
||||||
|
quantity={20}
|
||||||
|
ease={70}
|
||||||
|
size={0.05}
|
||||||
|
staticity={70}
|
||||||
|
color="#ffffff"
|
||||||
/>
|
/>
|
||||||
<Button type="submit">Download Zen 🎉</Button>
|
<Particles
|
||||||
{watchRelease === "Linux" && (
|
className="absolute inset-0 -z-10 block dark:hidden"
|
||||||
<div className="mt-20 rounded border bg-muted p-4 text-muted-foreground">
|
quantity={20}
|
||||||
<p>
|
ease={70}
|
||||||
<strong>Linux user?</strong><br/>
|
size={0.05}
|
||||||
We{"'"}ve recently added support for flatpak! You can download the flatpak version by running the following command:
|
staticity={70}
|
||||||
<pre className="bg-white dark:bg-black p-2 rounded mt-4 overflow-auto">flatpak install flathub io.github.zen_browser.zen</pre>
|
color="#000000"
|
||||||
</p>
|
/>
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +71,8 @@ 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. 🌟" />*/}
|
||||||
|
<BlurFade delay={0.35} inView>
|
||||||
<Feature
|
<Feature
|
||||||
title="Split Views"
|
title="Split Views"
|
||||||
description="View multiple tabs at once. Divide your screen into multiple views and browse multiple websites at the same time."
|
description="View multiple tabs at once. Divide your screen into multiple views and browse multiple websites at the same time."
|
||||||
@@ -79,6 +81,8 @@ export default function Features() {
|
|||||||
transform: "translate(-50%, -50%)"
|
transform: "translate(-50%, -50%)"
|
||||||
}} />
|
}} />
|
||||||
</Feature>
|
</Feature>
|
||||||
|
</BlurFade>
|
||||||
|
<BlurFade delay={0.35} inView>
|
||||||
<Feature
|
<Feature
|
||||||
title="Sidebar"
|
title="Sidebar"
|
||||||
description="Access websites with ease. The sidebar allows you to quickly access your favorite websites without disrupting your browsing experience."
|
description="Access websites with ease. The sidebar allows you to quickly access your favorite websites without disrupting your browsing experience."
|
||||||
@@ -87,6 +91,8 @@ export default function Features() {
|
|||||||
transform: "translate(-50%, -50%)"
|
transform: "translate(-50%, -50%)"
|
||||||
}} />
|
}} />
|
||||||
</Feature>
|
</Feature>
|
||||||
|
</BlurFade>
|
||||||
|
<BlurFade delay={0.35} inView>
|
||||||
<Feature
|
<Feature
|
||||||
title="Profiles"
|
title="Profiles"
|
||||||
description="Switch between profiles with ease. Create multiple profiles to keep your work and personal browsing separate."
|
description="Switch between profiles with ease. Create multiple profiles to keep your work and personal browsing separate."
|
||||||
@@ -95,6 +101,7 @@ export default function Features() {
|
|||||||
<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={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" />
|
<ProfileImage enter={profile3Enter} src="/profile-3.png" alt="Profiles" id="profile-3" className="absolute left-1/2 w-3/4 top-1/2" />
|
||||||
</Feature>
|
</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>
|
||||||
|
|||||||
@@ -1,42 +1,25 @@
|
|||||||
|
'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() {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const inView = useInView(ref, { once: true, margin: '-100px' })
|
||||||
return (
|
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">
|
|
||||||
<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">
|
<a href="/download">
|
||||||
<AnimatedGradientText>
|
<AnimatedGradientText>
|
||||||
🎉
|
🎉
|
||||||
@@ -53,31 +36,68 @@ export default function Header() {
|
|||||||
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
||||||
</AnimatedGradientText>
|
</AnimatedGradientText>
|
||||||
</a>
|
</a>
|
||||||
|
<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>
|
||||||
<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>
|
</div>
|
||||||
</HeaderElement>
|
</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"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/components/ui/blur-fade.tsx
Normal file
60
src/components/ui/blur-fade.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
49
src/components/ui/border-beam.tsx
Normal file
49
src/components/ui/border-beam.tsx
Normal 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,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
120
src/components/ui/confetti.tsx
Normal file
120
src/components/ui/confetti.tsx
Normal 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
|
||||||
271
src/components/ui/particles.tsx
Normal file
271
src/components/ui/particles.tsx
Normal 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
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -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: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['var(--font-sans)', ...fontFamily.sans],
|
||||||
|
},
|
||||||
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%": {
|
|
||||||
"background-position": "0% 0%",
|
|
||||||
},
|
},
|
||||||
"50%": {
|
'image-glow': {
|
||||||
"background-position": "100% 100%",
|
'0%': {
|
||||||
|
'opacity': '0',
|
||||||
|
'animation-timing-function': 'cubic-bezier(0.74, 0.25, 0.76, 1)',
|
||||||
},
|
},
|
||||||
to: {
|
'10%': {
|
||||||
"background-position": "0% 0%",
|
'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: {
|
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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user