Refactor Prettier configuration to adhere to code style guidelines
This commit is contained in:
12
.prettierrc.json
Normal file
12
.prettierrc.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"bracketSameLine": true,
|
||||
"endOfLine": "lf",
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"jsxSingleQuote": false,
|
||||
"semi": true,
|
||||
"printWidth": 128,
|
||||
"plugins": []
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')
|
||||
const { PHASE_DEVELOPMENT_SERVER } = require("next/constants");
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = (phase, { defaultConfig }) => {
|
||||
@@ -13,11 +12,11 @@ const nextConfig = (phase, { defaultConfig }) => {
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "cdn.jsdelivr.net",
|
||||
port: '',
|
||||
pathname: '/gh/zen-browser/**',
|
||||
}
|
||||
port: "",
|
||||
pathname: "/gh/zen-browser/**",
|
||||
},
|
||||
],
|
||||
domains: ['localhost', 'cdn.jsdelivr.net', "raw.githubusercontent.com"], // Allow images from jsDelivr
|
||||
domains: ["localhost", "cdn.jsdelivr.net", "raw.githubusercontent.com"], // Allow images from jsDelivr
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
@@ -38,7 +37,7 @@ const nextConfig = (phase, { defaultConfig }) => {
|
||||
return {
|
||||
...defaultConfigWWW,
|
||||
// production only config options here
|
||||
output: 'export',
|
||||
output: "export",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
10457
package-lock.json
generated
10457
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function WhyAreYouEvenHere() {
|
||||
|
||||
@@ -3,13 +3,16 @@ import Footer from "@/components/footer";
|
||||
import { Navigation } from "@/components/navigation";
|
||||
import { releaseNoteIsAlpha, releaseNotes } from "@/lib/release-notes";
|
||||
import Link from "next/link";
|
||||
import Markdown from 'react-markdown'
|
||||
import '../privacy-policy/markdown.css';
|
||||
import Markdown from "react-markdown";
|
||||
import "../privacy-policy/markdown.css";
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||
<div id="policy" className="min-h-screen py-42 flex mx-auto my-52 p-10 lg:p-0 w-full lg:w-1/3 flex-col">
|
||||
<div
|
||||
id="policy"
|
||||
className="py-42 mx-auto my-52 flex min-h-screen w-full flex-col p-10 lg:w-1/3 lg:p-0"
|
||||
>
|
||||
<Markdown>
|
||||
{`
|
||||
# Main Developer Team
|
||||
@@ -36,5 +39,5 @@ export default function PrivacyPolicy() {
|
||||
</Markdown>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import DownloadPage from "@/components/download";
|
||||
import Footer from "@/components/footer";
|
||||
import { Navigation } from "@/components/navigation";
|
||||
|
||||
@@ -15,9 +15,8 @@ const RSS_ENTRY_LIMIT = 20;
|
||||
*/
|
||||
export async function GET() {
|
||||
// Just in case the release notes array is empty for whatever reason.
|
||||
const latestDate = releaseNotes.length > 0
|
||||
? formatRssDate(releaseNotes[0].date)
|
||||
: new Date();
|
||||
const latestDate =
|
||||
releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date) : new Date();
|
||||
|
||||
const feed = new Feed({
|
||||
id: "https://www.zen-browser.app/release-notes",
|
||||
@@ -43,7 +42,7 @@ export async function GET() {
|
||||
|
||||
return new Response(feed.rss2(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
"Content-Type": "application/xml; charset=utf-8",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -73,37 +72,38 @@ function formatRssDate(dateStr: string) {
|
||||
* @returns The formatted release note as a HTML string.
|
||||
*/
|
||||
function formatReleaseNote(releaseNote: ReleaseNote) {
|
||||
let content = "<p>If you encounter any issues, please report them on <a href=\"https://github.com/zen-browser/desktop/issues/\">the issues page</a>. Thanks everyone for your feedback! ❤️</p>";
|
||||
let content =
|
||||
'<p>If you encounter any issues, please report them on <a href="https://github.com/zen-browser/desktop/issues/">the issues page</a>. Thanks everyone for your feedback! ❤️</p>';
|
||||
|
||||
if (releaseNote.extra) {
|
||||
content += `<p>${releaseNote.extra.replace(/(\n)/g, "<br />")}</p>`
|
||||
content += `<p>${releaseNote.extra.replace(/(\n)/g, "<br />")}</p>`;
|
||||
}
|
||||
|
||||
if (releaseNote.breakingChanges) {
|
||||
content += `<h2>⚠️ Breaking changes</h2>`
|
||||
content += `<ul>`
|
||||
content += `<h2>⚠️ Breaking changes</h2>`;
|
||||
content += `<ul>`;
|
||||
for (const breakingChange of releaseNote.breakingChanges) {
|
||||
content += `<li>${breakingChange}</li>`
|
||||
content += `<li>${breakingChange}</li>`;
|
||||
}
|
||||
content += `</ul>`
|
||||
content += `</ul>`;
|
||||
}
|
||||
|
||||
if (releaseNote.features) {
|
||||
content += `<h2>⭐ Features</h2>`
|
||||
content += `<ul>`
|
||||
content += `<h2>⭐ Features</h2>`;
|
||||
content += `<ul>`;
|
||||
for (const feature of releaseNote.features) {
|
||||
content += `<li>${feature}</li>`
|
||||
content += `<li>${feature}</li>`;
|
||||
}
|
||||
content += `</ul>`
|
||||
content += `</ul>`;
|
||||
}
|
||||
|
||||
if (releaseNote.fixes) {
|
||||
content += `<h2>✓ Fixes</h2>`
|
||||
content += `<ul>`
|
||||
content += `<h2>✓ Fixes</h2>`;
|
||||
content += `<ul>`;
|
||||
for (const fix of releaseNote.fixes) {
|
||||
content += `<li>${fix.description}</li>`
|
||||
content += `<li>${fix.description}</li>`;
|
||||
}
|
||||
content += `</ul>`
|
||||
content += `</ul>`;
|
||||
}
|
||||
|
||||
return content;
|
||||
|
||||
@@ -19,20 +19,24 @@ export default async function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
|
||||
return (
|
||||
<html suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="me" href="https://fosstodon.org/@zenbrowser"></link>
|
||||
<script defer data-domain="zen-browser.app" src="https://plausible.io/js/script.js"></script>
|
||||
<link rel="alternate" type="application/rss+xml" title="Zen Browser Release Notes" href="https://www.zen-browser.app/feed.xml" />
|
||||
<script
|
||||
defer
|
||||
data-domain="zen-browser.app"
|
||||
src="https://plausible.io/js/script.js"
|
||||
></script>
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title="Zen Browser Release Notes"
|
||||
href="https://www.zen-browser.app/feed.xml"
|
||||
/>
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ThemeProvider attribute="class" enableSystem disableTransitionOnChange>
|
||||
<StyledComponentsRegistry>
|
||||
<div>
|
||||
{children}
|
||||
|
||||
@@ -3,17 +3,17 @@ import { HomeIcon } from "@radix-ui/react-icons";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<main className="min-h-screen grid place-items-center">
|
||||
<div className="flex flex-col justify-center items-center text-center">
|
||||
<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">
|
||||
<main className="grid min-h-screen place-items-center">
|
||||
<div className="flex flex-col items-center justify-center text-center">
|
||||
<h1 className="-translate-y-4 animate-fade-in 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] dark:from-white dark:to-white/40 sm:text-6xl md:text-7xl lg:text-8xl">
|
||||
Page Not Found!
|
||||
</h1>
|
||||
<a href="/"><Button
|
||||
className="flex items-center justify-center animate-fade-in -translate-y-4 gap-1 text-white opacity-0 ease-in-out [--animation-delay:600ms] font-medium dark:text-black"
|
||||
>
|
||||
<a href="/">
|
||||
<Button className="flex -translate-y-4 animate-fade-in items-center justify-center gap-1 font-medium text-white opacity-0 ease-in-out [--animation-delay:600ms] dark:text-black">
|
||||
<span>Back to Home</span>
|
||||
<HomeIcon className="ml-1 size-4 transition-transform duration-300 ease-in-out group-hover:translate-x-1" />
|
||||
</Button></a>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,9 @@ import { Navigation } from "@/components/navigation";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="flex min-h-screen overflow-x-hidden flex-col items-center justify-start">
|
||||
<main className="flex min-h-screen flex-col items-center justify-start overflow-x-hidden">
|
||||
<Header />
|
||||
<Features />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
margin-top: 4em;
|
||||
}
|
||||
|
||||
|
||||
#policy h1:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,16 @@ import Footer from "@/components/footer";
|
||||
import { Navigation } from "@/components/navigation";
|
||||
import { releaseNoteIsAlpha, releaseNotes } from "@/lib/release-notes";
|
||||
import Link from "next/link";
|
||||
import Markdown from 'react-markdown'
|
||||
import './markdown.css';
|
||||
import Markdown from "react-markdown";
|
||||
import "./markdown.css";
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||
<div id="policy" className="min-h-screen py-42 flex mx-auto my-52 p-10 lg:p-0 w-full lg:w-1/3 flex-col">
|
||||
<div
|
||||
id="policy"
|
||||
className="py-42 mx-auto my-52 flex min-h-screen w-full flex-col p-10 lg:w-1/3 lg:p-0"
|
||||
>
|
||||
<Markdown>
|
||||
{`
|
||||
# Privacy Policy
|
||||
@@ -92,5 +95,5 @@ By using Zen Browser, you agree to this Privacy Policy. Remember, with Zen, your
|
||||
</Markdown>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import Footer from "@/components/footer";
|
||||
import { Navigation } from "@/components/navigation";
|
||||
import ReleaseNote from "@/components/release-note";
|
||||
@@ -7,27 +6,36 @@ import { Button } from "@/components/ui/button";
|
||||
import { releaseNotes } from "@/lib/release-notes";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, ChevronDown } from "lucide-react";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return [{version: "latest"}, ...releaseNotes.map((note) => ({ version: note.version }))];
|
||||
return [
|
||||
{ version: "latest" },
|
||||
...releaseNotes.map((note) => ({ version: note.version })),
|
||||
];
|
||||
}
|
||||
|
||||
export default function ReleaseNotePage({ params }: { params: { version: string } }) {
|
||||
export default function ReleaseNotePage({
|
||||
params,
|
||||
}: {
|
||||
params: { version: string };
|
||||
}) {
|
||||
const { version } = params;
|
||||
|
||||
if (version === "latest") {
|
||||
return redirect(`/release-notes/${releaseNotes[0].version}`);
|
||||
}
|
||||
|
||||
const currentIndex = releaseNotes.findIndex((note) => note.version === version);
|
||||
const currentIndex = releaseNotes.findIndex(
|
||||
(note) => note.version === version,
|
||||
);
|
||||
const releaseNote = releaseNotes[currentIndex];
|
||||
|
||||
if (!releaseNote) {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center">
|
||||
<div className="h-screen flex flex-wrap items-center justify-center">
|
||||
<h1 className="text-4xl font-bold mt-12">Release note not found</h1>
|
||||
<div className="flex h-screen flex-wrap items-center justify-center">
|
||||
<h1 className="mt-12 text-4xl font-bold">Release note not found</h1>
|
||||
<a href="/release-notes">
|
||||
<Button className="mt-4 items-center justify-center">
|
||||
Back to release notes
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import ReleaseNoteElement from "@/components/release-note";
|
||||
import { releaseNotes } from "@/lib/release-notes";
|
||||
import { Metadata } from "next";
|
||||
@@ -6,21 +5,43 @@ import { Metadata } from "next";
|
||||
export const metadata: Metadata = {
|
||||
title: "Release Notes",
|
||||
description: "Stay up to date with the latest changes to Zen Browser",
|
||||
keywords: ["Zen", "Browser", "Zen Browser", "Web", "Internet", "Fast", "Release", "Notes"],
|
||||
keywords: [
|
||||
"Zen",
|
||||
"Browser",
|
||||
"Zen Browser",
|
||||
"Web",
|
||||
"Internet",
|
||||
"Fast",
|
||||
"Release",
|
||||
"Notes",
|
||||
],
|
||||
};
|
||||
|
||||
export default function ReleaseNotes() {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||
<div className="min-h-screen py-42 flex justify-center flex-col px-10 lg:px-0 lg:w-4/5 xl:w-3/5">
|
||||
<h1 className="text-4xl font-bold mt-48">Release Notes</h1>
|
||||
<div className="py-42 flex min-h-screen flex-col justify-center px-10 lg:w-4/5 lg:px-0 xl:w-3/5">
|
||||
<h1 className="mt-48 text-4xl font-bold">Release Notes</h1>
|
||||
<p className="mt-8 text-lg text-muted-foreground">
|
||||
Stay up to date with the latest changes to Zen Browser! Since the <a className="text-blue-500" href="#1.0.0-a.1">first release</a> till <a className="text-blue-500" href={`/release-notes/${releaseNotes[0].version}`}>{releaseNotes[0].version}</a>, we've been working hard to make Zen Browser the best it can be.<br /><br /> Thanks everyone for your feedback! ❤️
|
||||
Stay up to date with the latest changes to Zen Browser! Since the{" "}
|
||||
<a className="text-blue-500" href="#1.0.0-a.1">
|
||||
first release
|
||||
</a>{" "}
|
||||
till{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
href={`/release-notes/${releaseNotes[0].version}`}
|
||||
>
|
||||
{releaseNotes[0].version}
|
||||
</a>
|
||||
, we've been working hard to make Zen Browser the best it can be.
|
||||
<br />
|
||||
<br /> Thanks everyone for your feedback! ❤️
|
||||
</p>
|
||||
{releaseNotes.map((releaseNote) => (
|
||||
<ReleaseNoteElement key={releaseNote.version} data={releaseNote} />
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import Footer from "@/components/footer";
|
||||
import { Navigation } from "@/components/navigation";
|
||||
import ThemePage from "@/components/theme-page";
|
||||
@@ -7,9 +6,9 @@ import { Metadata, ResolvingMetadata } from "next";
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params, searchParams }: any,
|
||||
parent: ResolvingMetadata
|
||||
parent: ResolvingMetadata,
|
||||
): Promise<Metadata> {
|
||||
const theme = params.theme
|
||||
const theme = params.theme;
|
||||
const themeData = await getThemeFromId(theme);
|
||||
if (!themeData) {
|
||||
return {
|
||||
@@ -44,7 +43,11 @@ export async function generateStaticParams() {
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function ThemeInfoPage({ params }: { params: { theme: string } }) {
|
||||
export default async function ThemeInfoPage({
|
||||
params,
|
||||
}: {
|
||||
params: { theme: string };
|
||||
}) {
|
||||
const { theme } = params;
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import Footer from "@/components/footer";
|
||||
import MarketplacePage from "@/components/marketplace";
|
||||
import { Navigation } from "@/components/navigation";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import WelcomePage from "@/components/welcome";
|
||||
import Footer from "@/components/footer";
|
||||
import { Navigation } from "@/components/navigation";
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
|
||||
import { LOGO_COLORS } from "@/lib/logos";
|
||||
|
||||
export function BrandingAssets() {
|
||||
return (
|
||||
<div className="flex flex-col w-full mx-auto p-5 lg:w-1/2 lg:p-0 items-center justify-center h-full mt-36">
|
||||
<div className="mx-auto mt-36 flex h-full w-full flex-col items-center justify-center p-5 lg:w-1/2 lg:p-0">
|
||||
<div className="mx-auto w-full text-center">
|
||||
<h1 className="text-4xl lg:text-7xl font-bold">Branding Assets</h1>
|
||||
<p className="text-muted-foreground mt-2">Download Zen Browser branding assets for your website or project.</p>
|
||||
<h1 className="text-4xl font-bold lg:text-7xl">Branding Assets</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Download Zen Browser branding assets for your website or project.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full lg:w-2/3 flex-col mt-10">
|
||||
<h2 className="text-2xl font-bold mt-10">Logos</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
<div className="mt-10 flex w-full flex-col lg:w-2/3">
|
||||
<h2 className="mt-10 text-2xl font-bold">Logos</h2>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Download the Zen Browser logo in different colors.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-10 mt-10 w-full">
|
||||
<div className="mt-10 grid w-full grid-cols-2 gap-10 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{LOGO_COLORS.map((color) => (
|
||||
<div key={color} className="flex flex-col items-center">
|
||||
<img src={`https://cdn.jsdelivr.net/gh/zen-browser/www/public/logos/zen-${color}.svg`} alt={`Zen Browser ${color} logo`} className="w-24 h-24 mt-4" />
|
||||
<div className="flex items-center my-2">
|
||||
<img
|
||||
src={`https://cdn.jsdelivr.net/gh/zen-browser/www/public/logos/zen-${color}.svg`}
|
||||
alt={`Zen Browser ${color} logo`}
|
||||
className="mt-4 h-24 w-24"
|
||||
/>
|
||||
<div className="my-2 flex items-center">
|
||||
<a
|
||||
href={`/logos/zen-${color}.svg`}
|
||||
download={`zen-${color}.svg`}
|
||||
className="text-blue-500 text-md ml-2"
|
||||
className="text-md ml-2 text-blue-500"
|
||||
>
|
||||
{color}
|
||||
</a>
|
||||
@@ -30,20 +35,25 @@ export function BrandingAssets() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full lg:w-2/3 flex-col mt-10">
|
||||
<h2 className="text-2xl font-bold mt-10">Empty Logos</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Download the Zen Browser logo in different colors without a filled Zen letter.
|
||||
<div className="mt-10 flex w-full flex-col lg:w-2/3">
|
||||
<h2 className="mt-10 text-2xl font-bold">Empty Logos</h2>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Download the Zen Browser logo in different colors without a filled Zen
|
||||
letter.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-10 mt-10 w-full">
|
||||
<div className="mt-10 grid w-full grid-cols-2 gap-10 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{LOGO_COLORS.map((color) => (
|
||||
<div key={color} className="flex flex-col items-center">
|
||||
<img src={`https://cdn.jsdelivr.net/gh/zen-browser/www/public/logos/zen-alpha-${color}.svg`} alt={`Zen Browser ${color} logo`} className="w-24 h-24 mt-4" />
|
||||
<div className="flex items-center my-2">
|
||||
<img
|
||||
src={`https://cdn.jsdelivr.net/gh/zen-browser/www/public/logos/zen-alpha-${color}.svg`}
|
||||
alt={`Zen Browser ${color} logo`}
|
||||
className="mt-4 h-24 w-24"
|
||||
/>
|
||||
<div className="my-2 flex items-center">
|
||||
<a
|
||||
href={`/logos/zen-alpha-${color}.svg`}
|
||||
download={`zen-alpha-${color}.svg`}
|
||||
className="text-blue-500 text-md ml-2"
|
||||
className="text-md ml-2 text-blue-500"
|
||||
>
|
||||
{color}
|
||||
</a>
|
||||
@@ -54,7 +64,7 @@ export function BrandingAssets() {
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<h2 className="text-2xl font-bold">License</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
All branding assets are licensed under the{" "}
|
||||
<a
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/"
|
||||
@@ -64,12 +74,18 @@ export function BrandingAssets() {
|
||||
>
|
||||
CC BY-SA 4.0
|
||||
</a>
|
||||
. Thanks to <a href="https://www.onnno.nl/" className="text-blue-500">Donno (mr. Logos)</a> for the assets.
|
||||
. Thanks to{" "}
|
||||
<a href="https://www.onnno.nl/" className="text-blue-500">
|
||||
Donno (mr. Logos)
|
||||
</a>{" "}
|
||||
for the assets.
|
||||
<br />
|
||||
These logos however shall not be modified in a way that suggests the licensor endorses you or your use.
|
||||
These logos however shall not be modified in a way that suggests the
|
||||
licensor endorses you or your use.
|
||||
<br />
|
||||
<br />
|
||||
You are free to share and adapt the assets for any purpose, even commercially.
|
||||
You are free to share and adapt the assets for any purpose, even
|
||||
commercially.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
35
src/components/cool-header-text.tsx
Normal file
35
src/components/cool-header-text.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import styled, { keyframes } from "styled-components";
|
||||
|
||||
const hueShift = keyframes`
|
||||
0% {
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
filter: hue-rotate(170deg);
|
||||
}
|
||||
100% {
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const TextTitle = styled.h1`
|
||||
background-clip: text;
|
||||
background-image: linear-gradient(90deg, #0077e7, #01d8d1);
|
||||
filter: hue-rotate(0deg);
|
||||
animation: ${hueShift} 10s infinite linear 1s;
|
||||
`;
|
||||
|
||||
export default function CoolHeaderText() {
|
||||
return (
|
||||
<>
|
||||
<div className="relative font-extrabold mt-5 mb-5 -translate-y-4 animate-fade-in 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] dark:from-white dark:to-white/40 sm:text-6xl md:text-7xl lg:text-8xl">
|
||||
<TextTitle>
|
||||
Beautiful. Fast. Private.<br />Your Browser, Your Way.
|
||||
</TextTitle>
|
||||
</div>
|
||||
<div className="absolute top-[-5px] right-[-20px] transform shadow rotate-[15deg] rounded-full mt-12 pointer-events-none hidden md:block bg-blue-500 px-3 py-1 w-fit h-fit">
|
||||
Alpha Version
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,25 +4,33 @@ import { ny } from "@/lib/utils";
|
||||
import { Button } from "./ui/button";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from "@radix-ui/react-dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@radix-ui/react-dialog";
|
||||
import { DialogFooter, DialogHeader } from "./ui/dialog";
|
||||
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "./ui/sheet";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "./ui/sheet";
|
||||
|
||||
export const COLORS = [
|
||||
"#ffaa40",
|
||||
"#9c40ff",
|
||||
"#ff40aa",
|
||||
"#40ffaa",
|
||||
"#40aaff",
|
||||
];
|
||||
export const COLORS = ["#ffaa40", "#9c40ff", "#ff40aa", "#40ffaa", "#40aaff"];
|
||||
|
||||
const ThemeFormWrapper = styled.div<{
|
||||
primaryColor: string,
|
||||
accentColor: string,
|
||||
secondaryColor: string,
|
||||
tertiaryColor: string,
|
||||
colorsBorder: string,
|
||||
dialogBg: string,
|
||||
primaryColor: string;
|
||||
accentColor: string;
|
||||
secondaryColor: string;
|
||||
tertiaryColor: string;
|
||||
colorsBorder: string;
|
||||
dialogBg: string;
|
||||
}>`
|
||||
${({
|
||||
primaryColor,
|
||||
@@ -71,24 +79,50 @@ const defaultStyles = {
|
||||
light: "var(--zen-colors-tertiary)",
|
||||
dark: "color-mix(in srgb, var(--zen-primary-color) 10%, black 90%)",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default function CreateThemePage() {
|
||||
const [selectedColor, setSelectedColor] = React.useState(COLORS[0]);
|
||||
const [isDarkMode, setIsDarkMode] = React.useState(false);
|
||||
|
||||
const [primaryColor, setPrimaryColor] = React.useState(defaultStyles.primaryColor.dark);
|
||||
const [secondaryColor, setSecondaryColor] = React.useState(defaultStyles.secondaryColor.dark);
|
||||
const [tertiaryColor, setTertiaryColor] = React.useState(defaultStyles.tertiaryColor.dark);
|
||||
const [colorsBorder, setColorsBorder] = React.useState(defaultStyles.colorsBorder.dark);
|
||||
const [primaryColor, setPrimaryColor] = React.useState(
|
||||
defaultStyles.primaryColor.dark,
|
||||
);
|
||||
const [secondaryColor, setSecondaryColor] = React.useState(
|
||||
defaultStyles.secondaryColor.dark,
|
||||
);
|
||||
const [tertiaryColor, setTertiaryColor] = React.useState(
|
||||
defaultStyles.tertiaryColor.dark,
|
||||
);
|
||||
const [colorsBorder, setColorsBorder] = React.useState(
|
||||
defaultStyles.colorsBorder.dark,
|
||||
);
|
||||
const [dialogBg, setDialogBg] = React.useState(defaultStyles.dialogBg.dark);
|
||||
|
||||
React.useEffect(() => {
|
||||
setPrimaryColor(isDarkMode ? defaultStyles.primaryColor.dark : defaultStyles.primaryColor.light);
|
||||
setSecondaryColor(isDarkMode ? defaultStyles.secondaryColor.dark : defaultStyles.secondaryColor.light);
|
||||
setTertiaryColor(isDarkMode ? defaultStyles.tertiaryColor.dark : defaultStyles.tertiaryColor.light);
|
||||
setColorsBorder(isDarkMode ? defaultStyles.colorsBorder.dark : defaultStyles.colorsBorder.light);
|
||||
setDialogBg(isDarkMode ? defaultStyles.dialogBg.dark : defaultStyles.dialogBg.light);
|
||||
setPrimaryColor(
|
||||
isDarkMode
|
||||
? defaultStyles.primaryColor.dark
|
||||
: defaultStyles.primaryColor.light,
|
||||
);
|
||||
setSecondaryColor(
|
||||
isDarkMode
|
||||
? defaultStyles.secondaryColor.dark
|
||||
: defaultStyles.secondaryColor.light,
|
||||
);
|
||||
setTertiaryColor(
|
||||
isDarkMode
|
||||
? defaultStyles.tertiaryColor.dark
|
||||
: defaultStyles.tertiaryColor.light,
|
||||
);
|
||||
setColorsBorder(
|
||||
isDarkMode
|
||||
? defaultStyles.colorsBorder.dark
|
||||
: defaultStyles.colorsBorder.light,
|
||||
);
|
||||
setDialogBg(
|
||||
isDarkMode ? defaultStyles.dialogBg.dark : defaultStyles.dialogBg.light,
|
||||
);
|
||||
}, [isDarkMode]);
|
||||
|
||||
const generateThemeData = () => {
|
||||
@@ -96,26 +130,49 @@ export default function CreateThemePage() {
|
||||
isDarkMode,
|
||||
};
|
||||
// Dont add the default values
|
||||
if (primaryColor !== (isDarkMode ? defaultStyles.primaryColor.dark : defaultStyles.primaryColor.light)) {
|
||||
if (
|
||||
primaryColor !==
|
||||
(isDarkMode
|
||||
? defaultStyles.primaryColor.dark
|
||||
: defaultStyles.primaryColor.light)
|
||||
) {
|
||||
theme["primaryColor"] = primaryColor;
|
||||
}
|
||||
if (secondaryColor !== (isDarkMode ? defaultStyles.secondaryColor.dark : defaultStyles.secondaryColor.light)) {
|
||||
if (
|
||||
secondaryColor !==
|
||||
(isDarkMode
|
||||
? defaultStyles.secondaryColor.dark
|
||||
: defaultStyles.secondaryColor.light)
|
||||
) {
|
||||
theme["secondaryColor"] = secondaryColor;
|
||||
}
|
||||
if (tertiaryColor !== (isDarkMode ? defaultStyles.tertiaryColor.dark : defaultStyles.tertiaryColor.light)) {
|
||||
if (
|
||||
tertiaryColor !==
|
||||
(isDarkMode
|
||||
? defaultStyles.tertiaryColor.dark
|
||||
: defaultStyles.tertiaryColor.light)
|
||||
) {
|
||||
theme["tertiaryColor"] = tertiaryColor;
|
||||
}
|
||||
if (colorsBorder !== (isDarkMode ? defaultStyles.colorsBorder.dark : defaultStyles.colorsBorder.light)) {
|
||||
theme["colorsBorder"] = colorsBorder
|
||||
if (
|
||||
colorsBorder !==
|
||||
(isDarkMode
|
||||
? defaultStyles.colorsBorder.dark
|
||||
: defaultStyles.colorsBorder.light)
|
||||
) {
|
||||
theme["colorsBorder"] = colorsBorder;
|
||||
}
|
||||
if (dialogBg !== (isDarkMode ? defaultStyles.dialogBg.dark : defaultStyles.dialogBg.light)) {
|
||||
theme["dialogBg"] = dialogBg
|
||||
if (
|
||||
dialogBg !==
|
||||
(isDarkMode ? defaultStyles.dialogBg.dark : defaultStyles.dialogBg.light)
|
||||
) {
|
||||
theme["dialogBg"] = dialogBg;
|
||||
}
|
||||
if (COLORS.indexOf(selectedColor) !== 0) {
|
||||
theme["accentColor"] = selectedColor;
|
||||
}
|
||||
return JSON.stringify(theme, null, 4);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeFormWrapper
|
||||
@@ -125,92 +182,154 @@ export default function CreateThemePage() {
|
||||
tertiaryColor={tertiaryColor}
|
||||
colorsBorder={colorsBorder}
|
||||
dialogBg={dialogBg}
|
||||
className="flex flex-col mt-32 items-center justify-center w-full min-h-screen">
|
||||
<div className="w-full lg:w-1/2 xl:w-1/2 mx-auto px-2 lg:px-none">
|
||||
<h1 className="text-4xl lg:text-7xl font-bold">Create your theme</h1>
|
||||
<p className="text-lg opacity-40 mt-2">Create your own theme for Zen Browser and share it with the community.</p>
|
||||
<div className="text-xs text-muted-foreground mt-8">
|
||||
If the color is chosen from the palette, the accent color will be set to the user's selection in the preferences. However, if the color is chosen from the color picker, the accent color will be the color selected.
|
||||
className="mt-32 flex min-h-screen w-full flex-col items-center justify-center"
|
||||
>
|
||||
<div className="lg:px-none mx-auto w-full px-2 lg:w-1/2 xl:w-1/2">
|
||||
<h1 className="text-4xl font-bold lg:text-7xl">Create your theme</h1>
|
||||
<p className="mt-2 text-lg opacity-40">
|
||||
Create your own theme for Zen Browser and share it with the community.
|
||||
</p>
|
||||
<div className="mt-8 text-xs text-muted-foreground">
|
||||
If the color is chosen from the palette, the accent color will be set
|
||||
to the user's selection in the preferences. However, if the color is
|
||||
chosen from the color picker, the accent color will be the color
|
||||
selected.
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<div className="mt-2 flex items-center">
|
||||
{COLORS.map((color) => (
|
||||
<div
|
||||
key={color}
|
||||
onClick={() => setSelectedColor(color)}
|
||||
className={ny(`w-6 h-6 mx-2 cursor-pointer rounded-md shadow-sm text-white`, selectedColor === color ? "ring-2 ring-black dark:ring-white" : "")}
|
||||
className={ny(
|
||||
`mx-2 h-6 w-6 cursor-pointer rounded-md text-white shadow-sm`,
|
||||
selectedColor === color
|
||||
? "ring-2 ring-black dark:ring-white"
|
||||
: "",
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
></div>
|
||||
))}
|
||||
<div className="mx-4">
|
||||
or
|
||||
</div>
|
||||
<input type="color" value={selectedColor} onChange={(e) => setSelectedColor(e.target.value)} className="w-9 h-7 rounded cursor-pointer outline-none" />
|
||||
<div className="mx-4">or</div>
|
||||
<input
|
||||
type="color"
|
||||
value={selectedColor}
|
||||
onChange={(e) => setSelectedColor(e.target.value)}
|
||||
className="h-7 w-9 cursor-pointer rounded outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<div className="w-full">
|
||||
<div className="flex items-center mt-10 select-none">
|
||||
<input type="checkbox" className="mr-2" checked={isDarkMode} onChange={(e) => setIsDarkMode(e.target.checked)} id="dark-mode" />
|
||||
<label htmlFor="dark-mode" className="text-md font-bold opacity-60">Dark mode</label>
|
||||
<div className="mt-10 flex select-none items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={isDarkMode}
|
||||
onChange={(e) => setIsDarkMode(e.target.checked)}
|
||||
id="dark-mode"
|
||||
/>
|
||||
<label
|
||||
htmlFor="dark-mode"
|
||||
className="text-md font-bold opacity-60"
|
||||
>
|
||||
Dark mode
|
||||
</label>
|
||||
</div>
|
||||
<h2 className="text-lg mt-8 font-bold opacity-70">
|
||||
Primary color
|
||||
</h2>
|
||||
<div className="flex items-center mt-2">
|
||||
<input type="text" className="border text-gray-500 rounded-lg p-2 w-2/3" value={primaryColor} onChange={(e) => setPrimaryColor(e.target.value)} />
|
||||
<div className="w-11 h-11 ml-4 rounded-lg border bg-[var(--zen-colors-primary)]"></div>
|
||||
<h2 className="mt-8 text-lg font-bold opacity-70">Primary color</h2>
|
||||
<div className="mt-2 flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
className="w-2/3 rounded-lg border p-2 text-gray-500"
|
||||
value={primaryColor}
|
||||
onChange={(e) => setPrimaryColor(e.target.value)}
|
||||
/>
|
||||
<div className="ml-4 h-11 w-11 rounded-lg border bg-[var(--zen-colors-primary)]"></div>
|
||||
</div>
|
||||
<h2 className="text-lg mt-8 font-bold opacity-70">
|
||||
<h2 className="mt-8 text-lg font-bold opacity-70">
|
||||
Secondary color
|
||||
</h2>
|
||||
<div className="flex items-center mt-2">
|
||||
<input type="text" className="border text-gray-500 rounded-lg p-2 w-2/3" value={secondaryColor} onChange={(e) => setSecondaryColor(e.target.value)} />
|
||||
<div className="w-11 h-11 ml-4 rounded-lg border bg-[var(--zen-colors-secondary)]"></div>
|
||||
<div className="mt-2 flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
className="w-2/3 rounded-lg border p-2 text-gray-500"
|
||||
value={secondaryColor}
|
||||
onChange={(e) => setSecondaryColor(e.target.value)}
|
||||
/>
|
||||
<div className="ml-4 h-11 w-11 rounded-lg border bg-[var(--zen-colors-secondary)]"></div>
|
||||
</div>
|
||||
<h2 className="text-lg mt-8 font-bold opacity-70">
|
||||
<h2 className="mt-8 text-lg font-bold opacity-70">
|
||||
Tertiary color
|
||||
</h2>
|
||||
<div className="flex items-center mt-2">
|
||||
<input type="text" className="border text-gray-500 rounded-lg p-2 w-2/3" value={tertiaryColor} onChange={(e) => setTertiaryColor(e.target.value)} />
|
||||
<div className="w-11 h-11 ml-4 rounded-lg border bg-[var(--zen-colors-tertiary)]"></div>
|
||||
<div className="mt-2 flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
className="w-2/3 rounded-lg border p-2 text-gray-500"
|
||||
value={tertiaryColor}
|
||||
onChange={(e) => setTertiaryColor(e.target.value)}
|
||||
/>
|
||||
<div className="ml-4 h-11 w-11 rounded-lg border bg-[var(--zen-colors-tertiary)]"></div>
|
||||
</div>
|
||||
<h2 className="text-lg mt-8 font-bold opacity-70">
|
||||
Border color
|
||||
</h2>
|
||||
<div className="flex items-center mt-2">
|
||||
<input type="text" className="border text-gray-500 rounded-lg p-2 w-2/3" value={colorsBorder} onChange={(e) => setColorsBorder(e.target.value)} />
|
||||
<div className="w-11 h-11 ml-4 rounded-lg border bg-[var(--zen-colors-border)]"></div>
|
||||
<h2 className="mt-8 text-lg font-bold opacity-70">Border color</h2>
|
||||
<div className="mt-2 flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
className="w-2/3 rounded-lg border p-2 text-gray-500"
|
||||
value={colorsBorder}
|
||||
onChange={(e) => setColorsBorder(e.target.value)}
|
||||
/>
|
||||
<div className="ml-4 h-11 w-11 rounded-lg border bg-[var(--zen-colors-border)]"></div>
|
||||
</div>
|
||||
<h2 className="text-lg mt-8 font-bold opacity-70">
|
||||
<h2 className="mt-8 text-lg font-bold opacity-70">
|
||||
Dialog background color
|
||||
</h2>
|
||||
<div className="flex items-center mt-2">
|
||||
<input type="text" className="border text-gray-500 rounded-lg p-2 w-2/3" value={dialogBg} onChange={(e) => setDialogBg(e.target.value)} />
|
||||
<div className="w-11 h-11 ml-4 rounded-lg border bg-[var(--zen-dialog-background)]"></div>
|
||||
<div className="mt-2 flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
className="w-2/3 rounded-lg border p-2 text-gray-500"
|
||||
value={dialogBg}
|
||||
onChange={(e) => setDialogBg(e.target.value)}
|
||||
/>
|
||||
<div className="ml-4 h-11 w-11 rounded-lg border bg-[var(--zen-dialog-background)]"></div>
|
||||
</div>
|
||||
<div className="text-md font-bold text-muted-foreground mt-8">
|
||||
Right now, we aren't taking more color themes for the browser, until we find a way to make it more accessible for everyone. However, you can still create your own theme and share it with the community.
|
||||
<div className="text-md mt-8 font-bold text-muted-foreground">
|
||||
Right now, we aren't taking more color themes for the browser,
|
||||
until we find a way to make it more accessible for everyone.
|
||||
However, you can still create your own theme and share it with the
|
||||
community.
|
||||
</div>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button disabled className="mt-8">Create theme</Button>
|
||||
<Button disabled className="mt-8">
|
||||
Create theme
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="!w-[600px] !max-w-lg">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Theme data</SheetTitle>
|
||||
<SheetDescription>
|
||||
Copy the following JSON object and paste it into your Zen Browser theme format.
|
||||
Copy the following JSON object and paste it into your Zen
|
||||
Browser theme format.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<pre className="text-sm mt-6 text-wrap font-mono p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">{generateThemeData()}</pre>
|
||||
<pre className="mt-6 text-wrap rounded-lg bg-gray-100 p-4 font-mono text-sm dark:bg-gray-800">
|
||||
{generateThemeData()}
|
||||
</pre>
|
||||
<SheetFooter className="mt-4">
|
||||
<Button onClick={() =>
|
||||
<Button
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(generateThemeData())
|
||||
} variant="ghost">
|
||||
}
|
||||
variant="ghost"
|
||||
>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
window.open("https://github.com/zen-browser/theme-store/issues/new?assignees=&labels=new-theme&projects=&template=create-theme.yml&title=%5Bcreate-theme%5D%3A+", "_blank");
|
||||
}} >
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://github.com/zen-browser/theme-store/issues/new?assignees=&labels=new-theme&projects=&template=create-theme.yml&title=%5Bcreate-theme%5D%3A+",
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
>
|
||||
Submit theme
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
@@ -218,9 +337,16 @@ export default function CreateThemePage() {
|
||||
</Sheet>
|
||||
</div>
|
||||
{/* Preview */}
|
||||
<div className="p-4 pr-0 pb-0 rounded-xl border-2 relative overflow-hidden w-72 h-48 border-[var(--zen-colors-border)] bg-[var(--zen-colors-tertiary)]">
|
||||
<div className="border-2 flex items-center justify-center border-[var(--zen-colors-border)] h-full w-full bg-[var(--zen-dialog-background)] rounded-tl-xl p-4 border-b-0 border-r-0">
|
||||
<Button className={ny("bg-[var(--zen-colors-secondary)]", isDarkMode ? "text-white" : "text-black")}>Button</Button>
|
||||
<div className="relative h-48 w-72 overflow-hidden rounded-xl border-2 border-[var(--zen-colors-border)] bg-[var(--zen-colors-tertiary)] p-4 pb-0 pr-0">
|
||||
<div className="flex h-full w-full items-center justify-center rounded-tl-xl border-2 border-b-0 border-r-0 border-[var(--zen-colors-border)] bg-[var(--zen-dialog-background)] p-4">
|
||||
<Button
|
||||
className={ny(
|
||||
"bg-[var(--zen-colors-secondary)]",
|
||||
isDarkMode ? "text-white" : "text-black",
|
||||
)}
|
||||
>
|
||||
Button
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,10 +73,10 @@ export default function DownloadPage() {
|
||||
const [platform, setPlatform] = useState<string | null>(null);
|
||||
const [architecture, setArchitecture] = useState<string | null>(null);
|
||||
const [windowsDownloadType, setWindowsDownloadType] = useState<string | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
const [linuxDownloadType, setLinuxDownloadType] = useState<string | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
|
||||
const [selectedPlatform, setSelectedPlatform] = useState("");
|
||||
@@ -135,7 +135,7 @@ export default function DownloadPage() {
|
||||
let releaseTarget: string;
|
||||
if (selectedLinuxDownloadType === "flatpak") {
|
||||
window.open(
|
||||
"https://dl.flathub.org/repo/appstream/io.github.zen_browser.zen.flatpakref"
|
||||
"https://dl.flathub.org/repo/appstream/io.github.zen_browser.zen.flatpakref",
|
||||
);
|
||||
releaseTarget = "flatpak";
|
||||
} else {
|
||||
@@ -209,16 +209,16 @@ export default function DownloadPage() {
|
||||
href="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css"
|
||||
/>
|
||||
|
||||
<div className="w-full overflow-hidden relative h-screen flex items-center justify-center flex-col lg:flex-row">
|
||||
<div className="flex flex-col justify-center w-full p-10 md:p-20 lg:p-0 lg:w-1/2 2xl:w-1/3 mx-auto">
|
||||
<div className="relative flex h-screen w-full flex-col items-center justify-center overflow-hidden lg:flex-row">
|
||||
<div className="mx-auto flex w-full flex-col justify-center p-10 md:p-20 lg:w-1/2 lg:p-0 2xl:w-1/3">
|
||||
{(hasDownloaded && (
|
||||
<div className="flex items-start mt-20 flex-col">
|
||||
<div className="mt-20 flex flex-col items-start">
|
||||
<h1 className="text-6xl font-bold">Downloaded! ❤️</h1>
|
||||
<p className="text-muted-foreground mt-3">
|
||||
Your download of Zen Browser will begin shortly. Enjoy browsing the
|
||||
web with Zen!
|
||||
<p className="mt-3 text-muted-foreground">
|
||||
Your download of Zen Browser will begin shortly. Enjoy browsing
|
||||
the web with Zen!
|
||||
</p>
|
||||
<div className="flex font-bold mt-5 items-center">
|
||||
<div className="mt-5 flex items-center font-bold">
|
||||
<a href="https://github.com/zen-browser">Source Code</a>
|
||||
<a
|
||||
className="ml-5"
|
||||
@@ -231,17 +231,16 @@ export default function DownloadPage() {
|
||||
</a>
|
||||
</div>
|
||||
{selectedLinuxDownloadType === "appimage" && (
|
||||
<div className="border rounded-md shadow bg-surface mt-10 p-5">
|
||||
<div className="mt-10 rounded-md border bg-surface p-5 shadow">
|
||||
<div className="flex">
|
||||
<InfoIcon className="size-4" />
|
||||
<p className="ml-3 font-bold">
|
||||
AppImage users?
|
||||
</p>
|
||||
<p className="ml-3 font-bold">AppImage users?</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
If you're using an AppImage, you can use the automatic installer, check it out{" "}
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
If you're using an AppImage, you can use the automatic
|
||||
installer, check it out{" "}
|
||||
</p>
|
||||
<pre className="text-muted-foreground bg-background p-2 rounded-md mt-2">
|
||||
<pre className="mt-2 rounded-md bg-background p-2 text-muted-foreground">
|
||||
bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
</pre>
|
||||
</div>
|
||||
@@ -249,9 +248,13 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
</div>
|
||||
)) || (
|
||||
<>
|
||||
<h1 className="text-6xl font-bold flex flex-col lg:flex-row">Download <SparklesText className="mx-2" text="Zen" /></h1>
|
||||
<p className="text-muted-foreground mt-3">
|
||||
We're thrilled for you to experience Zen Browser. First, let us know which device you're using. This will only take a moment, we promise.
|
||||
<h1 className="flex flex-col text-6xl font-bold lg:flex-row">
|
||||
Download <SparklesText className="mx-2" text="Zen" />
|
||||
</h1>
|
||||
<p className="mt-3 text-muted-foreground">
|
||||
We're thrilled for you to experience Zen Browser. First, let us
|
||||
know which device you're using. This will only take a moment, we
|
||||
promise.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@@ -267,8 +270,8 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
<div
|
||||
onClick={() => setSelectedPlatform("Windows")}
|
||||
className={ny(
|
||||
"select-none mr-2 flex flex-col items-center justify-center rounded-lg bg-background cursor-pointer border",
|
||||
selectedPlatform === "Windows" ? "border-blue-400" : ""
|
||||
"mr-2 flex cursor-pointer select-none flex-col items-center justify-center rounded-lg border bg-background",
|
||||
selectedPlatform === "Windows" ? "border-blue-400" : "",
|
||||
)}
|
||||
style={{
|
||||
height: "11.25rem",
|
||||
@@ -276,7 +279,7 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className="devicon-windows8-original p-2 border border-blue-400 rounded-lg"
|
||||
className="devicon-windows8-original rounded-lg border border-blue-400 p-2"
|
||||
style={{ marginBottom: "10px" }}
|
||||
></i>
|
||||
<div className="font-bold">Windows</div>
|
||||
@@ -285,8 +288,8 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
<div
|
||||
onClick={() => setSelectedPlatform("Linux")}
|
||||
className={ny(
|
||||
"select-none mr-2 flex flex-col items-center justify-center rounded-lg bg-background cursor-pointer border",
|
||||
selectedPlatform === "Linux" ? "border-yellow-400" : ""
|
||||
"mr-2 flex cursor-pointer select-none flex-col items-center justify-center rounded-lg border bg-background",
|
||||
selectedPlatform === "Linux" ? "border-yellow-400" : "",
|
||||
)}
|
||||
style={{
|
||||
height: "11.25rem",
|
||||
@@ -294,7 +297,7 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className="devicon-linux-plain p-2 border border-yellow-400 rounded-lg"
|
||||
className="devicon-linux-plain rounded-lg border border-yellow-400 p-2"
|
||||
style={{ marginBottom: "10px" }}
|
||||
></i>
|
||||
<div className="font-bold">Linux</div>
|
||||
@@ -303,8 +306,8 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
<div
|
||||
onClick={() => setSelectedPlatform("MacOS")}
|
||||
className={ny(
|
||||
"select-none flex flex-col items-center justify-center rounded-lg bg-background cursor-pointer border",
|
||||
selectedPlatform === "MacOS" ? "border-purple-400" : ""
|
||||
"flex cursor-pointer select-none flex-col items-center justify-center rounded-lg border bg-background",
|
||||
selectedPlatform === "MacOS" ? "border-purple-400" : "",
|
||||
)}
|
||||
style={{
|
||||
height: "11.25rem",
|
||||
@@ -312,7 +315,7 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className="devicon-apple-original p-2 border border-purple-400 rounded-lg"
|
||||
className="devicon-apple-original rounded-lg border border-purple-400 p-2"
|
||||
style={{ marginBottom: "10px" }}
|
||||
></i>
|
||||
<div className="font-bold">MacOS</div>
|
||||
@@ -343,34 +346,34 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
<div
|
||||
onClick={() => setSelectedArchitecture("specific")}
|
||||
className={ny(
|
||||
"select-none w-full h-full mb-2 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border",
|
||||
"mb-2 flex h-full w-full cursor-pointer select-none flex-col items-center rounded-lg border bg-background p-5",
|
||||
selectedArchitecture === "specific"
|
||||
? "border-blue-400"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">
|
||||
<h1 className="my-2 text-5xl opacity-40 dark:opacity-20">
|
||||
🚀
|
||||
</h1>
|
||||
<h1 className="text-2xl font-semibold my-2">Optimized</h1>
|
||||
<p className="text-muted-foreground mx-auto text-center">
|
||||
<h1 className="my-2 text-2xl font-semibold">Optimized</h1>
|
||||
<p className="mx-auto text-center text-muted-foreground">
|
||||
Blazing fast and compatible with modern devices
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setSelectedArchitecture("generic")}
|
||||
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",
|
||||
"mb-2 ml-10 flex h-full w-full cursor-pointer select-none flex-col items-center rounded-lg border bg-background p-5",
|
||||
selectedArchitecture === "generic"
|
||||
? "border-blue-400"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">
|
||||
<h1 className="my-2 text-5xl opacity-40 dark:opacity-20">
|
||||
👴
|
||||
</h1>
|
||||
<h1 className="text-2xl font-semibold my-2">Generic</h1>
|
||||
<p className="text-muted-foreground mx-auto text-center">
|
||||
<h1 className="my-2 text-2xl font-semibold">Generic</h1>
|
||||
<p className="mx-auto text-center text-muted-foreground">
|
||||
Slow but compatible with older devices.
|
||||
</p>
|
||||
</div>
|
||||
@@ -390,26 +393,34 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
<div
|
||||
onClick={() => setSelectedArchitecture("specific")}
|
||||
className={ny(
|
||||
"select-none w-full h-64 mb-2 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border",
|
||||
selectedArchitecture === "specific" ? "border-blue-400" : ""
|
||||
"mb-2 flex h-64 w-full cursor-pointer select-none flex-col items-center rounded-lg border bg-background p-5",
|
||||
selectedArchitecture === "specific"
|
||||
? "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">
|
||||
<h1 className="my-2 text-5xl opacity-40 dark:opacity-20">
|
||||
🍏
|
||||
</h1>
|
||||
<h1 className="my-2 text-2xl font-semibold">AArch64</h1>
|
||||
<p className="mx-auto text-center text-muted-foreground">
|
||||
64-bit ARM architecture, for Apple's M Series Chips
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setSelectedArchitecture("generic")}
|
||||
className={ny(
|
||||
"select-none w-full h-64 mb-2 ml-10 p-5 flex flex-col items-center rounded-lg bg-background cursor-pointer border",
|
||||
selectedArchitecture === "generic" ? "border-blue-400" : ""
|
||||
"mb-2 ml-10 flex h-64 w-full cursor-pointer select-none flex-col items-center rounded-lg border bg-background p-5",
|
||||
selectedArchitecture === "generic"
|
||||
? "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">
|
||||
<h1 className="my-2 text-5xl font-bold opacity-40 dark:opacity-20">
|
||||
x64
|
||||
</h1>
|
||||
<h1 className="my-2 text-2xl font-semibold">Intel</h1>
|
||||
<p className="mx-auto text-center text-muted-foreground">
|
||||
64-bit Intel architecture, for older Macs
|
||||
</p>
|
||||
</div>
|
||||
@@ -431,34 +442,34 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
<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",
|
||||
"mb-2 flex h-full w-full cursor-pointer select-none flex-col items-center rounded-lg border bg-background p-5",
|
||||
selectedWindowsDownloadType === "installer"
|
||||
? "border-blue-400"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">
|
||||
<h1 className="my-2 text-5xl 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">
|
||||
<h1 className="my-2 text-2xl font-semibold">Installer</h1>
|
||||
<p className="mx-auto text-center text-muted-foreground">
|
||||
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",
|
||||
"mb-2 ml-10 flex h-full w-full cursor-pointer select-none flex-col items-center rounded-lg border bg-background p-5",
|
||||
selectedWindowsDownloadType === "portable"
|
||||
? "border-blue-400"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">
|
||||
<h1 className="my-2 text-5xl 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">
|
||||
<h1 className="my-2 text-2xl font-semibold">Portable</h1>
|
||||
<p className="mx-auto text-center text-muted-foreground">
|
||||
Download Zen as a ZIP file
|
||||
</p>
|
||||
</div>
|
||||
@@ -480,54 +491,54 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
<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",
|
||||
"mb-2 flex h-full w-full cursor-pointer select-none flex-col items-center rounded-lg border bg-background p-5",
|
||||
selectedLinuxDownloadType === "appimage"
|
||||
? "border-blue-400"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">
|
||||
<h1 className="my-2 text-5xl 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">
|
||||
<h1 className="my-2 text-2xl font-semibold">AppImage</h1>
|
||||
<p className="mx-auto text-center text-muted-foreground">
|
||||
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",
|
||||
"mb-2 ml-5 flex h-full w-full cursor-pointer select-none flex-col items-center rounded-lg border bg-background p-5",
|
||||
selectedLinuxDownloadType === "portable"
|
||||
? "border-blue-400"
|
||||
: ""
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">
|
||||
<h1 className="my-2 text-5xl 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">
|
||||
<h1 className="my-2 text-2xl font-semibold">Portable</h1>
|
||||
<p className="mx-auto text-center text-muted-foreground">
|
||||
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",
|
||||
"mb-2 ml-5 flex h-full w-full cursor-pointer select-none flex-col items-center rounded-lg border bg-background p-5",
|
||||
selectedLinuxDownloadType === "flatpak"
|
||||
? "border-blue-400"
|
||||
: "",
|
||||
selectedArchitecture === "specific"
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: ""
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<h1 className="text-5xl my-2 opacity-40 dark:opacity-20">
|
||||
<h1 className="my-2 text-5xl 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">
|
||||
<h1 className="my-2 text-2xl font-semibold">Flatpak</h1>
|
||||
<p className="mx-auto text-center text-muted-foreground">
|
||||
Install Zen from the Flatpak repository.
|
||||
</p>
|
||||
</div>
|
||||
@@ -542,7 +553,7 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
onClick={() => goBackFlow()}
|
||||
className={ny(
|
||||
"opacity-70",
|
||||
platform === null ? "invisible" : ""
|
||||
platform === null ? "invisible" : "",
|
||||
)}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
@@ -561,7 +572,7 @@ bash {"<"}(curl https://updates.zen-browser.app/appimage.sh)
|
||||
{(platform === "Linux" || platform === "Windows") &&
|
||||
flowIndex === 1 && (
|
||||
<div className="mt-5 flex items-center">
|
||||
<InfoCircledIcon className="size-4 mr-2" />
|
||||
<InfoCircledIcon className="mr-2 size-4" />
|
||||
<p className="text-muted-foreground">
|
||||
Confused about which build to choose?{" "}
|
||||
<a
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import Sticky from 'react-sticky-el';
|
||||
"use client";
|
||||
import Sticky from "react-sticky-el";
|
||||
import {
|
||||
BookmarkCheckIcon,
|
||||
CheckIcon,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
SplitSquareVerticalIcon,
|
||||
TableIcon,
|
||||
XIcon,
|
||||
} from 'lucide-react';
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Cross1Icon,
|
||||
EyeClosedIcon,
|
||||
@@ -38,12 +38,12 @@ import {
|
||||
ReloadIcon,
|
||||
SpaceBetweenHorizontallyIcon,
|
||||
UpdateIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
} from "@radix-ui/react-icons";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from 'next/link';
|
||||
import { Button } from './ui/button';
|
||||
import { COLORS } from './create-theme';
|
||||
import { Slider } from './ui/slider';
|
||||
import Link from "next/link";
|
||||
import { Button } from "./ui/button";
|
||||
import { COLORS } from "./create-theme";
|
||||
import { Slider } from "./ui/slider";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -51,263 +51,508 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from './ui/table';
|
||||
import React, { useState } from 'react';
|
||||
import { ny } from '@/lib/utils';
|
||||
import ThemeCard from './theme-card';
|
||||
import { getAllThemes, ZenTheme } from '@/lib/themes';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './ui/accordion';
|
||||
import Logo from './logo';
|
||||
import CachedImage from './CachedImage';
|
||||
import { transform } from 'next/dist/build/swc';
|
||||
} from "./ui/table";
|
||||
import React, { useState } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
import ThemeCard from "./theme-card";
|
||||
import { getAllThemes, ZenTheme } from "@/lib/themes";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./ui/accordion";
|
||||
import Logo from "./logo";
|
||||
import CachedImage from "./CachedImage";
|
||||
import { transform } from "next/dist/build/swc";
|
||||
|
||||
function Checkmark() {
|
||||
return (
|
||||
<CheckIcon className="text-black rounded-full bg-green-500 dark:bg-green-400 p-1 w-7 h-7 flex-none" />
|
||||
<CheckIcon className="h-7 w-7 flex-none rounded-full bg-green-500 p-1 text-black dark:bg-green-400" />
|
||||
);
|
||||
}
|
||||
|
||||
function Cross() {
|
||||
return (
|
||||
<XIcon className="text-black rounded-full bg-red-500 dark:bg-red-400 p-1 w-7 h-7" />
|
||||
<XIcon className="h-7 w-7 rounded-full bg-red-500 p-1 text-black dark:bg-red-400" />
|
||||
);
|
||||
}
|
||||
|
||||
function Question() {
|
||||
return (
|
||||
<QuestionMarkIcon className="text-black rounded-full bg-yellow-500 dark:bg-yellow-400 p-1 w-7 h-7" />
|
||||
<QuestionMarkIcon className="h-7 w-7 rounded-full bg-yellow-500 p-1 text-black dark:bg-yellow-400" />
|
||||
);
|
||||
}
|
||||
|
||||
export default function Features() {
|
||||
const [feature, setFeature] = useState("item-1");
|
||||
return (
|
||||
<section className="flex-col w-full" id="features">
|
||||
<div className='w-full md:w-5/6 lg:w-3/4 flex flex-col lg:flex-row md:rounded-md mx-auto bg-surface mt-16 shadow'>
|
||||
<div className="p-5 lg:p-12 flex-1">
|
||||
<div className="flex p-12 flex-col justify-center">
|
||||
<h3 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Your Browser, your way <PaintBucket className='inline w-10 h-10'></PaintBucket></h3>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>With Zen's Theme Store, you can customize your browsing experience to reflect your unique style and preferences. Choose from a wide array of themes, colors, and layouts to make Zen truly your own, transforming your browser into a personalized digital space.</p>
|
||||
<section className="w-full flex-col" id="features">
|
||||
<div className="mx-auto mt-16 flex w-full flex-col bg-surface shadow md:w-5/6 md:rounded-md lg:w-3/4 lg:flex-row">
|
||||
<div className="flex-1 p-5 lg:p-12">
|
||||
<div className="flex flex-col justify-center p-12">
|
||||
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Your Browser, your way{" "}
|
||||
<PaintBucket className="inline h-10 w-10"></PaintBucket>
|
||||
</h3>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
With Zen's Theme Store, you can customize your browsing experience
|
||||
to reflect your unique style and preferences. Choose from a wide
|
||||
array of themes, colors, and layouts to make Zen truly your own,
|
||||
transforming your browser into a personalized digital space.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<Button className='mt-8' onClick={() => window.open('/themes', '_self')}>View Theme Store</Button>
|
||||
<Button
|
||||
className="mt-8"
|
||||
onClick={() => window.open("/themes", "_self")}
|
||||
>
|
||||
View Theme Store
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="flex p-12 flex-col justify-center">
|
||||
<h3 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Always up to date <UpdateIcon className='inline w-10 h-10'></UpdateIcon></h3>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>Zen Browser is built on top of Firefox, ensuring it always stays up to date with the latest features, security patches, and performance improvements.</p>
|
||||
<div className="flex flex-col justify-center p-12">
|
||||
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Always up to date{" "}
|
||||
<UpdateIcon className="inline h-10 w-10"></UpdateIcon>
|
||||
</h3>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
Zen Browser is built on top of Firefox, ensuring it always stays
|
||||
up to date with the latest features, security patches, and
|
||||
performance improvements.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<Button className='mt-8' onClick={() => window.open('/download', '_self')}>Download Now</Button>
|
||||
<Button
|
||||
className="mt-8"
|
||||
onClick={() => window.open("/download", "_self")}
|
||||
>
|
||||
Download Now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t lg:border-t-0 lg:border-l h-[1px] lg:h-[unset] lg:w-[1px] mx-2"></div>
|
||||
<div className="p-5 lg:p-12 flex-1">
|
||||
<div className="flex p-12 flex-col justify-center">
|
||||
<h3 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Community driven and Open Source <Link1Icon className='inline w-10 h-10'></Link1Icon></h3>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>Zen thrives on the contributions of its vibrant community. As an open-source project, Zen encourages collaboration and innovation, allowing users and developers alike to shape the future of the browser.</p>
|
||||
<div className='relative'>
|
||||
<Button className='mt-8' onClick={() => window.open('https://github.com/zen-browser', '_blank')}>GitHub Page</Button>
|
||||
</div>
|
||||
<div className='w-full mt-14'>
|
||||
<div className='flex items-center'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Firefox Based</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-5'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Fully Open source</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-5'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Automated Releases to ensure security</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-5'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Community driven</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-5'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Constantly improving</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full md:w-5/6 lg:w-3/4 flex flex-col lg:flex-row md:rounded-md mx-auto bg-surface mt-36 shadow'>
|
||||
<div className='p-16 lg:w-1/2 flex flex-col justify-center'>
|
||||
<h1 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Built for simplicity <EyeIcon className='inline w-8 h-8'></EyeIcon></h1>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>Zen Browser is designed to be simple and easy to use. It's built with the user in mind, so you can focus on what matters most.</p>
|
||||
<div className='w-full mt-8'>
|
||||
<div className='flex items-center'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Completely Customizable</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-4'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Vertical Tabs</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-4'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Thoughtful Design</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CachedImage width={1350} height={900} src="www/public/browser-1.png" alt="Zen Browser" className="rounded-md lg:w-1/2 object-cover object-right" />
|
||||
</div>
|
||||
<div className='w-full md:w-5/6 lg:w-3/4 flex flex-col lg:flex-row md:rounded-md mx-auto bg-surface mt-36 shadow'>
|
||||
<CachedImage width={1350} height={900} src="www/public/browser-2.png" alt="Zen Browser" className="rounded-md lg:w-1/2 object-cover object-left" />
|
||||
<div className='p-16 lg:w-1/2 flex flex-col justify-center'>
|
||||
<h1 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Split Views <SplitSquareHorizontal className='inline w-8 h-8'></SplitSquareHorizontal></h1>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>Zen Browser allows you to split your view into multiple panes, so you can work on multiple things at once. It's perfect for multitasking.</p>
|
||||
<div className="relative">
|
||||
<Button className='mt-8' onClick={() => window.open('/download', '_self')}>Download Now</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full md:w-5/6 lg:w-3/4 p-5 lg:p-12 flex flex-col lg:flex-row md:rounded-md mx-auto bg-surface mt-36 shadow'>
|
||||
<div className="flex p-16 lg:w-1/2 flex-col justify-center">
|
||||
<h3 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Better tab management <BookmarkCheckIcon className='inline w-8 h-8'></BookmarkCheckIcon></h3>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>Better tab management helps you stay organized and focused, reducing clutter and enhancing productivity</p>
|
||||
<div className='w-full mt-8'>
|
||||
<div className='flex items-center'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Workspaces</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-4'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Fast profile switcher</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-4'>
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Container Tabs</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-4'>
|
||||
<Question />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Tab Groups (Coming Soon)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t lg:border-t-0 lg:border-l h-[1px] lg:h-[unset] lg:w-[1px] mx-2"></div>
|
||||
<div className="flex p-16 lg:w-1/2 flex-col">
|
||||
<h3 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Security and Privacy is <span className='text-purple-500 font-bold'>important</span> to us <LockClosedIcon className='inline w-8 h-8'></LockClosedIcon></h3>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>
|
||||
Zen is based on Firefox, ensuring that your browsing experience prioritizes security and privacy. With advanced tracking protection and minimal data collection, Zen keeps your online activity safe and secure, giving you peace of mind as you explore the web.
|
||||
<div className="mx-2 h-[1px] border-t lg:h-[unset] lg:w-[1px] lg:border-l lg:border-t-0"></div>
|
||||
<div className="flex-1 p-5 lg:p-12">
|
||||
<div className="flex flex-col justify-center p-12">
|
||||
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Community driven and Open Source{" "}
|
||||
<Link1Icon className="inline h-10 w-10"></Link1Icon>
|
||||
</h3>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
Zen thrives on the contributions of its vibrant community. As an
|
||||
open-source project, Zen encourages collaboration and innovation,
|
||||
allowing users and developers alike to shape the future of the
|
||||
browser.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<Button className='mt-8' variant="ghost" onClick={() => window.open('https://docs.zen-browser.app/security', '_blank')}>Security in Zen <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' /></Button>
|
||||
<Button className='mt-8' variant="ghost" onClick={() => window.open('/privacy-policy', '_blank')}>Your Privacy <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' /></Button>
|
||||
<Button
|
||||
className="mt-8"
|
||||
onClick={() =>
|
||||
window.open("https://github.com/zen-browser", "_blank")
|
||||
}
|
||||
>
|
||||
GitHub Page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full md:w-5/6 lg:w-3/4 flex flex-col lg:flex-row md:rounded-md mx-auto bg-surface mt-36 shadow'>
|
||||
<div className='p-16 lg:w-1/2 flex flex-col justify-center'>
|
||||
<h1 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Sidebar <SidebarIcon className='inline w-8 h-8 ml-1'></SidebarIcon></h1>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>Zen Browser has a built-in sidebar that lets you quickly access your favorite websites, bookmarks, and more. It's the perfect way to stay organized.</p>
|
||||
<div className='w-full mt-8'>
|
||||
<div className='flex items-center'>
|
||||
<div className="mt-14 w-full">
|
||||
<div className="flex items-center">
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Quick Access</p>
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Firefox Based
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-4'>
|
||||
<div className="mt-5 flex items-center">
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Customizable</p>
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Fully Open source
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center mt-4'>
|
||||
<div className="mt-5 flex items-center">
|
||||
<Checkmark />
|
||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Easy to Use</p>
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Automated Releases to ensure security
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Community driven
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Constantly improving
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CachedImage width={1350} height={900} src="www/public/browser-3.png" alt="Zen Browser" className="rounded-md lg:w-1/2 object-cover object-left" />
|
||||
</div>
|
||||
<div className='w-full md:w-5/6 lg:w-3/4 flex flex-col lg:flex-row md:rounded-md mx-auto bg-surface mt-36 shadow'>
|
||||
<CachedImage width={1350} height={900} src="www/public/browser-4.jpg" alt="Zen Browser" className="rounded-md lg:w-1/2 object-cover object-left" />
|
||||
<div className='p-16 lg:w-1/2 flex flex-col justify-center'>
|
||||
<h1 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Introducing Compact Mode <SidebarCloseIcon className='inline w-8 h-8'></SidebarCloseIcon></h1>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>Zen Browser's compact mode gives you more screen real estate by hiding the title bar and tabs. It's perfect for when you need to focus on your work.</p>
|
||||
</div>
|
||||
<div className="mx-auto mt-36 flex w-full flex-col bg-surface shadow md:w-5/6 md:rounded-md lg:w-3/4 lg:flex-row">
|
||||
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||
<h1 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Built for simplicity <EyeIcon className="inline h-8 w-8"></EyeIcon>
|
||||
</h1>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
Zen Browser is designed to be simple and easy to use. It's built
|
||||
with the user in mind, so you can focus on what matters most.
|
||||
</p>
|
||||
<div className="mt-8 w-full">
|
||||
<div className="flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Completely Customizable
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Vertical Tabs
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Thoughtful Design
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CachedImage
|
||||
width={1350}
|
||||
height={900}
|
||||
src="www/public/browser-1.png"
|
||||
alt="Zen Browser"
|
||||
className="rounded-md object-cover object-right lg:w-1/2"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto mt-36 flex w-full flex-col bg-surface shadow md:w-5/6 md:rounded-md lg:w-3/4 lg:flex-row">
|
||||
<CachedImage
|
||||
width={1350}
|
||||
height={900}
|
||||
src="www/public/browser-2.png"
|
||||
alt="Zen Browser"
|
||||
className="rounded-md object-cover object-left lg:w-1/2"
|
||||
/>
|
||||
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||
<h1 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Split Views{" "}
|
||||
<SplitSquareHorizontal className="inline h-8 w-8"></SplitSquareHorizontal>
|
||||
</h1>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
Zen Browser allows you to split your view into multiple panes, so
|
||||
you can work on multiple things at once. It's perfect for
|
||||
multitasking.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<Button className='mt-8' onClick={() => window.open('/download', '_self')}>What are you waiting for?</Button>
|
||||
<Button
|
||||
className="mt-8"
|
||||
onClick={() => window.open("/download", "_self")}
|
||||
>
|
||||
Download Now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full md:w-5/6 lg:w-3/4 flex flex-col lg:flex-row md:rounded-md mx-auto bg-surface mt-36 shadow'>
|
||||
<div className="relative w-full lg:w-1/2 p-5 lg:p-12 flex flex-col justify-center">
|
||||
<h1 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Frequently Asked Questions <QuestionMarkCircledIcon className='inline w-8 h-8'></QuestionMarkCircledIcon></h1>
|
||||
<Accordion type="single" value={feature} onValueChange={setFeature} defaultValue="item-1" className='mt-8'>
|
||||
<div className="mx-auto mt-36 flex w-full flex-col bg-surface p-5 shadow md:w-5/6 md:rounded-md lg:w-3/4 lg:flex-row lg:p-12">
|
||||
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Better tab management{" "}
|
||||
<BookmarkCheckIcon className="inline h-8 w-8"></BookmarkCheckIcon>
|
||||
</h3>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
Better tab management helps you stay organized and focused, reducing
|
||||
clutter and enhancing productivity
|
||||
</p>
|
||||
<div className="mt-8 w-full">
|
||||
<div className="flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Workspaces
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Fast profile switcher
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Container Tabs
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Question />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Tab Groups (Coming Soon)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-2 h-[1px] border-t lg:h-[unset] lg:w-[1px] lg:border-l lg:border-t-0"></div>
|
||||
<div className="flex flex-col p-16 lg:w-1/2">
|
||||
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Security and Privacy is{" "}
|
||||
<span className="font-bold text-purple-500">important</span> to us{" "}
|
||||
<LockClosedIcon className="inline h-8 w-8"></LockClosedIcon>
|
||||
</h3>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
Zen is based on Firefox, ensuring that your browsing experience
|
||||
prioritizes security and privacy. With advanced tracking protection
|
||||
and minimal data collection, Zen keeps your online activity safe and
|
||||
secure, giving you peace of mind as you explore the web.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<Button
|
||||
className="mt-8"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
window.open("https://docs.zen-browser.app/security", "_blank")
|
||||
}
|
||||
>
|
||||
Security in Zen{" "}
|
||||
<ExternalLinkIcon className="ml-4 h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
<Button
|
||||
className="mt-8"
|
||||
variant="ghost"
|
||||
onClick={() => window.open("/privacy-policy", "_blank")}
|
||||
>
|
||||
Your Privacy{" "}
|
||||
<ExternalLinkIcon className="ml-4 h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mt-36 flex w-full flex-col bg-surface shadow md:w-5/6 md:rounded-md lg:w-3/4 lg:flex-row">
|
||||
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||
<h1 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Sidebar <SidebarIcon className="ml-1 inline h-8 w-8"></SidebarIcon>
|
||||
</h1>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
Zen Browser has a built-in sidebar that lets you quickly access your
|
||||
favorite websites, bookmarks, and more. It's the perfect way to stay
|
||||
organized.
|
||||
</p>
|
||||
<div className="mt-8 w-full">
|
||||
<div className="flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Quick Access
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Customizable
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Checkmark />
|
||||
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||
Easy to Use
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CachedImage
|
||||
width={1350}
|
||||
height={900}
|
||||
src="www/public/browser-3.png"
|
||||
alt="Zen Browser"
|
||||
className="rounded-md object-cover object-left lg:w-1/2"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto mt-36 flex w-full flex-col bg-surface shadow md:w-5/6 md:rounded-md lg:w-3/4 lg:flex-row">
|
||||
<CachedImage
|
||||
width={1350}
|
||||
height={900}
|
||||
src="www/public/browser-4.jpg"
|
||||
alt="Zen Browser"
|
||||
className="rounded-md object-cover object-left lg:w-1/2"
|
||||
/>
|
||||
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||
<h1 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Introducing Compact Mode{" "}
|
||||
<SidebarCloseIcon className="inline h-8 w-8"></SidebarCloseIcon>
|
||||
</h1>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
Zen Browser's compact mode gives you more screen real estate by
|
||||
hiding the title bar and tabs. It's perfect for when you need to
|
||||
focus on your work.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<Button
|
||||
className="mt-8"
|
||||
onClick={() => window.open("/download", "_self")}
|
||||
>
|
||||
What are you waiting for?
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mt-36 flex w-full flex-col bg-surface shadow md:w-5/6 md:rounded-md lg:w-3/4 lg:flex-row">
|
||||
<div className="relative flex w-full flex-col justify-center p-5 lg:w-1/2 lg:p-12">
|
||||
<h1 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Frequently Asked Questions{" "}
|
||||
<QuestionMarkCircledIcon className="inline h-8 w-8"></QuestionMarkCircledIcon>
|
||||
</h1>
|
||||
<Accordion
|
||||
type="single"
|
||||
value={feature}
|
||||
onValueChange={setFeature}
|
||||
defaultValue="item-1"
|
||||
className="mt-8"
|
||||
>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>Is it Firefox based?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes, Zen Browser is focused on being always at the latest version of Firefox, ensuring that you have the latest security updates and features.
|
||||
Yes, Zen Browser is focused on being always at the latest
|
||||
version of Firefox, ensuring that you have the latest security
|
||||
updates and features.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>Does it track me?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<strong>No!</strong> Zen Browser is built with privacy in mind. We don't track you, we don't collect your data, and we don't sell your data to third parties.
|
||||
<strong>No!</strong> Zen Browser is built with privacy in mind.
|
||||
We don't track you, we don't collect your data, and we don't
|
||||
sell your data to third parties.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>How secure is Zen Browser?</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Zen Browser is built on top of Firefox, which is known for its security features. We also have additional security features like https only built into Zen Browser to help keep you safe online.
|
||||
Zen Browser is built on top of Firefox, which is known for its
|
||||
security features. We also have additional security features
|
||||
like https only built into Zen Browser to help keep you safe
|
||||
online.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
<div className="lg:w-1/2 h-auto rounded-md relative overflow-hidden">
|
||||
<CachedImage width={1350} height={900} src="www/public/feature-item-1.png" alt="Zen Browser" className="object-cover h-full w-full robject-right ounded-md" />
|
||||
<div className="relative h-auto overflow-hidden rounded-md lg:w-1/2">
|
||||
<CachedImage
|
||||
width={1350}
|
||||
height={900}
|
||||
src="www/public/feature-item-1.png"
|
||||
alt="Zen Browser"
|
||||
className="robject-right ounded-md h-full w-full object-cover"
|
||||
/>
|
||||
{feature == "item-1" && (
|
||||
<div className='w-full h-full absolute top-0 left-0 grid grid-rows-3'>
|
||||
<div className="absolute left-0 top-0 grid h-full w-full grid-rows-3">
|
||||
<div></div>
|
||||
<div className="w-fit h-fit m-auto tems-center bg-surface p-4 border-2 border-white shadow flex rounded-full animate-fade-in">
|
||||
<Logo className='w-10 h-10' /> <span className='text-4xl mx-4'>+</span> <svg className='w-10 h-10 relative dark:fill-white' xmlns="http://www.w3.org/2000/svg" fillOpacity="context-fill-opacity"><path style={{ transform: "scale(2) translate(5%, 5%)" }} d="M10.39 0C8.948.788 7.987 2.025 7.767 3.66c-1.017.162-1.768.781-1.768.781s.72-.44 1.736-.511a4.04 4.04 0 0 1 3.789 2.034s-.758-.62-1.928-.468c1.315.68 1.872 2.002 1.701 3.369-.17 1.367-1.183 2.435-2.354 2.723-1.171.287-2.333.099-3.229-.61-.896-.708-1.251-1.533-1.305-2.254.213-.533.541-.812 1.1-1.092.558-.279 1.422-.283 1.572-.283s.8-.507.95-.894c-.726-.363-1.292-.65-1.696-.934-.404-.283-.492-.534-1.012-.898-.307-1.006-.021-1.955-.021-1.955s-1.043.437-1.93 1.49c0 0-.342-.338-.28-2.006-.427.155-1.366 1.004-1.947 1.92a7.277 7.277 0 0 0-.798 1.723A8.296 8.296 0 0 0-.003 8a8 8 0 0 0 16 0c0-2.256-.93-4.252-2.188-5.002 0 0 .542.932.813 2.43-.4-1.04-1.235-2.166-1.877-2.844-.643-.678-2.068-1.88-2.357-2.584z" /></svg>
|
||||
<div className="tems-center m-auto flex h-fit w-fit animate-fade-in rounded-full border-2 border-white bg-surface p-4 shadow">
|
||||
<Logo className="h-10 w-10" />{" "}
|
||||
<span className="mx-4 text-4xl">+</span>{" "}
|
||||
<svg
|
||||
className="relative h-10 w-10 dark:fill-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fillOpacity="context-fill-opacity"
|
||||
>
|
||||
<path
|
||||
style={{ transform: "scale(2) translate(5%, 5%)" }}
|
||||
d="M10.39 0C8.948.788 7.987 2.025 7.767 3.66c-1.017.162-1.768.781-1.768.781s.72-.44 1.736-.511a4.04 4.04 0 0 1 3.789 2.034s-.758-.62-1.928-.468c1.315.68 1.872 2.002 1.701 3.369-.17 1.367-1.183 2.435-2.354 2.723-1.171.287-2.333.099-3.229-.61-.896-.708-1.251-1.533-1.305-2.254.213-.533.541-.812 1.1-1.092.558-.279 1.422-.283 1.572-.283s.8-.507.95-.894c-.726-.363-1.292-.65-1.696-.934-.404-.283-.492-.534-1.012-.898-.307-1.006-.021-1.955-.021-1.955s-1.043.437-1.93 1.49c0 0-.342-.338-.28-2.006-.427.155-1.366 1.004-1.947 1.92a7.277 7.277 0 0 0-.798 1.723A8.296 8.296 0 0 0-.003 8a8 8 0 0 0 16 0c0-2.256-.93-4.252-2.188-5.002 0 0 .542.932.813 2.43-.4-1.04-1.235-2.166-1.877-2.844-.643-.678-2.068-1.88-2.357-2.584z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<a href='https://github.com/zen-browser/desktop?tab=readme-ov-file#compatibility' target='_blank' className="w-fit h-fit m-auto items-center border-2 border-white shadow tems-center bg-surface p-4 flex rounded-full opacity-0 animate-fade-in [--animation-delay:300ms]">
|
||||
See what version of Firefox Zen uses <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' />
|
||||
<a
|
||||
href="https://github.com/zen-browser/desktop?tab=readme-ov-file#compatibility"
|
||||
target="_blank"
|
||||
className="tems-center m-auto flex h-fit w-fit animate-fade-in items-center rounded-full border-2 border-white bg-surface p-4 opacity-0 shadow [--animation-delay:300ms]"
|
||||
>
|
||||
See what version of Firefox Zen uses{" "}
|
||||
<ExternalLinkIcon className="ml-4 h-4 w-4 opacity-50" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{feature == "item-2" && (
|
||||
<div className='w-full h-full absolute top-0 left-0 grid grid-rows-3'>
|
||||
<div className="absolute left-0 top-0 grid h-full w-full grid-rows-3">
|
||||
<div></div>
|
||||
<div className="w-fit h-fit m-auto tems-center bg-surface p-4 border-2 border-white shadow flex rounded-full animate-fade-in">
|
||||
<LockClosedIcon className='w-10 h-10' /> <span className='text-4xl mx-4'>+</span> <EyeClosedIcon className='w-10 h-10' />
|
||||
<div className="tems-center m-auto flex h-fit w-fit animate-fade-in rounded-full border-2 border-white bg-surface p-4 shadow">
|
||||
<LockClosedIcon className="h-10 w-10" />{" "}
|
||||
<span className="mx-4 text-4xl">+</span>{" "}
|
||||
<EyeClosedIcon className="h-10 w-10" />
|
||||
</div>
|
||||
<a href='/privacy-policy' target='_blank' className="w-fit h-fit m-auto items-center border-2 border-white shadow tems-center bg-surface p-4 flex rounded-full opacity-0 animate-fade-in [--animation-delay:300ms]">
|
||||
Learn about Zen's privacy policy <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' />
|
||||
<a
|
||||
href="/privacy-policy"
|
||||
target="_blank"
|
||||
className="tems-center m-auto flex h-fit w-fit animate-fade-in items-center rounded-full border-2 border-white bg-surface p-4 opacity-0 shadow [--animation-delay:300ms]"
|
||||
>
|
||||
Learn about Zen's privacy policy{" "}
|
||||
<ExternalLinkIcon className="ml-4 h-4 w-4 opacity-50" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{feature == "item-3" && (
|
||||
<div className='w-full h-full absolute top-0 left-0 grid grid-rows-3'>
|
||||
<div className="absolute left-0 top-0 grid h-full w-full grid-rows-3">
|
||||
<div></div>
|
||||
<div className="w-fit h-fit m-auto tems-center bg-surface p-4 border-2 border-white shadow flex rounded-full animate-fade-in">
|
||||
<ShieldCheck className='w-10 h-10' /> <span className='text-4xl mx-4'>+</span> <ShieldAlertIcon className='w-10 h-10' />
|
||||
<div className="tems-center m-auto flex h-fit w-fit animate-fade-in rounded-full border-2 border-white bg-surface p-4 shadow">
|
||||
<ShieldCheck className="h-10 w-10" />{" "}
|
||||
<span className="mx-4 text-4xl">+</span>{" "}
|
||||
<ShieldAlertIcon className="h-10 w-10" />
|
||||
</div>
|
||||
<a href='https://docs.zen-browser.app/security' target='_blank' className="w-fit h-fit m-auto items-center w-fit border-2 border-white shadow tems-center bg-surface p-4 flex rounded-full opacity-0 animate-fade-in [--animation-delay:300ms]">
|
||||
See how Zen keeps you safe <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' />
|
||||
<a
|
||||
href="https://docs.zen-browser.app/security"
|
||||
target="_blank"
|
||||
className="tems-center m-auto flex h-fit w-fit animate-fade-in items-center rounded-full border-2 border-white bg-surface p-4 opacity-0 shadow [--animation-delay:300ms]"
|
||||
>
|
||||
See how Zen keeps you safe{" "}
|
||||
<ExternalLinkIcon className="ml-4 h-4 w-4 opacity-50" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full md:w-5/6 lg:w-3/4 p-5 lg:p-12 flex flex-col lg:flex-row md:rounded-md mx-auto bg-surface mt-36 shadow'>
|
||||
<div className="flex p-16 lg:w-1/2 flex-col justify-center">
|
||||
<h3 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Convinced?</h3>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>Download Zen Browser now and experience the future of browsing.</p>
|
||||
<div className="mx-auto mt-36 flex w-full flex-col bg-surface p-5 shadow md:w-5/6 md:rounded-md lg:w-3/4 lg:flex-row lg:p-12">
|
||||
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Convinced?
|
||||
</h3>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
Download Zen Browser now and experience the future of browsing.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<Button className='mt-8' onClick={() => window.open('/download', '_self')}>Download Now</Button>
|
||||
<Button
|
||||
className="mt-8"
|
||||
onClick={() => window.open("/download", "_self")}
|
||||
>
|
||||
Download Now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t lg:border-t-0 lg:border-l h-[1px] lg:h-[unset] lg:w-[1px] mx-2"></div>
|
||||
<div className="flex p-16 lg:w-1/2 flex-col justify-center">
|
||||
<h3 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Even more convinced? <HeartHandshake className='inline text-red-500 h-10 w-10' /></h3>
|
||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>Help support the development of Zen Browser by donating to our cause.</p>
|
||||
<div className="mx-2 h-[1px] border-t lg:h-[unset] lg:w-[1px] lg:border-l lg:border-t-0"></div>
|
||||
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||
Even more convinced?{" "}
|
||||
<HeartHandshake className="inline h-10 w-10 text-red-500" />
|
||||
</h3>
|
||||
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||
Help support the development of Zen Browser by donating to our
|
||||
cause.
|
||||
</p>
|
||||
<div className="relative mt-8 flex">
|
||||
<Button variant="ghost" onClick={() => window.open('https://patreon.com/zen_browser', '_blank')}>Patreon <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' /></Button>
|
||||
<Button className="ml-8" variant="ghost" onClick={() => window.open('https://ko-fi.com/zen_browser', '_blank')}>Ko-fi <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' /></Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
window.open("https://patreon.com/zen_browser", "_blank")
|
||||
}
|
||||
>
|
||||
Patreon <ExternalLinkIcon className="ml-4 h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-8"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
window.open("https://ko-fi.com/zen_browser", "_blank")
|
||||
}
|
||||
>
|
||||
Ko-fi <ExternalLinkIcon className="ml-4 h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import Logo from "./logo";
|
||||
import TextReveal from "./ui/text-reveal";
|
||||
@@ -8,22 +7,22 @@ import { Button } from "./ui/button";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div className="font-medium px-10 md:px-0 border-t w-full border-grey py-10 mt-10 flex align-center flex-col">
|
||||
<div className="flex mx-auto px-10 lg:px-0 border-b pb-10 justify-between pt-10 w-full lg:!w-2/3">
|
||||
<div className="border-grey align-center mt-10 flex w-full flex-col border-t px-10 py-10 font-medium md:px-0">
|
||||
<div className="mx-auto flex w-full justify-between border-b px-10 pb-10 pt-10 lg:!w-2/3 lg:px-0">
|
||||
<div className="flex flex-col">
|
||||
<Logo />
|
||||
<div className="mt-auto">
|
||||
<h1 className="text-2xl font-bold opacity-80">Zen Browser</h1>
|
||||
<h2 className="text-md font-bold opacity-80 mt-6">Follow Us</h2>
|
||||
<div className="flex mt-4 opacity-70">
|
||||
<h2 className="text-md mt-6 font-bold opacity-80">Follow Us</h2>
|
||||
<div className="mt-4 flex opacity-70">
|
||||
<a href="https://github.com/zen-browser">
|
||||
<GitHubLogoIcon className="w-5 h-5" />
|
||||
<GitHubLogoIcon className="h-5 w-5" />
|
||||
</a>
|
||||
<a href="https://discord.gg/zen-browser" className="ml-5">
|
||||
<DiscordLogoIcon className="w-5 h-5" />
|
||||
<DiscordLogoIcon className="h-5 w-5" />
|
||||
</a>
|
||||
<a href="https://fosstodon.org/@zenbrowser" className="ml-5">
|
||||
<MastodonLogo className="w-5 h-5" />
|
||||
<MastodonLogo className="h-5 w-5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,27 +30,21 @@ export default function Footer() {
|
||||
<div className="flex flex-col md:flex-row">
|
||||
<div>
|
||||
<h2 className="text-md font-bold opacity-80">Get Started</h2>
|
||||
<ul className="mt-4 opacity-70 font-normal">
|
||||
<ul className="mt-4 font-normal opacity-70">
|
||||
<li>
|
||||
<a href="/themes">
|
||||
Themes
|
||||
</a>
|
||||
<a href="/themes">Themes</a>
|
||||
</li>
|
||||
<li className="mt-2">
|
||||
<a href="/download">
|
||||
Download
|
||||
</a>
|
||||
<a href="/download">Download</a>
|
||||
</li>
|
||||
<li className="mt-2">
|
||||
<a href="/create-theme">
|
||||
Create a Theme
|
||||
</a>
|
||||
<a href="/create-theme">Create a Theme</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-10 md:mt-0 md:ml-12 lg:ml-24">
|
||||
<div className="mt-10 md:ml-12 md:mt-0 lg:ml-24">
|
||||
<h2 className="text-md font-bold opacity-80">Get Help</h2>
|
||||
<ul className="mt-4 opacity-70 font-normal">
|
||||
<ul className="mt-4 font-normal opacity-70">
|
||||
<li>
|
||||
<a href="https://discord.com/servers/mauro-s-little-sweatshop-1088172780480114748">
|
||||
Discord
|
||||
@@ -63,21 +56,19 @@ export default function Footer() {
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 className="text-md font-bold opacity-80 mt-8">About</h2>
|
||||
<ul className="mt-4 opacity-70 font-normal">
|
||||
<h2 className="text-md mt-8 font-bold opacity-80">About</h2>
|
||||
<ul className="mt-4 font-normal opacity-70">
|
||||
<li className="mt-2">
|
||||
<a href="/about">
|
||||
About Us
|
||||
</a>
|
||||
<a href="/about">About Us</a>
|
||||
</li>
|
||||
<li className="mt-2">
|
||||
<a href="/privacy-policy">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-10 md:mt-0 md:ml-12 lg:ml-24">
|
||||
<div className="mt-10 md:ml-12 md:mt-0 lg:ml-24">
|
||||
<h2 className="text-md font-bold opacity-80">Resources</h2>
|
||||
<ul className="mt-4 opacity-70 font-normal">
|
||||
<ul className="mt-4 font-normal opacity-70">
|
||||
<li>
|
||||
<a href="/branding-assets">Branding Assets</a>
|
||||
</li>
|
||||
@@ -91,8 +82,8 @@ export default function Footer() {
|
||||
<a href="/release-notes">Release Notes</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 className="text-md font-bold opacity-80 mt-8">Support Us</h2>
|
||||
<ul className="mt-4 opacity-70 font-normal">
|
||||
<h2 className="text-md mt-8 font-bold opacity-80">Support Us</h2>
|
||||
<ul className="mt-4 font-normal opacity-70">
|
||||
<li>
|
||||
<a href="https://patreon.com/zen_browser">Patreon</a>
|
||||
</li>
|
||||
@@ -103,8 +94,11 @@ export default function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full pt-10 pr-5 pl-3 mx-auto lg:!w-2/3 items-center">
|
||||
<p className="text-xs font-normal opacity-30">Crafted with ❤️ by the community - Copyright © {new Date().getFullYear()} Zen Browser</p>
|
||||
<div className="mx-auto flex w-full items-center pl-3 pr-5 pt-10 lg:!w-2/3">
|
||||
<p className="text-xs font-normal opacity-30">
|
||||
Crafted with ❤️ by the community - Copyright ©{" "}
|
||||
{new Date().getFullYear()} Zen Browser
|
||||
</p>
|
||||
<a href="/download" className="ml-auto">
|
||||
<Button className="ml-auto">Download</Button>
|
||||
</a>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import Particles from "./ui/particles";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import CoolHeaderText from "./cool-header-text";
|
||||
export default function Header() {
|
||||
const ref = useRef(null);
|
||||
const inView = useInView(ref, { once: true, margin: "-100px" });
|
||||
@@ -18,41 +19,28 @@ export default function Header() {
|
||||
<>
|
||||
<section
|
||||
id="hero"
|
||||
className="relative mx-auto min-h-screen flex justify-center flex-col max-w-7xl px-6 text-center md:px-8"
|
||||
className="relative mx-auto flex min-h-screen max-w-7xl flex-col justify-center px-6 text-center md:px-8"
|
||||
>
|
||||
<a href="/download">
|
||||
<AnimatedGradientText>
|
||||
🎉 <hr className="mx-2 h-4 w-[1px] shrink-0 bg-gray-300" />{" "}
|
||||
<span
|
||||
className={ny(
|
||||
`inline animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`
|
||||
)}
|
||||
>
|
||||
Introducing Zen Alpha
|
||||
</span>
|
||||
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
||||
</AnimatedGradientText>
|
||||
</a>
|
||||
<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">
|
||||
<div className="relative">
|
||||
<CoolHeaderText />
|
||||
</div>
|
||||
<p className="mb-12 -translate-y-4 animate-fade-in 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>
|
||||
<div className="flex flex-col md:flex-row justify-center w-full">
|
||||
<div className="flex w-full flex-col justify-center md:flex-row">
|
||||
<a href="/download">
|
||||
<Button
|
||||
className="animate-fade-in -translate-y-4 gap-1 text-white opacity-0 ease-in-out [--animation-delay:600ms] dark:text-black"
|
||||
>
|
||||
<Button className="-translate-y-4 animate-fade-in gap-1 text-white opacity-0 ease-in-out [--animation-delay:600ms] dark:text-black">
|
||||
<span>Download Zen Now </span>
|
||||
<ArrowRightIcon className="ml-1 size-4 transition-transform duration-300 ease-in-out group-hover:translate-x-1" />
|
||||
</Button>
|
||||
</a>
|
||||
<a href="#features" className="animate-fade-up -translate-y-4 opacity-0 [--animation-delay:800ms]">
|
||||
<Button variant="ghost" className="mt-4 md:mt-0 md:ml-4">
|
||||
<a
|
||||
href="#features"
|
||||
className="-translate-y-4 animate-fade-up opacity-0 [--animation-delay:800ms]"
|
||||
>
|
||||
<Button variant="ghost" className="mt-4 md:ml-4 md:mt-0">
|
||||
Start Exploring <ChevronDown className="ml-1 size-4" />
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
export const MastodonLogo = (props: any) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" {...props}>
|
||||
|
||||
<path fill="currentColor" d="M184 32H72a40 40 0 0 0-40 40v120a40 40 0 0 0 40 40h88a8 8 0 0 0 0-16H72a24 24 0 0 1-24-24v-8h136a40 40 0 0 0 40-40V72a40 40 0 0 0-40-40m24 112a24 24 0 0 1-24 24H48V72a24 24 0 0 1 24-24h112a24 24 0 0 1 24 24Zm-24-40v32a8 8 0 0 1-16 0v-32a16 16 0 0 0-32 0v32a8 8 0 0 1-16 0v-32a16 16 0 0 0-32 0v32a8 8 0 0 1-16 0v-32a32 32 0 0 1 56-21.13A32 32 0 0 1 184 104"></path>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M184 32H72a40 40 0 0 0-40 40v120a40 40 0 0 0 40 40h88a8 8 0 0 0 0-16H72a24 24 0 0 1-24-24v-8h136a40 40 0 0 0 40-40V72a40 40 0 0 0-40-40m24 112a24 24 0 0 1-24 24H48V72a24 24 0 0 1 24-24h112a24 24 0 0 1 24 24Zm-24-40v32a8 8 0 0 1-16 0v-32a16 16 0 0 0-32 0v32a8 8 0 0 1-16 0v-32a16 16 0 0 0-32 0v32a8 8 0 0 1-16 0v-32a32 32 0 0 1 56-21.13A32 32 0 0 1 184 104"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -7,9 +7,18 @@ import CachedImage from "./CachedImage";
|
||||
|
||||
export default function Logo({ withText, ...props }: any) {
|
||||
return (
|
||||
<div className="flex items-center m-0" {...props}>
|
||||
<CachedImage src={`www/public/logos/zen-black.svg`} width={40} height={40} alt="Zen Logo" className={ny("transition-all duration-300 hover:scale-110", withText && "mr-2")} />
|
||||
{withText && <span className="text-2xl font-bold ml-2">zen</span>}
|
||||
<div className="m-0 flex items-center" {...props}>
|
||||
<CachedImage
|
||||
src={`www/public/logos/zen-black.svg`}
|
||||
width={40}
|
||||
height={40}
|
||||
alt="Zen Logo"
|
||||
className={ny(
|
||||
"transition-all duration-300 hover:scale-110",
|
||||
withText && "mr-2",
|
||||
)}
|
||||
/>
|
||||
{withText && <span className="ml-2 text-2xl font-bold">zen</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,19 @@ export default function MarketplacePage({ themes }: {themes:ZenTheme[]}) {
|
||||
const [tags, setTags] = React.useState<string[]>(["all"]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full mx-auto items-center justify-center h-full">
|
||||
<div className="mx-auto w-full text-center border-b pt-48 pb-24 mb-12 bg-surface dark:bg-[#121212]">
|
||||
<div className="w-full lg:w-1/2 xl:w-1/2 mx-auto px-2 lg:px-none">
|
||||
<h1 className="text-4xl lg:text-7xl font-bold">Themes Store</h1>
|
||||
<ThemesSearch input={searchInput} setInput={setSearchInput} tags={tags} setTags={setTags} />
|
||||
<div className="mx-auto flex h-full w-full flex-col items-center justify-center">
|
||||
<div className="mx-auto mb-12 w-full border-b bg-surface pb-24 pt-48 text-center dark:bg-[#121212]">
|
||||
<div className="lg:px-none mx-auto w-full px-2 lg:w-1/2 xl:w-1/2">
|
||||
<h1 className="text-4xl font-bold lg:text-7xl">Themes Store</h1>
|
||||
<ThemesSearch
|
||||
input={searchInput}
|
||||
setInput={setSearchInput}
|
||||
tags={tags}
|
||||
setTags={setTags}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 lg:px-0 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-8 mt-10 w-full lg:w-1/2 xl:w-2/3 2xl:w-3/4">
|
||||
<div className="mt-10 grid w-full grid-cols-1 gap-8 px-5 md:grid-cols-2 lg:w-1/2 lg:px-0 xl:w-2/3 xl:grid-cols-3 2xl:w-3/4 2xl:grid-cols-4">
|
||||
{getThemesFromSearch(themes, searchInput, tags).map((theme) => (
|
||||
<ThemeCard key={theme.name} theme={theme} />
|
||||
))}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { SidebarOpen } from 'lucide-react'
|
||||
import type { LinkProps } from 'next/link'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { Sheet, SheetContent, SheetTrigger } from './ui/sheet'
|
||||
import { Button } from './ui/button'
|
||||
import { ScrollArea } from './ui/scroll-area'
|
||||
import Logo from './logo'
|
||||
import { ny } from '@/lib/utils'
|
||||
import { components } from './navigation'
|
||||
import { SidebarOpen } from "lucide-react";
|
||||
import type { LinkProps } from "next/link";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet";
|
||||
import { Button } from "./ui/button";
|
||||
import { ScrollArea } from "./ui/scroll-area";
|
||||
import Logo from "./logo";
|
||||
import { ny } from "@/lib/utils";
|
||||
import { components } from "./navigation";
|
||||
|
||||
export function MobileNav() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@@ -18,11 +18,11 @@ export function MobileNav() {
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<div className="z-40 flex w-full items-center space-between sm:hidden">
|
||||
<Logo className="size-6 ml-4" />
|
||||
<div className="space-between z-40 flex w-full items-center sm:hidden">
|
||||
<Logo className="ml-4 size-6" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="mr-2 px-0 ml-auto text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
className="ml-auto mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<SidebarOpen className="size-6" />
|
||||
<span className="sr-only">Toggle Menu</span>
|
||||
@@ -39,30 +39,21 @@ export function MobileNav() {
|
||||
</MobileLink>
|
||||
<ScrollArea className="mt-4 h-[calc(100vh-8rem)] pl-6">
|
||||
<div className="flex flex-col space-y-3">
|
||||
<MobileLink
|
||||
href="/download"
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<MobileLink href="/download" onOpenChange={setOpen}>
|
||||
<div>Download</div>
|
||||
<p className="opacity-60 text-xs">
|
||||
<p className="text-xs opacity-60">
|
||||
Get the latest version of Zen Browser.
|
||||
</p>
|
||||
</MobileLink>
|
||||
<MobileLink
|
||||
href="/themes"
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<MobileLink href="/themes" onOpenChange={setOpen}>
|
||||
<div>Theme Store</div>
|
||||
<p className="opacity-60 text-xs">
|
||||
<p className="text-xs opacity-60">
|
||||
Customize your browsing experience.
|
||||
</p>
|
||||
</MobileLink>
|
||||
<MobileLink
|
||||
href="/release-notes"
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<MobileLink href="/release-notes" onOpenChange={setOpen}>
|
||||
<div>Release Notes</div>
|
||||
<p className="opacity-60 text-xs">
|
||||
<p className="text-xs opacity-60">
|
||||
Stay up to date with the latest changes.
|
||||
</p>
|
||||
</MobileLink>
|
||||
@@ -71,29 +62,25 @@ export function MobileNav() {
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<div>Donate {"<"}3</div>
|
||||
<p className="opacity-60 text-xs">Support the project</p>
|
||||
<p className="text-xs opacity-60">Support the project</p>
|
||||
</MobileLink>
|
||||
{components.map(({ title, href, description }) => (
|
||||
<MobileLink
|
||||
href={href}
|
||||
key={href}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<MobileLink href={href} key={href} onOpenChange={setOpen}>
|
||||
<div>{title}</div>
|
||||
<p className='opacity-60 text-xs'>{description}</p>
|
||||
<p className="text-xs opacity-60">{description}</p>
|
||||
</MobileLink>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface MobileLinkProps extends LinkProps {
|
||||
onOpenChange?: (open: boolean) => void
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function MobileLink({
|
||||
@@ -103,18 +90,18 @@ function MobileLink({
|
||||
children,
|
||||
...props
|
||||
}: MobileLinkProps) {
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
return (
|
||||
<a
|
||||
href={href.toString()}
|
||||
onClick={() => {
|
||||
router.push(href.toString())
|
||||
onOpenChange?.(false)
|
||||
router.push(href.toString());
|
||||
onOpenChange?.(false);
|
||||
}}
|
||||
className={ny(className, "my-4")}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -6,21 +6,21 @@ import { Button } from "./ui/button";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
const [currentTheme, setCurrentTheme] = useState('light');
|
||||
const [currentTheme, setCurrentTheme] = useState("light");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
const savedTheme = localStorage.getItem("theme") || "light";
|
||||
setCurrentTheme(savedTheme);
|
||||
setTheme(savedTheme);
|
||||
}, [setTheme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
const newTheme = currentTheme === "light" ? "dark" : "light";
|
||||
setCurrentTheme(newTheme);
|
||||
setTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
localStorage.setItem("theme", newTheme);
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
@@ -29,7 +29,7 @@ export function ModeToggle() {
|
||||
|
||||
return (
|
||||
<Button variant="ghost" size="icon" onClick={toggleTheme}>
|
||||
{currentTheme === 'light' ? (
|
||||
{currentTheme === "light" ? (
|
||||
<SunIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
) : (
|
||||
<MoonIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { ny } from "@/lib/utils"
|
||||
import { ny } from "@/lib/utils";
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
@@ -11,53 +10,61 @@ import {
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
} from "@/components/ui/navigation-menu"
|
||||
import Logo from "./logo"
|
||||
import { ModeToggle } from "./mode-toggle"
|
||||
import { MobileNav } from "./mobile-nav"
|
||||
import { HeartIcon } from "lucide-react"
|
||||
import { HeartFilledIcon } from "@radix-ui/react-icons"
|
||||
} from "@/components/ui/navigation-menu";
|
||||
import Logo from "./logo";
|
||||
import { ModeToggle } from "./mode-toggle";
|
||||
import { MobileNav } from "./mobile-nav";
|
||||
import { HeartIcon } from "lucide-react";
|
||||
import { HeartFilledIcon } from "@radix-ui/react-icons";
|
||||
|
||||
export const components: { title: string; href: string; description: string }[] = [
|
||||
export const components: {
|
||||
title: string;
|
||||
href: string;
|
||||
description: string;
|
||||
}[] = [
|
||||
{
|
||||
title: "Privacy Policy",
|
||||
href: "/privacy-policy",
|
||||
description: "Read our privacy policy to learn more about how we handle your data."
|
||||
description:
|
||||
"Read our privacy policy to learn more about how we handle your data.",
|
||||
},
|
||||
{
|
||||
title: "Discord",
|
||||
href: "https://discord.gg/zen-browser",
|
||||
description: "Join our Discord server to chat with the community and get support."
|
||||
description:
|
||||
"Join our Discord server to chat with the community and get support.",
|
||||
},
|
||||
{
|
||||
title: "Source Code",
|
||||
href: "https://github.com/zen-browser",
|
||||
description: "View the source code on GitHub and contribute to the project."
|
||||
description:
|
||||
"View the source code on GitHub and contribute to the project.",
|
||||
},
|
||||
{
|
||||
title: "Branding Assets",
|
||||
href: "/branding-assets",
|
||||
description: "Download our branding assets to use in your projects."
|
||||
description: "Download our branding assets to use in your projects.",
|
||||
},
|
||||
{
|
||||
title: "About",
|
||||
href: "/about",
|
||||
description: "Learn more about the Zen Browser project and the team behind it."
|
||||
description:
|
||||
"Learn more about the Zen Browser project and the team behind it.",
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
href: "https://docs.zen-browser.app/",
|
||||
description: "Read the documentation to learn more about Zen Browser."
|
||||
}
|
||||
]
|
||||
description: "Read the documentation to learn more about Zen Browser.",
|
||||
},
|
||||
];
|
||||
|
||||
export function Navigation() {
|
||||
return (
|
||||
<div className="bg-background z-40 top-0 left-0 w-full flex fixed border-b border-grey p-2 items-center justify-center">
|
||||
<div className="border-grey fixed left-0 top-0 z-40 flex w-full items-center justify-center border-b bg-background p-2">
|
||||
<MobileNav />
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList className="w-full hidden py-3 sm:flex">
|
||||
<NavigationMenuItem className="cursor-pointer mr-20">
|
||||
<NavigationMenuList className="hidden w-full py-3 sm:flex">
|
||||
<NavigationMenuItem className="mr-20 cursor-pointer">
|
||||
<NavigationMenuLink href="/">
|
||||
<Logo withText />
|
||||
</NavigationMenuLink>
|
||||
@@ -106,13 +113,15 @@ export function Navigation() {
|
||||
title="Patreon"
|
||||
href="https://patreon.com/zen_browser?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
|
||||
>
|
||||
Support us on Patreon and get exclusive rewards and keep the project alive.
|
||||
Support us on Patreon and get exclusive rewards and keep the
|
||||
project alive.
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title="Ko-Fi"
|
||||
href="https://ko-fi.com/zen_browser?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
|
||||
>
|
||||
Ko-fi is a way to support us with a one-time donation and help us keep the project alive.
|
||||
Ko-fi is a way to support us with a one-time donation and help
|
||||
us keep the project alive.
|
||||
</ListItem>
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
@@ -137,7 +146,7 @@ export function Navigation() {
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const ListItem = React.forwardRef<
|
||||
@@ -151,7 +160,7 @@ const ListItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={ny(
|
||||
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -162,6 +171,6 @@ const ListItem = React.forwardRef<
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
ListItem.displayName = "ListItem"
|
||||
);
|
||||
});
|
||||
ListItem.displayName = "ListItem";
|
||||
|
||||
@@ -12,15 +12,24 @@ import { ny } from "@/lib/utils";
|
||||
export default function ReleaseNoteElement({ data }: { data: ReleaseNote }) {
|
||||
const splitDate = data.date.split("/");
|
||||
return (
|
||||
<section className={ny("flex flex-col lg:flex-row border-t relative", data.version == releaseNotes[0].version ? "mt-24 pt-24" : "pt-36 mt-36")} id={data.version}>
|
||||
<StickyBox className="mb-6 lg:mb-0 mt-1 mr-24 ml-10 text-muted-foreground text-xs min-w-52 h-fit" offsetTop={120}>
|
||||
<section
|
||||
className={ny(
|
||||
"relative flex flex-col border-t lg:flex-row",
|
||||
data.version == releaseNotes[0].version ? "mt-24 pt-24" : "mt-36 pt-36",
|
||||
)}
|
||||
id={data.version}
|
||||
>
|
||||
<StickyBox
|
||||
className="mb-6 ml-10 mr-24 mt-1 h-fit min-w-52 text-xs text-muted-foreground lg:mb-0"
|
||||
offsetTop={120}
|
||||
>
|
||||
{moment({
|
||||
year: parseInt(splitDate[2]),
|
||||
month: parseInt(splitDate[1]) - 1,
|
||||
day: parseInt(splitDate[0]),
|
||||
}).format('MMMM Do, YYYY')}
|
||||
}).format("MMMM Do, YYYY")}
|
||||
</StickyBox>
|
||||
<div className="px-5 md:px-10 md:px-0 md:pr-32">
|
||||
<div className="px-5 md:px-0 md:px-10 md:pr-32">
|
||||
<h1 className="text-3xl font-bold">
|
||||
Release notes for {data.version} 🎉
|
||||
</h1>
|
||||
@@ -36,7 +45,7 @@ export default function ReleaseNoteElement({ data }: { data: ReleaseNote }) {
|
||||
</p>
|
||||
{data.extra && (
|
||||
<p
|
||||
className="text-md text-muted-foreground mt-8"
|
||||
className="text-md mt-8 text-muted-foreground"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: data.extra.replace(/(\n)/g, "<br />"),
|
||||
}}
|
||||
@@ -47,24 +56,24 @@ export default function ReleaseNoteElement({ data }: { data: ReleaseNote }) {
|
||||
<AccordionItem value="breaking-changes" title="Breaking Changes">
|
||||
<AccordionTrigger className="border-b">
|
||||
<div className="flex items-center">
|
||||
<ExclamationTriangleIcon className="text-red-500 mr-2 mt-1 size-5 opacity-50" />
|
||||
<ExclamationTriangleIcon className="mr-2 mt-1 size-5 text-red-500 opacity-50" />
|
||||
<span className="ml-2">Breaking Changes</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<ul className="ml-6" style={{ listStyleType: "initial" }}>
|
||||
{data.breakingChanges.map((change) => (
|
||||
<li key={change} className="mt-4 text-md text-muted-foreground">
|
||||
<span className="ml-1">
|
||||
{change}
|
||||
</span>
|
||||
<li
|
||||
key={change}
|
||||
className="text-md mt-4 text-muted-foreground"
|
||||
>
|
||||
<span className="ml-1">{change}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
)}
|
||||
{data.fixes && (
|
||||
<AccordionItem value="fixes" title="Fixes">
|
||||
<AccordionTrigger className="border-b">
|
||||
@@ -76,14 +85,15 @@ export default function ReleaseNoteElement({ data }: { data: ReleaseNote }) {
|
||||
<AccordionContent>
|
||||
<ul className="ml-6" style={{ listStyleType: "initial" }}>
|
||||
{data.fixes.map((fix) => (
|
||||
<li key={fix.description} className="mt-4 text-md text-muted-foreground">
|
||||
<span className="ml-1">
|
||||
{fix.description}
|
||||
</span>
|
||||
<li
|
||||
key={fix.description}
|
||||
className="text-md mt-4 text-muted-foreground"
|
||||
>
|
||||
<span className="ml-1">{fix.description}</span>
|
||||
{fix.issue && (
|
||||
<a
|
||||
href={`https://github.com/zen-browser/desktop/issues/${fix.issue}`}
|
||||
className="text-blue-500 ml-1 text-underline"
|
||||
className="text-underline ml-1 text-blue-500"
|
||||
>
|
||||
#{fix.issue}
|
||||
</a>
|
||||
@@ -98,17 +108,18 @@ export default function ReleaseNoteElement({ data }: { data: ReleaseNote }) {
|
||||
<AccordionItem value="features" title="Features">
|
||||
<AccordionTrigger>
|
||||
<div className="flex items-center">
|
||||
<StarIcon className="text-yellow-700 mr-2 mt-1 size-5 opacity-50" />
|
||||
<StarIcon className="mr-2 mt-1 size-5 text-yellow-700 opacity-50" />
|
||||
<span className="ml-2">Features</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<ul className="ml-6" style={{ listStyleType: "initial" }}>
|
||||
{data.features.map((feature) => (
|
||||
<li key={feature} className="mt-4 text-md text-muted-foreground">
|
||||
<span className="ml-1">
|
||||
{feature}
|
||||
</span>
|
||||
<li
|
||||
key={feature}
|
||||
className="text-md mt-4 text-muted-foreground"
|
||||
>
|
||||
<span className="ml-1">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -1,47 +1,75 @@
|
||||
|
||||
import { getThemeAuthorLink, ZenTheme } from "@/lib/themes";
|
||||
import styled from "styled-components";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";import { Button } from "./ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./ui/dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const ThemeCardWrapper = styled.div`
|
||||
`;
|
||||
const ThemeCardWrapper = styled.div``;
|
||||
|
||||
export default function ThemeCard({
|
||||
theme,
|
||||
className
|
||||
className,
|
||||
}: {
|
||||
theme: ZenTheme;
|
||||
className?: string;
|
||||
}) {
|
||||
|
||||
const maxNameLen = 50;
|
||||
const maxDescLen = 100;
|
||||
|
||||
return (
|
||||
<ThemeCardWrapper onClick={(event) => {
|
||||
<ThemeCardWrapper
|
||||
onClick={(event) => {
|
||||
if (event.target instanceof HTMLAnchorElement) return;
|
||||
window.open(`/themes/${theme.id}`, "_self");
|
||||
}} className={ny("flex flex-col justify-start p-5 rounded-lg shadow-sm bg-muted dark:bg-muted/50 border border-grey-900 dark:border-muted w-full hover:shadow-lg transition duration-300 ease-in-out hover:bg-surface hover:border-blue-500 cursor-pointer select-none ", className)}>
|
||||
<img src={theme.image} alt={theme.name} width={500} height={500}
|
||||
className="w-full h-32 object-cover rounded-lg border shadow" />
|
||||
<h2 className="text-xl font-bold mt-4 overflow-ellipsis text-start">{theme.name.substring(0, maxNameLen).trim() + (theme.name.length > maxNameLen ? "..." : "")}</h2>
|
||||
<div className="flex mt-2">
|
||||
}}
|
||||
className={ny(
|
||||
"border-grey-900 flex w-full cursor-pointer select-none flex-col justify-start rounded-lg border bg-muted p-5 shadow-sm transition duration-300 ease-in-out hover:border-blue-500 hover:bg-surface hover:shadow-lg dark:border-muted dark:bg-muted/50",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={theme.image}
|
||||
alt={theme.name}
|
||||
width={500}
|
||||
height={500}
|
||||
className="h-32 w-full rounded-lg border object-cover shadow"
|
||||
/>
|
||||
<h2 className="mt-4 overflow-ellipsis text-start text-xl font-bold">
|
||||
{theme.name.substring(0, maxNameLen).trim() +
|
||||
(theme.name.length > maxNameLen ? "..." : "")}
|
||||
</h2>
|
||||
<div className="mt-2 flex">
|
||||
{theme.homepage && (
|
||||
<>
|
||||
<a href={theme.homepage} className="text-blue-500 text-md" target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
href={theme.homepage}
|
||||
className="text-md text-blue-500"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Homepage
|
||||
</a>
|
||||
<span className="text-muted-foreground text-md mx-2">
|
||||
{"·"}
|
||||
</span>
|
||||
<span className="text-md mx-2 text-muted-foreground">{"·"}</span>
|
||||
</>
|
||||
)}
|
||||
<a href={getThemeAuthorLink(theme)} className="text-blue-500 text-md" target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
href={getThemeAuthorLink(theme)}
|
||||
className="text-md text-blue-500"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Author
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-md mt-2 overflow-ellipsis text-muted-foreground text-start">
|
||||
<p className="text-md mt-2 overflow-ellipsis text-start text-muted-foreground">
|
||||
{theme.description.substring(0, maxDescLen).trim() +
|
||||
(theme.description.length > maxDescLen ? "..." : "")}
|
||||
</p>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
|
||||
|
||||
import { getThemeAuthorLink, getThemeFromId, getThemeMarkdown, ZenTheme } from "@/lib/themes";
|
||||
import {
|
||||
getThemeAuthorLink,
|
||||
getThemeFromId,
|
||||
getThemeMarkdown,
|
||||
ZenTheme,
|
||||
} from "@/lib/themes";
|
||||
import { Button } from "./ui/button";
|
||||
import Markdown from "react-markdown";
|
||||
import '../app/privacy-policy/markdown.css';
|
||||
import "../app/privacy-policy/markdown.css";
|
||||
import { ChevronLeft, LoaderCircleIcon } from "lucide-react";
|
||||
|
||||
export default async function ThemePage({ themeID }: { themeID: string }) {
|
||||
|
||||
const theme = await getThemeFromId(themeID);
|
||||
if (!theme) {
|
||||
return <div>Theme not found</div>;
|
||||
@@ -16,19 +18,30 @@ export default async function ThemePage({ themeID }: { themeID: string }) {
|
||||
const readme = await getThemeMarkdown(theme);
|
||||
|
||||
return (
|
||||
<div className="mt-24 lg:mt-56 flex-col lg:flex-row flex mx-auto items-start relative">
|
||||
<div className="mx-auto md:mx-0 flex flex-col relative bg-surface rounded-lg border shadow lg:sticky lg:top-0 w-md h-full p-5 mr-5 w-full md:max-w-sm">
|
||||
<a className="flex mt-2 mb-4 items-center cursor-pointer opacity-70" href="/themes">
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||
<div className="relative mx-auto mt-24 flex flex-col items-start lg:mt-56 lg:flex-row">
|
||||
<div className="w-md relative mx-auto mr-5 flex h-full w-full flex-col rounded-lg border bg-surface p-5 shadow md:mx-0 md:max-w-sm lg:sticky lg:top-0">
|
||||
<a
|
||||
className="mb-4 mt-2 flex cursor-pointer items-center opacity-70"
|
||||
href="/themes"
|
||||
>
|
||||
<ChevronLeft className="mr-1 h-4 w-4" />
|
||||
<h3 className="text-md">Go back</h3>
|
||||
</a>
|
||||
<img src={theme.image} alt={theme.name} width={500} height={500} className="w-full object-cover rounded-lg border-2 shadow" />
|
||||
<h1 className="text-2xl mt-5 font-bold">{theme.name}</h1>
|
||||
<p className="text-sm text-muted-foreground mt-2">{theme.description}</p>
|
||||
<img
|
||||
src={theme.image}
|
||||
alt={theme.name}
|
||||
width={500}
|
||||
height={500}
|
||||
className="w-full rounded-lg border-2 object-cover shadow"
|
||||
/>
|
||||
<h1 className="mt-5 text-2xl font-bold">{theme.name}</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{theme.description}
|
||||
</p>
|
||||
{theme.homepage && (
|
||||
<a
|
||||
href={theme.homepage}
|
||||
className="text-blue-500 text-md mt-4"
|
||||
className="text-md mt-4 text-blue-500"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -40,26 +53,43 @@ export default async function ThemePage({ themeID }: { themeID: string }) {
|
||||
className="mt-4 hidden !rounded-lg"
|
||||
id="install-theme"
|
||||
zen-theme-id={theme.id}
|
||||
>Install Theme 🎉</Button>
|
||||
>
|
||||
Install Theme 🎉
|
||||
</Button>
|
||||
<Button
|
||||
className="mt-4 hidden !rounded-lg"
|
||||
id="install-theme-uninstall"
|
||||
zen-theme-id={theme.id}
|
||||
>Uninstall Theme</Button>
|
||||
<p id="install-theme-error" className="text-muted-foreground text-sm mt-2">You need to have Zen Browser installed to install this theme. <a href="/download" className="text-blue-500">Download now!</a></p>
|
||||
>
|
||||
Uninstall Theme
|
||||
</Button>
|
||||
<p
|
||||
id="install-theme-error"
|
||||
className="mt-2 text-sm text-muted-foreground"
|
||||
>
|
||||
You need to have Zen Browser installed to install this theme.{" "}
|
||||
<a href="/download" className="text-blue-500">
|
||||
Download now!
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col lg:min-h-[calc(100vh/2-2rem)] px-5 lg:pl-10 max-w-xl lg:min-w-96 w-full">
|
||||
<div className="flex w-full max-w-xl flex-col px-5 lg:min-h-[calc(100vh/2-2rem)] lg:min-w-96 lg:pl-10">
|
||||
<div id="policy" className="w-full">
|
||||
{readme === null ? (
|
||||
<LoaderCircleIcon className="animate-spin w-12 h-12 mx-auto" />
|
||||
<LoaderCircleIcon className="mx-auto h-12 w-12 animate-spin" />
|
||||
) : (
|
||||
<Markdown>{`${readme}`}</Markdown>
|
||||
)}
|
||||
</div>
|
||||
<hr className="my-5" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Theme by{" "}
|
||||
<a href={getThemeAuthorLink(theme)} className="text-blue-500 text-md mt-4" target="_blank" rel="noopener noreferrer">
|
||||
<a
|
||||
href={getThemeAuthorLink(theme)}
|
||||
className="text-md mt-4 text-blue-500"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{theme.author}
|
||||
</a>
|
||||
{` • v${theme.version}`}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ import { SearchIcon } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const TAGS = [
|
||||
"all",
|
||||
"color-scheme",
|
||||
"utility",
|
||||
];
|
||||
const TAGS = ["all", "color-scheme", "utility"];
|
||||
|
||||
export default function ThemesSearch({
|
||||
input, setInput, tags, setTags
|
||||
input,
|
||||
setInput,
|
||||
tags,
|
||||
setTags,
|
||||
}: {
|
||||
input: string;
|
||||
setInput: (input: string) => void;
|
||||
@@ -18,32 +17,45 @@ export default function ThemesSearch({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full p-2 bg-black/10 dark:bg-muted/50 rounded-full overflow-hidden mt-10 items-center border border-black dark:border-muted">
|
||||
<SearchIcon className="size-6 mx-4 text-black dark:text-white" />
|
||||
<div className="mt-10 flex w-full items-center overflow-hidden rounded-full border border-black bg-black/10 p-2 dark:border-muted dark:bg-muted/50">
|
||||
<SearchIcon className="mx-4 size-6 text-black dark:text-white" />
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Search themes"
|
||||
className="w-full bg-transparent border-none focus:outline-none focus:border-none focus:ring-0 dark:text-white text-black"
|
||||
className="w-full border-none bg-transparent text-black focus:border-none focus:outline-none focus:ring-0 dark:text-white"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => window.open("https://docs.zen-browser.app/themes-store/themes-marketplace-submission-guidelines#themes-store-submission-guidelines", "_blank")}
|
||||
className="text-muted rounded-full rounded-r-none hidden md:block"
|
||||
>Submit a theme</Button>
|
||||
onClick={() =>
|
||||
window.open(
|
||||
"https://docs.zen-browser.app/themes-store/themes-marketplace-submission-guidelines#themes-store-submission-guidelines",
|
||||
"_blank",
|
||||
)
|
||||
}
|
||||
className="hidden rounded-full rounded-r-none text-muted md:block"
|
||||
>
|
||||
Submit a theme
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => window.open("/create-theme", "_self")}
|
||||
className="text-muted rounded-full rounded-l-none border-l border-black dark:border-none hidden md:block"
|
||||
>Create your theme</Button>
|
||||
className="hidden rounded-full rounded-l-none border-l border-black text-muted dark:border-none md:block"
|
||||
>
|
||||
Create your theme
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{TAGS.map((tag) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
key={tag}
|
||||
onClick={() => setTags([tag])}
|
||||
className={ny(`!rounded-full px-5 ${tags.includes(tag) ? "bg-black dark:bg-white text-white dark:text-black" : ""}`)}
|
||||
>{tag}</Button>
|
||||
className={ny(
|
||||
`!rounded-full px-5 ${tags.includes(tag) ? "bg-black text-white dark:bg-white dark:text-black" : ""}`,
|
||||
)}
|
||||
>
|
||||
{tag}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
||||
import { ChevronDownIcon } from '@radix-ui/react-icons'
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "@radix-ui/react-icons";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
@@ -14,11 +14,11 @@ const AccordionItem = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={ny('border-b', className)}
|
||||
className={ny("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = 'AccordionItem'
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
@@ -28,17 +28,17 @@ const AccordionTrigger = React.forwardRef<
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground size-4 shrink-0 transition-transform duration-200" />
|
||||
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
@@ -46,12 +46,12 @@ const AccordionContent = React.forwardRef<
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={ny('pb-4 pt-0', className)}>{children}</div>
|
||||
<div className={ny("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { ny } from '@/lib/utils'
|
||||
import type { ReactNode } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
export default function AnimatedGradientText({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={ny(
|
||||
'group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-white/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] dark:bg-black/40',
|
||||
"group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-white/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] dark:bg-black/40",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] ![mask-composite:subtract] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]" />
|
||||
<div className="animate-gradient absolute inset-0 block h-full w-full bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]" />
|
||||
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
import { ny } from '@/lib/utils'
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface GridPatternProps {
|
||||
width?: number
|
||||
height?: number
|
||||
x?: number
|
||||
y?: number
|
||||
strokeDasharray?: any
|
||||
numSquares?: number
|
||||
className?: string
|
||||
maxOpacity?: number
|
||||
duration?: number
|
||||
repeatDelay?: number
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
strokeDasharray?: any;
|
||||
numSquares?: number;
|
||||
className?: string;
|
||||
maxOpacity?: number;
|
||||
duration?: number;
|
||||
repeatDelay?: number;
|
||||
}
|
||||
|
||||
export function GridPattern({
|
||||
@@ -30,16 +30,16 @@ export function GridPattern({
|
||||
repeatDelay = 0.5,
|
||||
...props
|
||||
}: GridPatternProps) {
|
||||
const id = useId()
|
||||
const containerRef = useRef(null)
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
||||
const [squares, setSquares] = useState(() => generateSquares(numSquares))
|
||||
const id = useId();
|
||||
const containerRef = useRef(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [squares, setSquares] = useState(() => generateSquares(numSquares));
|
||||
|
||||
function getPos() {
|
||||
return [
|
||||
Math.floor((Math.random() * dimensions.width) / width),
|
||||
Math.floor((Math.random() * dimensions.height) / height),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Adjust the generateSquares function to return objects with an id, x, and y
|
||||
@@ -47,13 +47,13 @@ export function GridPattern({
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
pos: getPos(),
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
// Function to update a single square's position
|
||||
const updateSquarePosition = (id: number) => {
|
||||
setSquares(currentSquares =>
|
||||
currentSquares.map(sq =>
|
||||
setSquares((currentSquares) =>
|
||||
currentSquares.map((sq) =>
|
||||
sq.id === id
|
||||
? {
|
||||
...sq,
|
||||
@@ -61,14 +61,14 @@ export function GridPattern({
|
||||
}
|
||||
: sq,
|
||||
),
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Update squares to animate in
|
||||
useEffect(() => {
|
||||
if (dimensions.width && dimensions.height)
|
||||
setSquares(generateSquares(numSquares))
|
||||
}, [dimensions, numSquares])
|
||||
setSquares(generateSquares(numSquares));
|
||||
}, [dimensions, numSquares]);
|
||||
|
||||
// Resize observer to update container dimensions
|
||||
useEffect(() => {
|
||||
@@ -77,25 +77,23 @@ export function GridPattern({
|
||||
setDimensions({
|
||||
width: entry.contentRect.width,
|
||||
height: entry.contentRect.height,
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (containerRef.current)
|
||||
resizeObserver.observe(containerRef.current)
|
||||
if (containerRef.current) resizeObserver.observe(containerRef.current);
|
||||
|
||||
return () => {
|
||||
if (containerRef.current)
|
||||
resizeObserver.unobserve(containerRef.current)
|
||||
}
|
||||
}, [containerRef])
|
||||
if (containerRef.current) resizeObserver.unobserve(containerRef.current);
|
||||
};
|
||||
}, [containerRef]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={containerRef}
|
||||
aria-hidden="true"
|
||||
className={ny(
|
||||
'pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30',
|
||||
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -126,7 +124,7 @@ export function GridPattern({
|
||||
duration,
|
||||
repeat: 1,
|
||||
delay: index * 0.1,
|
||||
repeatType: 'reverse',
|
||||
repeatType: "reverse",
|
||||
}}
|
||||
onAnimationComplete={() => updateSquarePosition(id)}
|
||||
key={`${x}-${y}-${index}`}
|
||||
@@ -140,7 +138,7 @@ export function GridPattern({
|
||||
))}
|
||||
</svg>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default GridPattern
|
||||
export default GridPattern;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { CSSProperties, FC, ReactNode } from 'react'
|
||||
import { ny } from '@/lib/utils'
|
||||
import type { CSSProperties, FC, ReactNode } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface AnimatedShinyTextProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
shimmerWidth?: number
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
shimmerWidth?: number;
|
||||
}
|
||||
|
||||
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
||||
@@ -16,24 +16,24 @@ const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
||||
<p
|
||||
style={
|
||||
{
|
||||
'--shimmer-width': `${shimmerWidth}px`,
|
||||
"--shimmer-width": `${shimmerWidth}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
className={ny(
|
||||
'mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50 ',
|
||||
"mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50",
|
||||
|
||||
// Shimmer effect
|
||||
'animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
|
||||
"animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
|
||||
|
||||
// Shimmer gradient
|
||||
'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',
|
||||
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
|
||||
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedShinyText
|
||||
export default AnimatedShinyText;
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useRef } from 'react'
|
||||
import type { Variants } from 'framer-motion'
|
||||
import { AnimatePresence, motion, useInView } from 'framer-motion'
|
||||
import { useRef } from "react";
|
||||
import type { Variants } from "framer-motion";
|
||||
import { AnimatePresence, motion, useInView } from "framer-motion";
|
||||
|
||||
interface BlurFadeProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
variant?: {
|
||||
hidden: { y: number }
|
||||
visible: { y: number }
|
||||
}
|
||||
duration?: number
|
||||
delay?: number
|
||||
yOffset?: number
|
||||
inView?: boolean
|
||||
inViewMargin?: any
|
||||
blur?: string
|
||||
hidden: { y: number };
|
||||
visible: { y: number };
|
||||
};
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
yOffset?: number;
|
||||
inView?: boolean;
|
||||
inViewMargin?: any;
|
||||
blur?: string;
|
||||
}
|
||||
|
||||
export default function BlurFade({
|
||||
@@ -27,34 +27,34 @@ export default function BlurFade({
|
||||
delay = 0,
|
||||
yOffset = 6,
|
||||
inView = false,
|
||||
inViewMargin = '-50px',
|
||||
blur = '6px',
|
||||
inViewMargin = "-50px",
|
||||
blur = "6px",
|
||||
}: BlurFadeProps) {
|
||||
const ref = useRef(null)
|
||||
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
|
||||
const isInView = !inView || inViewResult
|
||||
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
|
||||
};
|
||||
const combinedVariants = variant || defaultVariants;
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
exit="hidden"
|
||||
variants={combinedVariants}
|
||||
transition={{
|
||||
delay: 0.04 + delay,
|
||||
duration,
|
||||
ease: 'easeOut',
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
'use client'
|
||||
import { motion } from 'framer-motion'
|
||||
import { ny } from '@/lib/utils'
|
||||
"use client";
|
||||
import { motion } from "framer-motion";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface BlurIntProps {
|
||||
word: string
|
||||
className?: string
|
||||
word: string;
|
||||
className?: string;
|
||||
variant?: {
|
||||
hidden: { filter: string, opacity: number }
|
||||
visible: { filter: string, opacity: number }
|
||||
}
|
||||
duration?: number
|
||||
hidden: { filter: string; opacity: number };
|
||||
visible: { filter: string; opacity: number };
|
||||
};
|
||||
duration?: number;
|
||||
}
|
||||
function BlurIn({ word, className, variant, duration = 1 }: BlurIntProps) {
|
||||
const defaultVariants = {
|
||||
hidden: { filter: 'blur(10px)', opacity: 0 },
|
||||
visible: { filter: 'blur(0px)', opacity: 1 },
|
||||
}
|
||||
const combinedVariants = variant || defaultVariants
|
||||
hidden: { filter: "blur(10px)", opacity: 0 },
|
||||
visible: { filter: "blur(0px)", opacity: 1 },
|
||||
};
|
||||
const combinedVariants = variant || defaultVariants;
|
||||
|
||||
return (
|
||||
<motion.h1
|
||||
@@ -26,12 +26,12 @@ function BlurIn({ word, className, variant, duration = 1 }: BlurIntProps) {
|
||||
variants={combinedVariants}
|
||||
className={ny(
|
||||
className,
|
||||
'font-display text-center text-4xl font-bold tracking-[-0.02em] drop-shadow-sm md:text-7xl md:leading-[5rem]',
|
||||
"font-display text-center text-4xl font-bold tracking-[-0.02em] drop-shadow-sm md:text-7xl md:leading-[5rem]",
|
||||
)}
|
||||
>
|
||||
{word}
|
||||
</motion.h1>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default BlurIn
|
||||
export default BlurIn;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface BorderBeamProps {
|
||||
className?: string
|
||||
size?: number
|
||||
duration?: number
|
||||
borderWidth?: number
|
||||
anchor?: number
|
||||
colorFrom?: string
|
||||
colorTo?: string
|
||||
delay?: number
|
||||
className?: string;
|
||||
size?: number;
|
||||
duration?: number;
|
||||
borderWidth?: number;
|
||||
anchor?: number;
|
||||
colorFrom?: string;
|
||||
colorTo?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export function BorderBeam({
|
||||
@@ -17,33 +17,33 @@ export function BorderBeam({
|
||||
duration = 15,
|
||||
anchor = 90,
|
||||
borderWidth = 1.5,
|
||||
colorFrom = '#ffaa40',
|
||||
colorTo = '#9c40ff',
|
||||
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`,
|
||||
"--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]',
|
||||
"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)]',
|
||||
"![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))]',
|
||||
"after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:animate-border-beam 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,
|
||||
)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { CheckIcon } from '@radix-ui/react-icons'
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "@radix-ui/react-icons";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
@@ -13,18 +13,18 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={ny('flex items-center justify-center text-current')}
|
||||
className={ny("flex items-center justify-center text-current")}
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox }
|
||||
export { Checkbox };
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
'use client'
|
||||
import confetti from 'canvas-confetti'
|
||||
import type { ReactNode } from 'react'
|
||||
import React, { createContext, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
||||
"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, ButtonProps } from './button'
|
||||
} from "canvas-confetti";
|
||||
import { Button, ButtonProps } from "./button";
|
||||
|
||||
interface Api {
|
||||
fire: (options?: ConfettiOptions) => void
|
||||
fire: (options?: ConfettiOptions) => void;
|
||||
}
|
||||
|
||||
type Props = React.ComponentPropsWithRef<'canvas'> & {
|
||||
options?: ConfettiOptions
|
||||
globalOptions?: ConfettiGlobalOptions
|
||||
manualstart?: boolean
|
||||
children?: ReactNode
|
||||
}
|
||||
type Props = React.ComponentPropsWithRef<"canvas"> & {
|
||||
options?: ConfettiOptions;
|
||||
globalOptions?: ConfettiGlobalOptions;
|
||||
manualstart?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export type ConfettiRef = Api | null
|
||||
export type ConfettiRef = Api | null;
|
||||
|
||||
const ConfettiContext = createContext<Api>({} as Api)
|
||||
const ConfettiContext = createContext<Api>({} as Api);
|
||||
|
||||
const Confetti = forwardRef<ConfettiRef, Props>((props, ref) => {
|
||||
const {
|
||||
@@ -32,8 +40,8 @@ const Confetti = forwardRef<ConfettiRef, Props>((props, ref) => {
|
||||
manualstart = false,
|
||||
children,
|
||||
...rest
|
||||
} = props
|
||||
const instanceRef = useRef<ConfettiInstance | null>(null) // confetti instance
|
||||
} = props;
|
||||
const instanceRef = useRef<ConfettiInstance | null>(null); // confetti instance
|
||||
|
||||
const canvasRef = useCallback(
|
||||
// https://react.dev/reference/react-dom/components/common#ref-callback
|
||||
@@ -41,79 +49,76 @@ const Confetti = forwardRef<ConfettiRef, Props>((props, ref) => {
|
||||
(node: HTMLCanvasElement) => {
|
||||
if (node !== null) {
|
||||
// <canvas> is mounted => create the confetti instance
|
||||
if (instanceRef.current)
|
||||
return // if not already created
|
||||
if (instanceRef.current) return; // if not already created
|
||||
instanceRef.current = confetti.create(node, {
|
||||
...globalOptions,
|
||||
resize: true,
|
||||
})
|
||||
}
|
||||
else {
|
||||
});
|
||||
} else {
|
||||
// <canvas> is unmounted => reset and destroy instanceRef
|
||||
if (instanceRef.current) {
|
||||
instanceRef.current.reset()
|
||||
instanceRef.current = null
|
||||
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])
|
||||
useImperativeHandle(ref, () => api, [api]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!manualstart)
|
||||
fire()
|
||||
}, [manualstart, fire])
|
||||
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
|
||||
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
|
||||
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 { Confetti, ConfettiButton };
|
||||
|
||||
export default Confetti
|
||||
export default Confetti;
|
||||
|
||||
@@ -1,112 +1,110 @@
|
||||
'use client'
|
||||
import type { ReactNode } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
"use client";
|
||||
import type { ReactNode } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
export interface BaseParticle {
|
||||
element: HTMLElement | SVGSVGElement
|
||||
left: number
|
||||
size: number
|
||||
top: number
|
||||
element: HTMLElement | SVGSVGElement;
|
||||
left: number;
|
||||
size: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
export interface BaseParticleOptions {
|
||||
particle?: string
|
||||
size?: number
|
||||
particle?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface CoolParticle extends BaseParticle {
|
||||
direction: number
|
||||
speedHorz: number
|
||||
speedUp: number
|
||||
spinSpeed: number
|
||||
spinVal: number
|
||||
direction: number;
|
||||
speedHorz: number;
|
||||
speedUp: number;
|
||||
spinSpeed: number;
|
||||
spinVal: number;
|
||||
}
|
||||
|
||||
export interface CoolParticleOptions extends BaseParticleOptions {
|
||||
particleCount?: number
|
||||
speedHorz?: number
|
||||
speedUp?: number
|
||||
particleCount?: number;
|
||||
speedHorz?: number;
|
||||
speedUp?: number;
|
||||
}
|
||||
|
||||
function getContainer() {
|
||||
const id = '_coolMode_effect'
|
||||
const existingContainer = document.getElementById(id)
|
||||
const id = "_coolMode_effect";
|
||||
const existingContainer = document.getElementById(id);
|
||||
|
||||
if (existingContainer)
|
||||
return existingContainer
|
||||
if (existingContainer) return existingContainer;
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.setAttribute('id', id)
|
||||
const container = document.createElement("div");
|
||||
container.setAttribute("id", id);
|
||||
container.setAttribute(
|
||||
'style',
|
||||
'overflow:hidden; position:fixed; height:100%; top:0; left:0; right:0; bottom:0; pointer-events:none; z-index:2147483647',
|
||||
)
|
||||
"style",
|
||||
"overflow:hidden; position:fixed; height:100%; top:0; left:0; right:0; bottom:0; pointer-events:none; z-index:2147483647",
|
||||
);
|
||||
|
||||
document.body.appendChild(container)
|
||||
document.body.appendChild(container);
|
||||
|
||||
return container
|
||||
return container;
|
||||
}
|
||||
|
||||
let instanceCounter = 0
|
||||
let instanceCounter = 0;
|
||||
|
||||
function applyParticleEffect(
|
||||
element: HTMLElement,
|
||||
options?: CoolParticleOptions,
|
||||
): () => void {
|
||||
instanceCounter++
|
||||
instanceCounter++;
|
||||
|
||||
const defaultParticle = 'circle'
|
||||
const particleType = options?.particle || defaultParticle
|
||||
const sizes = [15, 20, 25, 35, 45]
|
||||
const limit = 45
|
||||
const defaultParticle = "circle";
|
||||
const particleType = options?.particle || defaultParticle;
|
||||
const sizes = [15, 20, 25, 35, 45];
|
||||
const limit = 45;
|
||||
|
||||
let particles: CoolParticle[] = []
|
||||
let autoAddParticle = false
|
||||
let mouseX = 0
|
||||
let mouseY = 0
|
||||
let particles: CoolParticle[] = [];
|
||||
let autoAddParticle = false;
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
|
||||
const container = getContainer()
|
||||
const container = getContainer();
|
||||
|
||||
function generateParticle() {
|
||||
const size
|
||||
= options?.size || sizes[Math.floor(Math.random() * sizes.length)]
|
||||
const speedHorz = options?.speedHorz || Math.random() * 10
|
||||
const speedUp = options?.speedUp || Math.random() * 25
|
||||
const spinVal = Math.random() * 360
|
||||
const spinSpeed = Math.random() * 35 * (Math.random() <= 0.5 ? -1 : 1)
|
||||
const top = mouseY - size / 2
|
||||
const left = mouseX - size / 2
|
||||
const direction = Math.random() <= 0.5 ? -1 : 1
|
||||
const size =
|
||||
options?.size || sizes[Math.floor(Math.random() * sizes.length)];
|
||||
const speedHorz = options?.speedHorz || Math.random() * 10;
|
||||
const speedUp = options?.speedUp || Math.random() * 25;
|
||||
const spinVal = Math.random() * 360;
|
||||
const spinSpeed = Math.random() * 35 * (Math.random() <= 0.5 ? -1 : 1);
|
||||
const top = mouseY - size / 2;
|
||||
const left = mouseX - size / 2;
|
||||
const direction = Math.random() <= 0.5 ? -1 : 1;
|
||||
|
||||
const particle = document.createElement('div')
|
||||
const particle = document.createElement("div");
|
||||
|
||||
if (particleType === 'circle') {
|
||||
const svgNS = 'http://www.w3.org/2000/svg'
|
||||
const circleSVG = document.createElementNS(svgNS, 'svg')
|
||||
const circle = document.createElementNS(svgNS, 'circle')
|
||||
circle.setAttributeNS(null, 'cx', (size / 2).toString())
|
||||
circle.setAttributeNS(null, 'cy', (size / 2).toString())
|
||||
circle.setAttributeNS(null, 'r', (size / 2).toString())
|
||||
if (particleType === "circle") {
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
const circleSVG = document.createElementNS(svgNS, "svg");
|
||||
const circle = document.createElementNS(svgNS, "circle");
|
||||
circle.setAttributeNS(null, "cx", (size / 2).toString());
|
||||
circle.setAttributeNS(null, "cy", (size / 2).toString());
|
||||
circle.setAttributeNS(null, "r", (size / 2).toString());
|
||||
circle.setAttributeNS(
|
||||
null,
|
||||
'fill',
|
||||
"fill",
|
||||
`hsl(${Math.random() * 360}, 70%, 50%)`,
|
||||
)
|
||||
);
|
||||
|
||||
circleSVG.appendChild(circle)
|
||||
circleSVG.setAttribute('width', size.toString())
|
||||
circleSVG.setAttribute('height', size.toString())
|
||||
circleSVG.appendChild(circle);
|
||||
circleSVG.setAttribute("width", size.toString());
|
||||
circleSVG.setAttribute("height", size.toString());
|
||||
|
||||
particle.appendChild(circleSVG)
|
||||
}
|
||||
else {
|
||||
particle.innerHTML = `<img src="${particleType}" width="${size}" height="${size}" style="border-radius: 50%">`
|
||||
particle.appendChild(circleSVG);
|
||||
} else {
|
||||
particle.innerHTML = `<img src="${particleType}" width="${size}" height="${size}" style="border-radius: 50%">`;
|
||||
}
|
||||
|
||||
particle.style.position = 'absolute'
|
||||
particle.style.transform = `translate3d(${left}px, ${top}px, 0px) rotate(${spinVal}deg)`
|
||||
particle.style.position = "absolute";
|
||||
particle.style.transform = `translate3d(${left}px, ${top}px, 0px) rotate(${spinVal}deg)`;
|
||||
|
||||
container.appendChild(particle)
|
||||
container.appendChild(particle);
|
||||
|
||||
particles.push({
|
||||
direction,
|
||||
@@ -118,122 +116,119 @@ function applyParticleEffect(
|
||||
spinSpeed,
|
||||
spinVal,
|
||||
top,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function refreshParticles() {
|
||||
particles.forEach((p) => {
|
||||
p.left = p.left - p.speedHorz * p.direction
|
||||
p.top = p.top - p.speedUp
|
||||
p.speedUp = Math.min(p.size, p.speedUp - 1)
|
||||
p.spinVal = p.spinVal + p.spinSpeed
|
||||
p.left = p.left - p.speedHorz * p.direction;
|
||||
p.top = p.top - p.speedUp;
|
||||
p.speedUp = Math.min(p.size, p.speedUp - 1);
|
||||
p.spinVal = p.spinVal + p.spinSpeed;
|
||||
|
||||
if (
|
||||
p.top
|
||||
>= Math.max(window.innerHeight, document.body.clientHeight) + p.size
|
||||
p.top >=
|
||||
Math.max(window.innerHeight, document.body.clientHeight) + p.size
|
||||
) {
|
||||
particles = particles.filter(o => o !== p)
|
||||
p.element.remove()
|
||||
particles = particles.filter((o) => o !== p);
|
||||
p.element.remove();
|
||||
}
|
||||
|
||||
p.element.setAttribute(
|
||||
'style',
|
||||
"style",
|
||||
[
|
||||
'position:absolute',
|
||||
'will-change:transform',
|
||||
"position:absolute",
|
||||
"will-change:transform",
|
||||
`top:${p.top}px`,
|
||||
`left:${p.left}px`,
|
||||
`transform:rotate(${p.spinVal}deg)`,
|
||||
].join(';'),
|
||||
)
|
||||
})
|
||||
].join(";"),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let animationFrame: number | undefined
|
||||
let animationFrame: number | undefined;
|
||||
|
||||
let lastParticleTimestamp = 0
|
||||
const particleGenerationDelay = 30
|
||||
let lastParticleTimestamp = 0;
|
||||
const particleGenerationDelay = 30;
|
||||
|
||||
function loop() {
|
||||
const currentTime = performance.now()
|
||||
const currentTime = performance.now();
|
||||
if (
|
||||
autoAddParticle
|
||||
&& particles.length < limit
|
||||
&& currentTime - lastParticleTimestamp > particleGenerationDelay
|
||||
autoAddParticle &&
|
||||
particles.length < limit &&
|
||||
currentTime - lastParticleTimestamp > particleGenerationDelay
|
||||
) {
|
||||
generateParticle()
|
||||
lastParticleTimestamp = currentTime
|
||||
generateParticle();
|
||||
lastParticleTimestamp = currentTime;
|
||||
}
|
||||
|
||||
refreshParticles()
|
||||
animationFrame = requestAnimationFrame(loop)
|
||||
refreshParticles();
|
||||
animationFrame = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
loop()
|
||||
loop();
|
||||
|
||||
const isTouchInteraction = 'ontouchstart' in window
|
||||
const isTouchInteraction = "ontouchstart" in window;
|
||||
|
||||
const tap = isTouchInteraction ? 'touchstart' : 'mousedown'
|
||||
const tapEnd = isTouchInteraction ? 'touchend' : 'mouseup'
|
||||
const move = isTouchInteraction ? 'touchmove' : 'mousemove'
|
||||
const tap = isTouchInteraction ? "touchstart" : "mousedown";
|
||||
const tapEnd = isTouchInteraction ? "touchend" : "mouseup";
|
||||
const move = isTouchInteraction ? "touchmove" : "mousemove";
|
||||
|
||||
const updateMousePosition = (e: MouseEvent | TouchEvent) => {
|
||||
if ('touches' in e) {
|
||||
mouseX = e.touches?.[0].clientX
|
||||
mouseY = e.touches?.[0].clientY
|
||||
}
|
||||
else {
|
||||
mouseX = e.clientX
|
||||
mouseY = e.clientY
|
||||
}
|
||||
if ("touches" in e) {
|
||||
mouseX = e.touches?.[0].clientX;
|
||||
mouseY = e.touches?.[0].clientY;
|
||||
} else {
|
||||
mouseX = e.clientX;
|
||||
mouseY = e.clientY;
|
||||
}
|
||||
};
|
||||
|
||||
const tapHandler = (e: MouseEvent | TouchEvent) => {
|
||||
updateMousePosition(e)
|
||||
autoAddParticle = true
|
||||
}
|
||||
updateMousePosition(e);
|
||||
autoAddParticle = true;
|
||||
};
|
||||
|
||||
const disableAutoAddParticle = () => {
|
||||
autoAddParticle = false
|
||||
}
|
||||
autoAddParticle = false;
|
||||
};
|
||||
|
||||
element.addEventListener(move, updateMousePosition, { passive: true })
|
||||
element.addEventListener(tap, tapHandler, { passive: true })
|
||||
element.addEventListener(tapEnd, disableAutoAddParticle, { passive: true })
|
||||
element.addEventListener('mouseleave', disableAutoAddParticle, {
|
||||
element.addEventListener(move, updateMousePosition, { passive: true });
|
||||
element.addEventListener(tap, tapHandler, { passive: true });
|
||||
element.addEventListener(tapEnd, disableAutoAddParticle, { passive: true });
|
||||
element.addEventListener("mouseleave", disableAutoAddParticle, {
|
||||
passive: true,
|
||||
})
|
||||
});
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(move, updateMousePosition)
|
||||
element.removeEventListener(tap, tapHandler)
|
||||
element.removeEventListener(tapEnd, disableAutoAddParticle)
|
||||
element.removeEventListener('mouseleave', disableAutoAddParticle)
|
||||
element.removeEventListener(move, updateMousePosition);
|
||||
element.removeEventListener(tap, tapHandler);
|
||||
element.removeEventListener(tapEnd, disableAutoAddParticle);
|
||||
element.removeEventListener("mouseleave", disableAutoAddParticle);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (animationFrame && particles.length === 0) {
|
||||
cancelAnimationFrame(animationFrame)
|
||||
clearInterval(interval)
|
||||
cancelAnimationFrame(animationFrame);
|
||||
clearInterval(interval);
|
||||
|
||||
if (--instanceCounter === 0)
|
||||
container.remove()
|
||||
}
|
||||
}, 500)
|
||||
if (--instanceCounter === 0) container.remove();
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
}
|
||||
|
||||
interface CoolModeProps {
|
||||
children: ReactNode
|
||||
options?: CoolParticleOptions
|
||||
children: ReactNode;
|
||||
options?: CoolParticleOptions;
|
||||
}
|
||||
|
||||
export const CoolMode: React.FC<CoolModeProps> = ({ children, options }) => {
|
||||
const ref = useRef<HTMLElement>(null)
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current)
|
||||
return applyParticleEffect(ref.current, options)
|
||||
}, [options])
|
||||
if (ref.current) return applyParticleEffect(ref.current, options);
|
||||
}, [options]);
|
||||
|
||||
return React.cloneElement(children as React.ReactElement, { ref })
|
||||
}
|
||||
return React.cloneElement(children as React.ReactElement, { ref });
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import { Cross2Icon } from '@radix-ui/react-icons'
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
@@ -21,13 +21,13 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
@@ -38,20 +38,20 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
|
||||
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
function DialogHeader({
|
||||
className,
|
||||
@@ -60,14 +60,14 @@ function DialogHeader({
|
||||
return (
|
||||
<div
|
||||
className={ny(
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
DialogHeader.displayName = 'DialogHeader'
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
@@ -76,14 +76,14 @@ function DialogFooter({
|
||||
return (
|
||||
<div
|
||||
className={ny(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
DialogFooter.displayName = 'DialogFooter'
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
@@ -92,13 +92,13 @@ const DialogTitle = React.forwardRef<
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
@@ -106,11 +106,11 @@ const DialogDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={ny('text-muted-foreground text-sm', className)}
|
||||
className={ny("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
@@ -123,4 +123,4 @@ export {
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useId } from 'react'
|
||||
import { ny } from '@/lib/utils'
|
||||
import { useId } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface DotPatternProps {
|
||||
width?: any
|
||||
height?: any
|
||||
x?: any
|
||||
y?: any
|
||||
cx?: any
|
||||
cy?: any
|
||||
cr?: any
|
||||
className?: string
|
||||
[key: string]: any
|
||||
width?: any;
|
||||
height?: any;
|
||||
x?: any;
|
||||
y?: any;
|
||||
cx?: any;
|
||||
cy?: any;
|
||||
cr?: any;
|
||||
className?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
export function DotPattern({
|
||||
width = 16,
|
||||
@@ -23,13 +23,13 @@ export function DotPattern({
|
||||
className,
|
||||
...props
|
||||
}: DotPatternProps) {
|
||||
const id = useId()
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={ny(
|
||||
'pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80',
|
||||
"pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -49,7 +49,7 @@ export function DotPattern({
|
||||
</defs>
|
||||
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default DotPattern
|
||||
export default DotPattern;
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
DotFilledIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
} from "@radix-ui/react-icons";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
|
||||
inset && 'pl-8',
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -40,9 +40,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName
|
||||
= DropdownMenuPrimitive.SubTrigger.displayName
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
@@ -51,14 +51,14 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName
|
||||
= DropdownMenuPrimitive.SubContent.displayName
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
@@ -69,33 +69,33 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={ny(
|
||||
'bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-md',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
@@ -104,7 +104,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
@@ -117,9 +117,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName
|
||||
= DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
@@ -128,7 +128,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -140,26 +140,26 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'px-2 py-1.5 text-sm font-semibold',
|
||||
inset && 'pl-8',
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
@@ -167,11 +167,11 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={ny('bg-muted -mx-1 my-1 h-px', className)}
|
||||
className={ny("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
@@ -179,12 +179,12 @@ function DropdownMenuShortcut({
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) {
|
||||
return (
|
||||
<span
|
||||
className={ny('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
className={ny("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
@@ -202,4 +202,4 @@ export {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import type { Variants } from 'framer-motion'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useMemo } from 'react'
|
||||
import type { Variants } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface FadeTextProps {
|
||||
className?: string
|
||||
direction?: 'up' | 'down' | 'left' | 'right'
|
||||
framerProps?: Variants
|
||||
text: string
|
||||
className?: string;
|
||||
direction?: "up" | "down" | "left" | "right";
|
||||
framerProps?: Variants;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function FadeText({
|
||||
direction = 'up',
|
||||
direction = "up",
|
||||
className,
|
||||
framerProps = {
|
||||
hidden: { opacity: 0 },
|
||||
show: { opacity: 1, transition: { type: 'spring' } },
|
||||
show: { opacity: 1, transition: { type: "spring" } },
|
||||
},
|
||||
text,
|
||||
}: FadeTextProps) {
|
||||
const directionOffset = useMemo(() => {
|
||||
const map = { up: 10, down: -10, left: -10, right: 10 }
|
||||
return map[direction]
|
||||
}, [direction])
|
||||
const map = { up: 10, down: -10, left: -10, right: 10 };
|
||||
return map[direction];
|
||||
}, [direction]);
|
||||
|
||||
const axis = direction === 'up' || direction === 'down' ? 'y' : 'x'
|
||||
const axis = direction === "up" || direction === "down" ? "y" : "x";
|
||||
|
||||
const FADE_ANIMATION_VARIANTS = useMemo(() => {
|
||||
const { hidden, show, ...rest } = framerProps as {
|
||||
[name: string]: { [name: string]: number, opacity: number }
|
||||
}
|
||||
[name: string]: { [name: string]: number; opacity: number };
|
||||
};
|
||||
|
||||
return {
|
||||
...rest,
|
||||
@@ -44,8 +44,8 @@ export function FadeText({
|
||||
opacity: show?.opacity ?? 1,
|
||||
[axis]: show?.[axis] ?? 0,
|
||||
},
|
||||
}
|
||||
}, [directionOffset, axis, framerProps])
|
||||
};
|
||||
}, [directionOffset, axis, framerProps]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -56,5 +56,5 @@ export function FadeText({
|
||||
>
|
||||
<motion.span className={className}>{text}</motion.span>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import * as React from 'react'
|
||||
import type * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'
|
||||
import { Controller, FormProvider, useFormContext, useFormState } from 'react-hook-form'
|
||||
import * as React from "react";
|
||||
import type * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form";
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ny } from "@/lib/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const Form = FormProvider
|
||||
const Form = FormProvider;
|
||||
|
||||
interface FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> {
|
||||
name: TName
|
||||
name: TName;
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
)
|
||||
);
|
||||
|
||||
function FormField<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
@@ -28,20 +33,20 @@ function FormField<
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function useFormField() {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext)
|
||||
throw new Error('useFormField should be used within <FormField>')
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
|
||||
const { id } = itemContext
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -50,54 +55,54 @@ function useFormField() {
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface FormItemContextValue {
|
||||
id: string
|
||||
id: string;
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
)
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={ny('space-y-2', className)} {...props} />
|
||||
<div ref={ref} className={ny("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = 'FormItem'
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={ny(error && 'text-destructive', className)}
|
||||
className={ny(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = 'FormLabel'
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId }
|
||||
= useFormField()
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
@@ -111,70 +116,68 @@ const FormControl = React.forwardRef<
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = 'FormControl'
|
||||
);
|
||||
});
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={ny('text-[0.8rem] text-muted-foreground', className)}
|
||||
className={ny("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = 'FormDescription'
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body)
|
||||
return null
|
||||
if (!body) return null;
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={ny('text-[0.8rem] font-medium text-destructive', className)}
|
||||
className={ny("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = 'FormMessage'
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
const FormGlobalError = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { errors } = useFormState()
|
||||
const rootError = errors.root
|
||||
if (!rootError)
|
||||
return null
|
||||
const { errors } = useFormState();
|
||||
const rootError = errors.root;
|
||||
if (!rootError) return null;
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
className={ny('text-sm font-medium text-destructive', className)}
|
||||
className={ny("text-sm font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{rootError.message}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormGlobalError.displayName = 'FormGlobalError'
|
||||
);
|
||||
});
|
||||
FormGlobalError.displayName = "FormGlobalError";
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
@@ -186,4 +189,4 @@ export {
|
||||
FormMessage,
|
||||
FormGlobalError,
|
||||
FormField,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import type { Variants } from 'framer-motion'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ny } from '@/lib/utils'
|
||||
import type { Variants } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface GradualSpacingProps {
|
||||
text: string
|
||||
duration?: number
|
||||
delayMultiple?: number
|
||||
framerProps?: Variants
|
||||
className?: string
|
||||
text: string;
|
||||
duration?: number;
|
||||
delayMultiple?: number;
|
||||
framerProps?: Variants;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function GradualSpacing({
|
||||
@@ -25,7 +25,7 @@ export default function GradualSpacing({
|
||||
return (
|
||||
<div className="flex justify-center space-x-1">
|
||||
<AnimatePresence>
|
||||
{text.split('').map((char, i) => (
|
||||
{text.split("").map((char, i) => (
|
||||
<motion.h1
|
||||
key={i}
|
||||
initial="hidden"
|
||||
@@ -33,12 +33,12 @@ export default function GradualSpacing({
|
||||
exit="hidden"
|
||||
variants={framerProps}
|
||||
transition={{ duration, delay: i * delayMultiple }}
|
||||
className={ny('drop-shadow-sm ', className)}
|
||||
className={ny("drop-shadow-sm", className)}
|
||||
>
|
||||
{char === ' ' ? <span> </span> : char}
|
||||
{char === " " ? <span> </span> : char}
|
||||
</motion.h1>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useId } from 'react'
|
||||
import { ny } from '@/lib/utils'
|
||||
import { useId } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface GridPatternProps {
|
||||
width?: any
|
||||
height?: any
|
||||
x?: any
|
||||
y?: any
|
||||
squares?: Array<[x: number, y: number]>
|
||||
strokeDasharray?: any
|
||||
className?: string
|
||||
[key: string]: any
|
||||
width?: any;
|
||||
height?: any;
|
||||
x?: any;
|
||||
y?: any;
|
||||
squares?: Array<[x: number, y: number]>;
|
||||
strokeDasharray?: any;
|
||||
className?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function GridPattern({
|
||||
@@ -22,13 +22,13 @@ export function GridPattern({
|
||||
className,
|
||||
...props
|
||||
}: GridPatternProps) {
|
||||
const id = useId()
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={ny(
|
||||
'pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30',
|
||||
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -65,7 +65,7 @@ export function GridPattern({
|
||||
</svg>
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default GridPattern
|
||||
export default GridPattern;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
)
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
@@ -20,7 +20,7 @@ const Label = React.forwardRef<
|
||||
className={ny(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
import { ny } from "@/lib/utils"
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
@@ -13,15 +13,15 @@ const NavigationMenu = React.forwardRef<
|
||||
ref={ref}
|
||||
className={ny(
|
||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
))
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||
));
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
@@ -31,18 +31,18 @@ const NavigationMenuList = React.forwardRef<
|
||||
ref={ref}
|
||||
className={ny(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||
));
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
|
||||
)
|
||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
|
||||
);
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
@@ -59,8 +59,8 @@ const NavigationMenuTrigger = React.forwardRef<
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
))
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||
));
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
@@ -70,14 +70,14 @@ const NavigationMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={ny(
|
||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||
));
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
@@ -87,15 +87,15 @@ const NavigationMenuViewport = React.forwardRef<
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={ny(
|
||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-fit overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
));
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName
|
||||
NavigationMenuPrimitive.Viewport.displayName;
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
@@ -105,15 +105,15 @@ const NavigationMenuIndicator = React.forwardRef<
|
||||
ref={ref}
|
||||
className={ny(
|
||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
))
|
||||
));
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName
|
||||
NavigationMenuPrimitive.Indicator.displayName;
|
||||
|
||||
export {
|
||||
navigationMenuTriggerStyle,
|
||||
@@ -125,4 +125,4 @@ export {
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ny } from '@/lib/utils'
|
||||
import type { CSSProperties, ReactElement, ReactNode } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface NeonColorsProps {
|
||||
firstColor: string
|
||||
secondColor: string
|
||||
firstColor: string;
|
||||
secondColor: string;
|
||||
}
|
||||
|
||||
interface NeonGradientCardProps {
|
||||
@@ -16,14 +16,14 @@ interface NeonGradientCardProps {
|
||||
* @description
|
||||
* The component to be rendered as the card
|
||||
*/
|
||||
as?: ReactElement
|
||||
as?: ReactElement;
|
||||
/**
|
||||
* @default ""
|
||||
* @type string
|
||||
* @description
|
||||
* The className of the card
|
||||
*/
|
||||
className?: string
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* @default ""
|
||||
@@ -31,7 +31,7 @@ interface NeonGradientCardProps {
|
||||
* @description
|
||||
* The children of the card
|
||||
*/
|
||||
children?: ReactNode
|
||||
children?: ReactNode;
|
||||
|
||||
/**
|
||||
* @default 5
|
||||
@@ -39,7 +39,7 @@ interface NeonGradientCardProps {
|
||||
* @description
|
||||
* The size of the border in pixels
|
||||
*/
|
||||
borderSize?: number
|
||||
borderSize?: number;
|
||||
|
||||
/**
|
||||
* @default 20
|
||||
@@ -47,7 +47,7 @@ interface NeonGradientCardProps {
|
||||
* @description
|
||||
* The size of the radius in pixels
|
||||
*/
|
||||
borderRadius?: number
|
||||
borderRadius?: number;
|
||||
|
||||
/**
|
||||
* @default "{ firstColor: '#ff00aa', secondColor: '#00FFF1' }"
|
||||
@@ -55,9 +55,9 @@ interface NeonGradientCardProps {
|
||||
* @description
|
||||
* The colors of the neon gradient
|
||||
*/
|
||||
neonColors?: NeonColorsProps
|
||||
neonColors?: NeonColorsProps;
|
||||
|
||||
[key: string]: any
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const NeonGradientCard: React.FC<NeonGradientCardProps> = ({
|
||||
@@ -66,79 +66,79 @@ const NeonGradientCard: React.FC<NeonGradientCardProps> = ({
|
||||
borderSize = 2,
|
||||
borderRadius = 20,
|
||||
neonColors = {
|
||||
firstColor: '#ff00aa',
|
||||
secondColor: '#00FFF1',
|
||||
firstColor: "#ff00aa",
|
||||
secondColor: "#00FFF1",
|
||||
},
|
||||
...props
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (containerRef.current) {
|
||||
const { offsetWidth, offsetHeight } = containerRef.current
|
||||
setDimensions({ width: offsetWidth, height: offsetHeight })
|
||||
}
|
||||
const { offsetWidth, offsetHeight } = containerRef.current;
|
||||
setDimensions({ width: offsetWidth, height: offsetHeight });
|
||||
}
|
||||
};
|
||||
|
||||
updateDimensions()
|
||||
window.addEventListener('resize', updateDimensions)
|
||||
updateDimensions();
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateDimensions)
|
||||
}
|
||||
}, [])
|
||||
window.removeEventListener("resize", updateDimensions);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const { offsetWidth, offsetHeight } = containerRef.current
|
||||
setDimensions({ width: offsetWidth, height: offsetHeight })
|
||||
const { offsetWidth, offsetHeight } = containerRef.current;
|
||||
setDimensions({ width: offsetWidth, height: offsetHeight });
|
||||
}
|
||||
}, [children])
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={
|
||||
{
|
||||
'--border-size': `${borderSize}px`,
|
||||
'--border-radius': `${borderRadius}px`,
|
||||
'--neon-first-color': neonColors.firstColor,
|
||||
'--neon-second-color': neonColors.secondColor,
|
||||
'--card-width': `${dimensions.width}px`,
|
||||
'--card-height': `${dimensions.height}px`,
|
||||
'--card-content-radius': `${borderRadius - borderSize}px`,
|
||||
'--pseudo-element-background-image': `linear-gradient(0deg, ${neonColors.firstColor}, ${neonColors.secondColor})`,
|
||||
'--pseudo-element-width': `${dimensions.width + borderSize * 2}px`,
|
||||
'--pseudo-element-height': `${dimensions.height + borderSize * 2}px`,
|
||||
'--after-blur': `${dimensions.width / 3}px`,
|
||||
"--border-size": `${borderSize}px`,
|
||||
"--border-radius": `${borderRadius}px`,
|
||||
"--neon-first-color": neonColors.firstColor,
|
||||
"--neon-second-color": neonColors.secondColor,
|
||||
"--card-width": `${dimensions.width}px`,
|
||||
"--card-height": `${dimensions.height}px`,
|
||||
"--card-content-radius": `${borderRadius - borderSize}px`,
|
||||
"--pseudo-element-background-image": `linear-gradient(0deg, ${neonColors.firstColor}, ${neonColors.secondColor})`,
|
||||
"--pseudo-element-width": `${dimensions.width + borderSize * 2}px`,
|
||||
"--pseudo-element-height": `${dimensions.height + borderSize * 2}px`,
|
||||
"--after-blur": `${dimensions.width / 3}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
className={ny(
|
||||
'relative z-10 size-full rounded-[var(--border-radius)]',
|
||||
"relative z-10 size-full rounded-[var(--border-radius)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={ny(
|
||||
'relative size-full min-h-[inherit] rounded-[var(--card-content-radius)] bg-gray-100 p-6',
|
||||
'before:absolute before:-left-[var(--border-size)] before:-top-[var(--border-size)] before:-z-10 before:block',
|
||||
'before:h-[var(--pseudo-element-height)] before:w-[var(--pseudo-element-width)] before:rounded-[var(--border-radius)] before:content-[\'\']',
|
||||
'before:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] before:bg-[length:100%_200%]',
|
||||
'before:animate-backgroundPositionSpin',
|
||||
'after:absolute after:-left-[var(--border-size)] after:-top-[var(--border-size)] after:-z-10 after:block',
|
||||
'after:h-[var(--pseudo-element-height)] after:w-[var(--pseudo-element-width)] after:rounded-[var(--border-radius)] after:blur-[var(--after-blur)] after:content-[\'\']',
|
||||
'after:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] after:bg-[length:100%_200%] after:opacity-80',
|
||||
'after:animate-backgroundPositionSpin',
|
||||
'dark:bg-neutral-900',
|
||||
"relative size-full min-h-[inherit] rounded-[var(--card-content-radius)] bg-gray-100 p-6",
|
||||
"before:absolute before:-left-[var(--border-size)] before:-top-[var(--border-size)] before:-z-10 before:block",
|
||||
"before:h-[var(--pseudo-element-height)] before:w-[var(--pseudo-element-width)] before:rounded-[var(--border-radius)] before:content-['']",
|
||||
"before:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] before:bg-[length:100%_200%]",
|
||||
"before:animate-backgroundPositionSpin",
|
||||
"after:absolute after:-left-[var(--border-size)] after:-top-[var(--border-size)] after:-z-10 after:block",
|
||||
"after:h-[var(--pseudo-element-height)] after:w-[var(--pseudo-element-width)] after:rounded-[var(--border-radius)] after:blur-[var(--after-blur)] after:content-['']",
|
||||
"after:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] after:bg-[length:100%_200%] after:opacity-80",
|
||||
"after:animate-backgroundPositionSpin",
|
||||
"dark:bg-neutral-900",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export { NeonGradientCard }
|
||||
export { NeonGradientCard };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
export default function OrbitingCircles({
|
||||
className,
|
||||
@@ -9,13 +9,13 @@ export default function OrbitingCircles({
|
||||
radius = 50,
|
||||
path = true,
|
||||
}: {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
reverse?: boolean
|
||||
duration?: number
|
||||
delay?: number
|
||||
radius?: number
|
||||
path?: boolean
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
reverse?: boolean;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
radius?: number;
|
||||
path?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
@@ -39,19 +39,19 @@ export default function OrbitingCircles({
|
||||
<div
|
||||
style={
|
||||
{
|
||||
'--duration': duration,
|
||||
'--radius': radius,
|
||||
'--delay': -delay,
|
||||
"--duration": duration,
|
||||
"--radius": radius,
|
||||
"--delay": -delay,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={ny(
|
||||
'animate-orbit absolute flex size-full transform-gpu items-center justify-center rounded-full border bg-black/10 [animation-delay:calc(var(--delay)*1000ms)] dark:bg-white/10',
|
||||
{ '[animation-direction:reverse]': reverse },
|
||||
"absolute flex size-full transform-gpu animate-orbit items-center justify-center rounded-full border bg-black/10 [animation-delay:calc(var(--delay)*1000ms)] dark:bg-white/10",
|
||||
{ "[animation-direction:reverse]": reverse },
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,150 +1,152 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface MousePosition {
|
||||
x: number
|
||||
y: number
|
||||
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 })
|
||||
}
|
||||
setMousePosition({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}, [])
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return mousePosition
|
||||
return mousePosition;
|
||||
}
|
||||
|
||||
interface ParticlesProps {
|
||||
className?: string
|
||||
quantity?: number
|
||||
staticity?: number
|
||||
ease?: number
|
||||
size?: number
|
||||
refresh?: boolean
|
||||
color?: string
|
||||
vx?: number
|
||||
vy?: number
|
||||
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]
|
||||
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 = '',
|
||||
className = "",
|
||||
quantity = 100,
|
||||
staticity = 50,
|
||||
ease = 50,
|
||||
size = 0.4,
|
||||
refresh = false,
|
||||
color = '#ffffff',
|
||||
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
|
||||
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')
|
||||
context.current = canvasRef.current.getContext("2d");
|
||||
}
|
||||
initCanvas()
|
||||
animate()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
initCanvas();
|
||||
animate();
|
||||
window.addEventListener("resize", initCanvas);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
}
|
||||
}, [color])
|
||||
window.removeEventListener("resize", initCanvas);
|
||||
};
|
||||
}, [color]);
|
||||
|
||||
useEffect(() => {
|
||||
onMouseMove()
|
||||
}, [mousePosition.x, mousePosition.y])
|
||||
onMouseMove();
|
||||
}, [mousePosition.x, mousePosition.y]);
|
||||
|
||||
useEffect(() => {
|
||||
initCanvas()
|
||||
}, [refresh])
|
||||
initCanvas();
|
||||
}, [refresh]);
|
||||
|
||||
const initCanvas = () => {
|
||||
resizeCanvas()
|
||||
drawParticles()
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
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
|
||||
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)
|
||||
}
|
||||
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
|
||||
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,
|
||||
@@ -156,26 +158,26 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
dx,
|
||||
dy,
|
||||
magnetism,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const rgb = hexToRgb(color)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
circles.current.push(circle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearContext = () => {
|
||||
if (context.current) {
|
||||
@@ -184,18 +186,18 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
0,
|
||||
canvasSize.current.w,
|
||||
canvasSize.current.h,
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const drawParticles = () => {
|
||||
clearContext()
|
||||
const particleCount = quantity
|
||||
clearContext();
|
||||
const particleCount = quantity;
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const circle = circleParams()
|
||||
drawCircle(circle)
|
||||
}
|
||||
const circle = circleParams();
|
||||
drawCircle(circle);
|
||||
}
|
||||
};
|
||||
|
||||
const remapValue = (
|
||||
value: number,
|
||||
@@ -204,13 +206,13 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
start2: number,
|
||||
end2: number,
|
||||
): number => {
|
||||
const remapped
|
||||
= ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
|
||||
return remapped > 0 ? remapped : 0
|
||||
}
|
||||
const remapped =
|
||||
((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||
return remapped > 0 ? remapped : 0;
|
||||
};
|
||||
|
||||
const animate = () => {
|
||||
clearContext()
|
||||
clearContext();
|
||||
circles.current.forEach((circle: Circle, i: number) => {
|
||||
// Handle the alpha value
|
||||
const edge = [
|
||||
@@ -218,54 +220,53 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
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 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
|
||||
circle.alpha += 0.02;
|
||||
if (circle.alpha > circle.targetAlpha) {
|
||||
circle.alpha = circle.targetAlpha
|
||||
circle.alpha = circle.targetAlpha;
|
||||
}
|
||||
} else {
|
||||
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
||||
}
|
||||
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
|
||||
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)
|
||||
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
|
||||
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)
|
||||
circles.current.splice(i, 1);
|
||||
// create a new circle
|
||||
const newCircle = circleParams()
|
||||
drawCircle(newCircle)
|
||||
const newCircle = circleParams();
|
||||
drawCircle(newCircle);
|
||||
// update the circle position
|
||||
}
|
||||
})
|
||||
window.requestAnimationFrame(animate)
|
||||
}
|
||||
});
|
||||
window.requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} ref={canvasContainerRef} aria-hidden="true">
|
||||
<canvas ref={canvasRef} className="size-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Particles
|
||||
export default Particles;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
interface PulsatingButtonProps {
|
||||
text: string
|
||||
pulseColor: string
|
||||
backgroundColor: string
|
||||
textColor: string
|
||||
animationDuration: string
|
||||
buttonWidth: string
|
||||
buttonHeight: string
|
||||
text: string;
|
||||
pulseColor: string;
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
animationDuration: string;
|
||||
buttonWidth: string;
|
||||
buttonHeight: string;
|
||||
}
|
||||
|
||||
export const PulsatingButton: React.FC<PulsatingButtonProps> = ({
|
||||
@@ -22,22 +22,20 @@ export const PulsatingButton: React.FC<PulsatingButtonProps> = ({
|
||||
buttonHeight,
|
||||
}) => {
|
||||
const pulseKeyframes = {
|
||||
'--tw-pulse-color': pulseColor,
|
||||
'animation': `pulse ${animationDuration} linear infinite`,
|
||||
}
|
||||
"--tw-pulse-color": pulseColor,
|
||||
animation: `pulse ${animationDuration} linear infinite`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex justify-center items-center"
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
className="relative block text-center cursor-pointer flex justify-center items-center"
|
||||
className="relative block flex cursor-pointer items-center justify-center text-center"
|
||||
style={{
|
||||
color: textColor,
|
||||
backgroundColor,
|
||||
width: buttonWidth,
|
||||
height: buttonHeight,
|
||||
borderRadius: '12px',
|
||||
borderRadius: "12px",
|
||||
...pulseKeyframes,
|
||||
}}
|
||||
>
|
||||
@@ -56,7 +54,7 @@ export const PulsatingButton: React.FC<PulsatingButtonProps> = ({
|
||||
}
|
||||
}
|
||||
button::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
@@ -72,7 +70,7 @@ export const PulsatingButton: React.FC<PulsatingButtonProps> = ({
|
||||
</style>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PulsatingButton
|
||||
export default PulsatingButton;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
type Type = 'circle' | 'ellipse'
|
||||
type Type = "circle" | "ellipse";
|
||||
|
||||
type Origin =
|
||||
| 'center'
|
||||
| 'top'
|
||||
| 'bottom'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'top left'
|
||||
| 'top right'
|
||||
| 'bottom left'
|
||||
| 'bottom right'
|
||||
| "center"
|
||||
| "top"
|
||||
| "bottom"
|
||||
| "left"
|
||||
| "right"
|
||||
| "top left"
|
||||
| "top right"
|
||||
| "bottom left"
|
||||
| "bottom right";
|
||||
|
||||
interface RadialProps {
|
||||
/**
|
||||
@@ -19,59 +19,59 @@ interface RadialProps {
|
||||
* @default circle
|
||||
* @type string
|
||||
*/
|
||||
type?: Type
|
||||
type?: Type;
|
||||
/**
|
||||
* The color to transition from
|
||||
* @default #00000000
|
||||
* @type string
|
||||
*/
|
||||
from?: string
|
||||
from?: string;
|
||||
|
||||
/**
|
||||
* The color to transition to
|
||||
* @default #290A5C
|
||||
* @type string
|
||||
*/
|
||||
to?: string
|
||||
to?: string;
|
||||
|
||||
/**
|
||||
* The size of the gradient in pixels
|
||||
* @default 300
|
||||
* @type number
|
||||
*/
|
||||
size?: number
|
||||
size?: number;
|
||||
|
||||
/**
|
||||
* The origin of the gradient
|
||||
* @default center
|
||||
* @type string
|
||||
*/
|
||||
origin?: Origin
|
||||
origin?: Origin;
|
||||
|
||||
/**
|
||||
* The class name to apply to the gradient
|
||||
* @default ""
|
||||
* @type string
|
||||
*/
|
||||
className?: string
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function RadialGradient({
|
||||
type = 'circle',
|
||||
from = 'rgba(120,119,198,0.3)',
|
||||
to = 'hsla(0, 0%, 0%, 0)',
|
||||
type = "circle",
|
||||
from = "rgba(120,119,198,0.3)",
|
||||
to = "hsla(0, 0%, 0%, 0)",
|
||||
size = 300,
|
||||
origin = 'center',
|
||||
origin = "center",
|
||||
className,
|
||||
}: RadialProps) {
|
||||
const styles: CSSProperties = {
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
position: "absolute",
|
||||
pointerEvents: "none",
|
||||
inset: 0,
|
||||
backgroundImage: `radial-gradient(${type} ${size}px at ${origin}, ${from}, ${to})`,
|
||||
};
|
||||
|
||||
return <div className={className} style={styles} />;
|
||||
}
|
||||
|
||||
return <div className={className} style={styles} />
|
||||
}
|
||||
|
||||
export default RadialGradient
|
||||
export default RadialGradient;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
export default function RetroGrid({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={ny(
|
||||
'pointer-events-none absolute h-full w-full overflow-hidden opacity-50 [perspective:200px]',
|
||||
"pointer-events-none absolute h-full w-full overflow-hidden opacity-50 [perspective:200px]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -12,15 +12,15 @@ export default function RetroGrid({ className }: { className?: string }) {
|
||||
<div className="absolute inset-0 [transform:rotateX(35deg)]">
|
||||
<div
|
||||
className={ny(
|
||||
'animate-grid',
|
||||
"animate-grid",
|
||||
|
||||
'[background-repeat:repeat] [background-size:60px_60px] [height:300vh] [inset:0%_0px] [margin-left:-50%] [transform-origin:100%_0_0] [width:600vw]',
|
||||
"[background-repeat:repeat] [background-size:60px_60px] [height:300vh] [inset:0%_0px] [margin-left:-50%] [transform-origin:100%_0_0] [width:600vw]",
|
||||
|
||||
// Light Styles
|
||||
'[background-image:linear-gradient(to_right,rgba(0,0,0,0.3)_1px,transparent_0),linear-gradient(to_bottom,rgba(0,0,0,0.3)_1px,transparent_0)]',
|
||||
"[background-image:linear-gradient(to_right,rgba(0,0,0,0.3)_1px,transparent_0),linear-gradient(to_bottom,rgba(0,0,0,0.3)_1px,transparent_0)]",
|
||||
|
||||
// Dark styles
|
||||
'dark:[background-image:linear-gradient(to_right,rgba(255,255,255,0.2)_1px,transparent_0),linear-gradient(to_bottom,rgba(255,255,255,0.2)_1px,transparent_0)]',
|
||||
"dark:[background-image:linear-gradient(to_right,rgba(255,255,255,0.2)_1px,transparent_0),linear-gradient(to_bottom,rgba(255,255,255,0.2)_1px,transparent_0)]",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -28,5 +28,5 @@ export default function RetroGrid({ className }: { className?: string }) {
|
||||
{/* Background Gradient */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-white to-transparent to-90% dark:from-black" />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
@@ -11,7 +11,7 @@ const ScrollArea = React.forwardRef<
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={ny('relative overflow-hidden', className)}
|
||||
className={ny("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]">
|
||||
@@ -20,29 +20,29 @@ const ScrollArea = React.forwardRef<
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={ny(
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical'
|
||||
&& 'h-full w-2.5 border-l border-l-transparent p-px',
|
||||
orientation === 'horizontal'
|
||||
&& 'h-2.5 flex-col border-t border-t-transparent p-px',
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-px",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-px",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="bg-border relative flex-1 rounded-full" />
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
export { ScrollArea, ScrollBar };
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as React from "react";
|
||||
import {
|
||||
CaretSortIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||
} from "@radix-ui/react-icons";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
@@ -24,12 +24,11 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-left text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 [&>span]:text-left',
|
||||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-left text-sm shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1 [&>span]:text-left",
|
||||
className,
|
||||
)}
|
||||
onPointerDown={(e) => {
|
||||
if (e.pointerType === 'touch')
|
||||
e.preventDefault()
|
||||
if (e.pointerType === "touch") e.preventDefault();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
@@ -38,8 +37,8 @@ const SelectTrigger = React.forwardRef<
|
||||
<CaretSortIcon className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
@@ -48,15 +47,15 @@ const SelectScrollUpButton = React.forwardRef<
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
@@ -65,28 +64,28 @@ const SelectScrollDownButton = React.forwardRef<
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName
|
||||
= SelectPrimitive.ScrollDownButton.displayName
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
position === 'popper'
|
||||
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
@@ -95,9 +94,9 @@ const SelectContent = React.forwardRef<
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={ny(
|
||||
'p-1',
|
||||
position === 'popper'
|
||||
&& 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -105,8 +104,8 @@ const SelectContent = React.forwardRef<
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
@@ -114,11 +113,11 @@ const SelectLabel = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={ny('px-2 py-1.5 text-sm font-semibold', className)}
|
||||
className={ny("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
@@ -127,7 +126,7 @@ const SelectItem = React.forwardRef<
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -139,8 +138,8 @@ const SelectItem = React.forwardRef<
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
@@ -148,11 +147,11 @@ const SelectSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={ny('-mx-1 my-1 h-px bg-muted', className)}
|
||||
className={ny("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
@@ -165,4 +164,4 @@ export {
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||
import { Cross2Icon } from '@radix-ui/react-icons'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
@@ -21,33 +21,33 @@ const SheetOverlay = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={ny(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
side: "right",
|
||||
},
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
@@ -56,7 +56,7 @@ interface SheetContentProps
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
@@ -64,15 +64,15 @@ const SheetContent = React.forwardRef<
|
||||
className={ny(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<Cross2Icon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
function SheetHeader({
|
||||
className,
|
||||
@@ -81,14 +81,14 @@ function SheetHeader({
|
||||
return (
|
||||
<div
|
||||
className={ny(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
SheetHeader.displayName = 'SheetHeader'
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
function SheetFooter({
|
||||
className,
|
||||
@@ -97,14 +97,14 @@ function SheetFooter({
|
||||
return (
|
||||
<div
|
||||
className={ny(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
SheetFooter.displayName = 'SheetFooter'
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
@@ -112,11 +112,11 @@ const SheetTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={ny('text-foreground text-lg font-semibold', className)}
|
||||
className={ny("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
@@ -124,11 +124,11 @@ const SheetDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={ny('text-muted-foreground text-sm', className)}
|
||||
className={ny("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
@@ -141,4 +141,4 @@ export {
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
type TColorProp = `#${string}` | `#${string}`[]
|
||||
type TColorProp = `#${string}` | `#${string}`[];
|
||||
interface ShineBorderProps {
|
||||
borderRadius?: number
|
||||
borderWidth?: number
|
||||
duration?: number
|
||||
color?: TColorProp
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
borderRadius?: number;
|
||||
borderWidth?: number;
|
||||
duration?: number;
|
||||
color?: TColorProp;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,7 @@ export default function ShineBorder({
|
||||
borderRadius = 8,
|
||||
borderWidth = 1,
|
||||
duration = 14,
|
||||
color = '#fff',
|
||||
color = "#fff",
|
||||
className,
|
||||
children,
|
||||
}: ShineBorderProps) {
|
||||
@@ -34,33 +34,32 @@ export default function ShineBorder({
|
||||
<div
|
||||
style={
|
||||
{
|
||||
'--border-radius': `${borderRadius}px`,
|
||||
"--border-radius": `${borderRadius}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={ny(
|
||||
'relative grid min-h-[60px] w-fit min-w-[300px] place-items-center rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white',
|
||||
"relative grid min-h-[60px] w-fit min-w-[300px] place-items-center rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
'--border-width': `${borderWidth}px`,
|
||||
'--border-radius': `${borderRadius}px`,
|
||||
'--border-radius-child': `${borderRadius * 0.2}px`,
|
||||
'--shine-pulse-duration': `${duration}s`,
|
||||
'--mask-linear-gradient': `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
||||
'--background-radial-gradient': `radial-gradient(transparent,transparent, ${
|
||||
!Array.isArray(color) ? color : color.join(',')
|
||||
"--border-width": `${borderWidth}px`,
|
||||
"--border-radius": `${borderRadius}px`,
|
||||
"--border-radius-child": `${borderRadius * 0.2}px`,
|
||||
"--shine-pulse-duration": `${duration}s`,
|
||||
"--mask-linear-gradient": `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
||||
"--background-radial-gradient": `radial-gradient(transparent,transparent, ${
|
||||
!Array.isArray(color) ? color : color.join(",")
|
||||
},transparent,transparent)`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={`before:bg-shine-size before:absolute before:inset-[0] before:aspect-square before:h-full before:w-full before:rounded-[--border-radius] before:p-[--border-width] before:will-change-[background-position] before:content-[""] before:![-webkit-mask-composite:xor] before:![mask-composite:exclude] before:[background-image:var(--background-radial-gradient)] before:[background-size:300%_300%] before:[mask:var(--mask-linear-gradient)] motion-safe:before:animate-[shine-pulse_var(--shine-pulse-duration)_infinite_linear]`}
|
||||
>
|
||||
</div>
|
||||
className={`before:bg-shine-size before:absolute before:inset-[0] before:aspect-square before:h-full before:w-full before:rounded-[--border-radius] before:p-[--border-width] before:will-change-[background-position] before:content-[""] before:![-webkit-mask-composite:xor] before:[background-image:var(--background-radial-gradient)] before:[background-size:300%_300%] before:![mask-composite:exclude] before:[mask:var(--mask-linear-gradient)] motion-safe:before:animate-[shine-pulse_var(--shine-pulse-duration)_infinite_linear]`}
|
||||
></div>
|
||||
<div className="z-[1] h-full w-full rounded-[--border-radius-child]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
'use client'
|
||||
import { type AnimationProps, motion } from 'framer-motion'
|
||||
"use client";
|
||||
import { type AnimationProps, motion } from "framer-motion";
|
||||
|
||||
const animationProps = {
|
||||
initial: { '--x': '100%', 'scale': 0.8 },
|
||||
animate: { '--x': '-100%', 'scale': 1 },
|
||||
initial: { "--x": "100%", scale: 0.8 },
|
||||
animate: { "--x": "-100%", scale: 1 },
|
||||
whileTap: { scale: 0.95 },
|
||||
transition: {
|
||||
repeat: Infinity,
|
||||
repeatType: 'loop',
|
||||
repeatType: "loop",
|
||||
repeatDelay: 1,
|
||||
type: 'spring',
|
||||
type: "spring",
|
||||
stiffness: 20,
|
||||
damping: 15,
|
||||
mass: 2,
|
||||
scale: {
|
||||
type: 'spring',
|
||||
type: "spring",
|
||||
stiffness: 200,
|
||||
damping: 5,
|
||||
mass: 0.5,
|
||||
},
|
||||
},
|
||||
} as AnimationProps
|
||||
} as AnimationProps;
|
||||
|
||||
function ShinyButton({ text = 'shiny-button' }) {
|
||||
function ShinyButton({ text = "shiny-button" }) {
|
||||
return (
|
||||
<motion.button
|
||||
{...animationProps}
|
||||
@@ -32,21 +32,20 @@ function ShinyButton({ text = 'shiny-button' }) {
|
||||
className="relative block h-full w-full text-sm uppercase tracking-wide text-[rgb(0,0,0,65%)] dark:font-light dark:text-[rgb(255,255,255,90%)]"
|
||||
style={{
|
||||
maskImage:
|
||||
'linear-gradient(-75deg,hsl(var(--primary)) calc(var(--x) + 20%),transparent calc(var(--x) + 30%),hsl(var(--primary)) calc(var(--x) + 100%))',
|
||||
"linear-gradient(-75deg,hsl(var(--primary)) calc(var(--x) + 20%),transparent calc(var(--x) + 30%),hsl(var(--primary)) calc(var(--x) + 100%))",
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
mask: 'linear-gradient(rgb(0,0,0), rgb(0,0,0)) content-box,linear-gradient(rgb(0,0,0), rgb(0,0,0))',
|
||||
maskComposite: 'exclude',
|
||||
mask: "linear-gradient(rgb(0,0,0), rgb(0,0,0)) content-box,linear-gradient(rgb(0,0,0), rgb(0,0,0))",
|
||||
maskComposite: "exclude",
|
||||
}}
|
||||
className="absolute inset-0 z-10 block rounded-[inherit] bg-[linear-gradient(-75deg,hsl(var(--primary)/10%)_calc(var(--x)+20%),hsl(var(--primary)/50%)_calc(var(--x)+25%),hsl(var(--primary)/10%)_calc(var(--x)+100%))] p-px"
|
||||
>
|
||||
</span>
|
||||
></span>
|
||||
</motion.button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ShinyButton
|
||||
export default ShinyButton;
|
||||
|
||||
@@ -1,125 +1,177 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
|
||||
showSteps?: 'none' | 'half' | 'full'
|
||||
formatLabel?: (value: number) => string
|
||||
formatLabelSide?: string
|
||||
interface SliderProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
|
||||
showSteps?: "none" | "half" | "full";
|
||||
formatLabel?: (value: number) => string;
|
||||
formatLabelSide?: string;
|
||||
}
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
SliderProps
|
||||
>(({ className, showSteps = 'none', formatLabel, formatLabelSide = 'top', ...props }, ref) => {
|
||||
const { min = 0, max = 100, step = 1, orientation = 'horizontal', value, defaultValue, onValueChange } = props
|
||||
const [hoveredThumbIndex, setHoveredThumbIndex] = React.useState<boolean>(false)
|
||||
const numberOfSteps = Math.floor((max - min) / step)
|
||||
const stepLines = Array.from({ length: numberOfSteps }, (_, index) => index * step + min)
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
showSteps = "none",
|
||||
formatLabel,
|
||||
formatLabelSide = "top",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const {
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
orientation = "horizontal",
|
||||
value,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
} = props;
|
||||
const [hoveredThumbIndex, setHoveredThumbIndex] =
|
||||
React.useState<boolean>(false);
|
||||
const numberOfSteps = Math.floor((max - min) / step);
|
||||
const stepLines = Array.from(
|
||||
{ length: numberOfSteps },
|
||||
(_, index) => index * step + min,
|
||||
);
|
||||
|
||||
const initialValue = Array.isArray(value) ? value : (Array.isArray(defaultValue) ? defaultValue : [min, max])
|
||||
const [localValues, setLocalValues] = React.useState<number[]>(initialValue)
|
||||
const initialValue = Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max];
|
||||
const [localValues, setLocalValues] =
|
||||
React.useState<number[]>(initialValue);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isEqual(value, localValues))
|
||||
setLocalValues(Array.isArray(value) ? value : (Array.isArray(defaultValue) ? defaultValue : [min, max]))
|
||||
}, [min, max, value])
|
||||
setLocalValues(
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
);
|
||||
}, [min, max, value]);
|
||||
|
||||
const handleValueChange = (newValues: number[]) => {
|
||||
setLocalValues(newValues)
|
||||
if (onValueChange)
|
||||
onValueChange(newValues)
|
||||
}
|
||||
setLocalValues(newValues);
|
||||
if (onValueChange) onValueChange(newValues);
|
||||
};
|
||||
|
||||
function isEqual(array1: number[] | undefined, array2: number[] | undefined) {
|
||||
array1 = array1 ?? []
|
||||
array2 = array2 ?? []
|
||||
function isEqual(
|
||||
array1: number[] | undefined,
|
||||
array2: number[] | undefined,
|
||||
) {
|
||||
array1 = array1 ?? [];
|
||||
array2 = array2 ?? [];
|
||||
|
||||
if (array1.length !== array2.length)
|
||||
return false
|
||||
if (array1.length !== array2.length) return false;
|
||||
|
||||
for (let i = 0; i < array1.length; i++) {
|
||||
if (array1[i] !== array2[i])
|
||||
return false
|
||||
if (array1[i] !== array2[i]) return false;
|
||||
}
|
||||
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'relative flex cursor-pointer touch-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
orientation === 'horizontal' ? 'w-full items-center' : 'h-full justify-center',
|
||||
"relative flex cursor-pointer touch-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
orientation === "horizontal"
|
||||
? "w-full items-center"
|
||||
: "h-full justify-center",
|
||||
className,
|
||||
)}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={localValues}
|
||||
onValueChange={value => handleValueChange(value)}
|
||||
onValueChange={(value) => handleValueChange(value)}
|
||||
{...props}
|
||||
onFocus={() => setHoveredThumbIndex(true)}
|
||||
onBlur={() => setHoveredThumbIndex(false)}
|
||||
>
|
||||
<SliderPrimitive.Track className={ny(
|
||||
'bg-primary/20 relative grow overflow-hidden rounded-full',
|
||||
orientation === 'horizontal' ? 'h-1.5 w-full' : 'h-full w-1.5',
|
||||
<SliderPrimitive.Track
|
||||
className={ny(
|
||||
"relative grow overflow-hidden rounded-full bg-primary/20",
|
||||
orientation === "horizontal" ? "h-1.5 w-full" : "h-full w-1.5",
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range className={ny(
|
||||
'bg-primary absolute',
|
||||
orientation === 'horizontal' ? 'h-full' : 'w-full',
|
||||
<SliderPrimitive.Range
|
||||
className={ny(
|
||||
"absolute bg-primary",
|
||||
orientation === "horizontal" ? "h-full" : "w-full",
|
||||
)}
|
||||
/>
|
||||
{showSteps !== undefined && showSteps !== 'none' && stepLines.map((value, index) => {
|
||||
if (value === min || value === max)
|
||||
return null
|
||||
{showSteps !== undefined &&
|
||||
showSteps !== "none" &&
|
||||
stepLines.map((value, index) => {
|
||||
if (value === min || value === max) return null;
|
||||
|
||||
const positionPercentage = ((value - min) / (max - min)) * 100
|
||||
const adjustedPosition = 50 + (positionPercentage - 50) * 0.96
|
||||
const positionPercentage = ((value - min) / (max - min)) * 100;
|
||||
const adjustedPosition = 50 + (positionPercentage - 50) * 0.96;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={ny(
|
||||
{ 'w-0.5 h-2': orientation !== 'vertical', 'w-2 h-0.5': orientation === 'vertical' },
|
||||
'bg-muted-foreground absolute',
|
||||
{
|
||||
'left-1': orientation === 'vertical' && showSteps === 'half',
|
||||
'top-1': orientation !== 'vertical' && showSteps === 'half',
|
||||
'left-0': orientation === 'vertical' && showSteps === 'full',
|
||||
'top-0': orientation !== 'vertical' && showSteps === 'full',
|
||||
'-translate-x-1/2': orientation !== 'vertical',
|
||||
'-translate-y-1/2': orientation === 'vertical',
|
||||
"h-2 w-0.5": orientation !== "vertical",
|
||||
"h-0.5 w-2": orientation === "vertical",
|
||||
},
|
||||
"absolute bg-muted-foreground",
|
||||
{
|
||||
"left-1":
|
||||
orientation === "vertical" && showSteps === "half",
|
||||
"top-1":
|
||||
orientation !== "vertical" && showSteps === "half",
|
||||
"left-0":
|
||||
orientation === "vertical" && showSteps === "full",
|
||||
"top-0":
|
||||
orientation !== "vertical" && showSteps === "full",
|
||||
"-translate-x-1/2": orientation !== "vertical",
|
||||
"-translate-y-1/2": orientation === "vertical",
|
||||
},
|
||||
)}
|
||||
style={{
|
||||
[orientation === 'vertical' ? 'bottom' : 'left']: `${adjustedPosition}%`,
|
||||
[orientation === "vertical" ? "bottom" : "left"]:
|
||||
`${adjustedPosition}%`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
})}
|
||||
|
||||
</SliderPrimitive.Track>
|
||||
{localValues.map((numberStep, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
key={index}
|
||||
className={ny(
|
||||
'border-primary/50 bg-background focus-visible:ring-ring block size-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1',
|
||||
"block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
||||
)}
|
||||
>
|
||||
{hoveredThumbIndex && formatLabel && (
|
||||
<div
|
||||
className={ny(
|
||||
{ 'bottom-8 left-1/2 -translate-x-1/2': formatLabelSide === 'top' },
|
||||
{ 'top-8 left-1/2 -translate-x-1/2': formatLabelSide === 'bottom' },
|
||||
{ 'right-8 -translate-y-1/4': formatLabelSide === 'left' },
|
||||
{ 'left-8 -translate-y-1/4': formatLabelSide === 'right' },
|
||||
'bg-popover text-popover-foreground absolute z-30 w-max items-center justify-items-center rounded-md border px-2 py-1 text-center shadow-sm',
|
||||
{
|
||||
"bottom-8 left-1/2 -translate-x-1/2":
|
||||
formatLabelSide === "top",
|
||||
},
|
||||
{
|
||||
"left-1/2 top-8 -translate-x-1/2":
|
||||
formatLabelSide === "bottom",
|
||||
},
|
||||
{ "right-8 -translate-y-1/4": formatLabelSide === "left" },
|
||||
{ "left-8 -translate-y-1/4": formatLabelSide === "right" },
|
||||
"absolute z-30 w-max items-center justify-items-center rounded-md border bg-popover px-2 py-1 text-center text-popover-foreground shadow-sm",
|
||||
)}
|
||||
>
|
||||
{formatLabel(numberStep)}
|
||||
@@ -128,9 +180,10 @@ const Slider = React.forwardRef<
|
||||
</SliderPrimitive.Thumb>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
})
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider }
|
||||
export { Slider };
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { motion } from 'framer-motion'
|
||||
import type { CSSProperties, ReactElement } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ny } from '@/lib/utils'
|
||||
import { motion } from "framer-motion";
|
||||
import type { CSSProperties, ReactElement } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface Sparkle {
|
||||
id: string
|
||||
x: string
|
||||
y: string
|
||||
color: string
|
||||
delay: number
|
||||
scale: number
|
||||
lifespan: number
|
||||
id: string;
|
||||
x: string;
|
||||
y: string;
|
||||
color: string;
|
||||
delay: number;
|
||||
scale: number;
|
||||
lifespan: number;
|
||||
}
|
||||
|
||||
interface SparklesTextProps {
|
||||
@@ -22,7 +22,7 @@ interface SparklesTextProps {
|
||||
* @description
|
||||
* The component to be rendered as the text
|
||||
*/
|
||||
as?: ReactElement
|
||||
as?: ReactElement;
|
||||
|
||||
/**
|
||||
* @default ""
|
||||
@@ -30,7 +30,7 @@ interface SparklesTextProps {
|
||||
* @description
|
||||
* The className of the text
|
||||
*/
|
||||
className?: string
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* @required
|
||||
@@ -38,7 +38,7 @@ interface SparklesTextProps {
|
||||
* @description
|
||||
* The text to be displayed
|
||||
*/
|
||||
text: string
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* @default 10
|
||||
@@ -46,7 +46,7 @@ interface SparklesTextProps {
|
||||
* @description
|
||||
* The count of sparkles
|
||||
*/
|
||||
sparklesCount?: number
|
||||
sparklesCount?: number;
|
||||
|
||||
/**
|
||||
* @default "{first: '#A07CFE', second: '#FE8FB5'}"
|
||||
@@ -55,66 +55,65 @@ interface SparklesTextProps {
|
||||
* The colors of the sparkles
|
||||
*/
|
||||
colors?: {
|
||||
first: string
|
||||
second: string
|
||||
}
|
||||
first: string;
|
||||
second: string;
|
||||
};
|
||||
}
|
||||
|
||||
const SparklesText: React.FC<SparklesTextProps> = ({
|
||||
text,
|
||||
colors = { first: '#A07CFE', second: '#FE8FB5' },
|
||||
colors = { first: "#A07CFE", second: "#FE8FB5" },
|
||||
className,
|
||||
sparklesCount = 10,
|
||||
...props
|
||||
}) => {
|
||||
const [sparkles, setSparkles] = useState<Sparkle[]>([])
|
||||
const [sparkles, setSparkles] = useState<Sparkle[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const generateStar = (): Sparkle => {
|
||||
const starX = `${Math.random() * 100}%`
|
||||
const starY = `${Math.random() * 100}%`
|
||||
const color = Math.random() > 0.5 ? colors.first : colors.second
|
||||
const delay = Math.random() * 2
|
||||
const scale = Math.random() * 1 + 0.3
|
||||
const lifespan = Math.random() * 10 + 5
|
||||
const id = `${starX}-${starY}-${Date.now()}`
|
||||
return { id, x: starX, y: starY, color, delay, scale, lifespan }
|
||||
}
|
||||
const starX = `${Math.random() * 100}%`;
|
||||
const starY = `${Math.random() * 100}%`;
|
||||
const color = Math.random() > 0.5 ? colors.first : colors.second;
|
||||
const delay = Math.random() * 2;
|
||||
const scale = Math.random() * 1 + 0.3;
|
||||
const lifespan = Math.random() * 10 + 5;
|
||||
const id = `${starX}-${starY}-${Date.now()}`;
|
||||
return { id, x: starX, y: starY, color, delay, scale, lifespan };
|
||||
};
|
||||
|
||||
const initializeStars = () => {
|
||||
const newSparkles = Array.from({ length: sparklesCount }, generateStar)
|
||||
setSparkles(newSparkles)
|
||||
}
|
||||
const newSparkles = Array.from({ length: sparklesCount }, generateStar);
|
||||
setSparkles(newSparkles);
|
||||
};
|
||||
|
||||
const updateStars = () => {
|
||||
setSparkles(currentSparkles =>
|
||||
setSparkles((currentSparkles) =>
|
||||
currentSparkles.map((star) => {
|
||||
if (star.lifespan <= 0)
|
||||
return generateStar()
|
||||
else return { ...star, lifespan: star.lifespan - 0.1 }
|
||||
if (star.lifespan <= 0) return generateStar();
|
||||
else return { ...star, lifespan: star.lifespan - 0.1 };
|
||||
}),
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
initializeStars()
|
||||
const interval = setInterval(updateStars, 100)
|
||||
initializeStars();
|
||||
const interval = setInterval(updateStars, 100);
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [colors.first, colors.second])
|
||||
return () => clearInterval(interval);
|
||||
}, [colors.first, colors.second]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ny('text-6xl font-bold', className)}
|
||||
className={ny("text-6xl font-bold", className)}
|
||||
{...props}
|
||||
style={
|
||||
{
|
||||
'--sparkles-first-color': `${colors.first}`,
|
||||
'--sparkles-second-color': `${colors.second}`,
|
||||
"--sparkles-first-color": `${colors.first}`,
|
||||
"--sparkles-second-color": `${colors.second}`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<span className="relative inline-block">
|
||||
{sparkles.map(sparkle => (
|
||||
{sparkles.map((sparkle) => (
|
||||
<Sparkle key={sparkle.id} {...sparkle} />
|
||||
))}
|
||||
<strong className="bg-gradient-to-r from-[var(--sparkles-first-color)] to-[var(--sparkles-second-color)] bg-clip-text text-transparent">
|
||||
@@ -122,8 +121,8 @@ const SparklesText: React.FC<SparklesTextProps> = ({
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const Sparkle: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {
|
||||
return (
|
||||
@@ -146,7 +145,7 @@ const Sparkle: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {
|
||||
fill={color}
|
||||
/>
|
||||
</motion.svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default SparklesText
|
||||
export default SparklesText;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import * as React from "react";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
@@ -9,20 +9,20 @@ const Table = React.forwardRef<
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={ny('w-full caption-bottom text-sm', className)}
|
||||
className={ny("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = 'Table'
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={ny('[&_tr]:border-b', className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = 'TableHeader'
|
||||
<thead ref={ref} className={ny("[&_tr]:border-b", className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
@@ -30,11 +30,11 @@ const TableBody = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={ny('[&_tr:last-child]:border-0', className)}
|
||||
className={ny("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = 'TableBody'
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
@@ -43,13 +43,13 @@ const TableFooter = React.forwardRef<
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = 'TableFooter'
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
@@ -58,13 +58,13 @@ const TableRow = React.forwardRef<
|
||||
<tr
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = 'TableRow'
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
@@ -73,13 +73,13 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = 'TableHead'
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
@@ -88,13 +88,13 @@ const TableCell = React.forwardRef<
|
||||
<td
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = 'TableCell'
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
@@ -102,11 +102,11 @@ const TableCaption = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={ny('text-muted-foreground mt-4 text-sm', className)}
|
||||
className={ny("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = 'TableCaption'
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export {
|
||||
Table,
|
||||
@@ -117,4 +117,4 @@ export {
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import * as React from 'react'
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { ny } from '@/lib/utils'
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
@@ -14,13 +14,13 @@ const TabsList = React.forwardRef<
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
@@ -29,13 +29,13 @@ const TabsTrigger = React.forwardRef<
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
@@ -44,12 +44,12 @@ const TabsContent = React.forwardRef<
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={ny(
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { motion, useScroll, useTransform } from 'framer-motion'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import { ny } from '@/lib/utils'
|
||||
import { motion, useScroll, useTransform } from "framer-motion";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { useRef } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface TextRevealByWordProps {
|
||||
text: string
|
||||
className?: string
|
||||
text: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TextRevealByWord: FC<TextRevealByWordProps> = ({
|
||||
text,
|
||||
className,
|
||||
}) => {
|
||||
const targetRef = useRef<HTMLDivElement | null>(null)
|
||||
const targetRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: targetRef,
|
||||
})
|
||||
const words = text.split(' ')
|
||||
});
|
||||
const words = text.split(" ");
|
||||
|
||||
return (
|
||||
<div ref={targetRef} className={ny('relative z-0 h-[200vh]', className)}>
|
||||
<div ref={targetRef} className={ny("relative z-0 h-[200vh]", className)}>
|
||||
<div className="sticky top-0 mx-auto flex h-[50%] max-w-4xl items-center bg-transparent px-[1rem] py-[5rem]">
|
||||
<p
|
||||
ref={targetRef}
|
||||
className="flex flex-wrap p-5 text-2xl font-bold text-black/20 dark:text-white/20 md:p-8 md:text-3xl lg:p-10 lg:text-4xl xl:text-5xl"
|
||||
>
|
||||
{words.map((word, i) => {
|
||||
const start = i / words.length
|
||||
const end = start + 1 / words.length
|
||||
const start = i / words.length;
|
||||
const end = start + 1 / words.length;
|
||||
return (
|
||||
<Word key={i} progress={scrollYProgress} range={[start, end]}>
|
||||
{word}
|
||||
</Word>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
interface WordProps {
|
||||
children: ReactNode
|
||||
progress: any
|
||||
range: [number, number]
|
||||
children: ReactNode;
|
||||
progress: any;
|
||||
range: [number, number];
|
||||
}
|
||||
|
||||
const Word: FC<WordProps> = ({ children, progress, range }) => {
|
||||
const opacity = useTransform(progress, range, [0, 1])
|
||||
const opacity = useTransform(progress, range, [0, 1]);
|
||||
return (
|
||||
<span className="xl:lg-3 relative mx-1 lg:mx-2.5">
|
||||
<span className="absolute opacity-30">{children}</span>
|
||||
@@ -58,7 +58,7 @@ const Word: FC<WordProps> = ({ children, progress, range }) => {
|
||||
{children}
|
||||
</motion.span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default TextRevealByWord
|
||||
export default TextRevealByWord;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ny } from '@/lib/utils'
|
||||
import { useEffect, useState } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface TypingAnimationProps {
|
||||
text: string
|
||||
duration?: number
|
||||
className?: string
|
||||
text: string;
|
||||
duration?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function TypingAnimation({
|
||||
@@ -14,33 +14,32 @@ export default function TypingAnimation({
|
||||
duration = 200,
|
||||
className,
|
||||
}: TypingAnimationProps) {
|
||||
const [displayedText, setDisplayedText] = useState<string>('')
|
||||
const [i, setI] = useState<number>(0)
|
||||
const [displayedText, setDisplayedText] = useState<string>("");
|
||||
const [i, setI] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const typingEffect = setInterval(() => {
|
||||
if (i < text.length) {
|
||||
setDisplayedText(text.substring(0, i + 1))
|
||||
setI(i + 1)
|
||||
setDisplayedText(text.substring(0, i + 1));
|
||||
setI(i + 1);
|
||||
} else {
|
||||
clearInterval(typingEffect);
|
||||
}
|
||||
else {
|
||||
clearInterval(typingEffect)
|
||||
}
|
||||
}, duration)
|
||||
}, duration);
|
||||
|
||||
return () => {
|
||||
clearInterval(typingEffect)
|
||||
}
|
||||
}, [duration, i])
|
||||
clearInterval(typingEffect);
|
||||
};
|
||||
}, [duration, i]);
|
||||
|
||||
return (
|
||||
<h1
|
||||
className={ny(
|
||||
'font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm',
|
||||
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{displayedText || text}
|
||||
</h1>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { useMemo } from 'react'
|
||||
import { ny } from '@/lib/utils'
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useMemo } from "react";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface WavyTextProps {
|
||||
word: string
|
||||
className?: string
|
||||
word: string;
|
||||
className?: string;
|
||||
variant?: {
|
||||
hidden: { y: number }
|
||||
visible: { y: number }
|
||||
}
|
||||
duration?: number
|
||||
delay?: number
|
||||
hidden: { y: number };
|
||||
visible: { y: number };
|
||||
};
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
}
|
||||
function WavyText({
|
||||
word,
|
||||
@@ -24,9 +24,9 @@ function WavyText({
|
||||
const defaultVariants = {
|
||||
hidden: { y: 10 },
|
||||
visible: { y: -10 },
|
||||
}
|
||||
const combinedVariants = variant || defaultVariants
|
||||
const characters = useMemo(() => word.split(''), [word])
|
||||
};
|
||||
const combinedVariants = variant || defaultVariants;
|
||||
const characters = useMemo(() => word.split(""), [word]);
|
||||
return (
|
||||
<div className="flex justify-center space-x-2 overflow-hidden p-3">
|
||||
<AnimatePresence>
|
||||
@@ -44,7 +44,7 @@ function WavyText({
|
||||
}}
|
||||
className={ny(
|
||||
className,
|
||||
'font-display text-center text-4xl font-bold tracking-[-0.15em] md:text-7xl',
|
||||
"font-display text-center text-4xl font-bold tracking-[-0.15em] md:text-7xl",
|
||||
)}
|
||||
>
|
||||
{char}
|
||||
@@ -52,7 +52,7 @@ function WavyText({
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default WavyText
|
||||
export default WavyText;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import type { Variants } from 'framer-motion'
|
||||
import { motion } from 'framer-motion'
|
||||
import { ny } from '@/lib/utils'
|
||||
import type { Variants } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import { ny } from "@/lib/utils";
|
||||
|
||||
interface WordPullUpProps {
|
||||
words: string
|
||||
delayMultiple?: number
|
||||
wrapperFramerProps?: Variants
|
||||
framerProps?: Variants
|
||||
className?: string
|
||||
words: string;
|
||||
delayMultiple?: number;
|
||||
wrapperFramerProps?: Variants;
|
||||
framerProps?: Variants;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function WordPullUp({
|
||||
@@ -35,19 +35,19 @@ export default function WordPullUp({
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className={ny(
|
||||
'font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm',
|
||||
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{words.split(' ').map((word, i) => (
|
||||
{words.split(" ").map((word, i) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
variants={framerProps}
|
||||
style={{ display: 'inline-block', paddingRight: '8px' }}
|
||||
style={{ display: "inline-block", paddingRight: "8px" }}
|
||||
>
|
||||
{word === '' ? <span> </span> : word}
|
||||
{word === "" ? <span> </span> : word}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.h1>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,17 @@ import WordPullUp from "./ui/word-pull-up";
|
||||
|
||||
export default function WelcomePage() {
|
||||
return (
|
||||
<div className="w-full relative min-h-screen flex flex-col items-center justify-center">
|
||||
<WordPullUp className="text-6xl text-center" words="Welcome to Zen Browser!" />
|
||||
<p className="max-w-90 text-lg mt-12">A Firefox based browser with a focus on privacy and customization.<br/>Start using it by clicking on the sidebar icon or trying out the split view feature!</p>
|
||||
<div className="relative flex min-h-screen w-full flex-col items-center justify-center">
|
||||
<WordPullUp
|
||||
className="text-center text-6xl"
|
||||
words="Welcome to Zen Browser!"
|
||||
/>
|
||||
<p className="max-w-90 mt-12 text-lg">
|
||||
A Firefox based browser with a focus on privacy and customization.
|
||||
<br />
|
||||
Start using it by clicking on the sidebar icon or trying out the split
|
||||
view feature!
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
|
||||
export const LOGO_COLORS = [
|
||||
"black", "blue", "brown", "buff", "indigo", "mantis", "orchid", "pink", "tangerine", "turqoise", "white", "yellow"
|
||||
]
|
||||
"black",
|
||||
"blue",
|
||||
"brown",
|
||||
"buff",
|
||||
"indigo",
|
||||
"mantis",
|
||||
"orchid",
|
||||
"pink",
|
||||
"tangerine",
|
||||
"turqoise",
|
||||
"white",
|
||||
"yellow",
|
||||
];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
interface Fix {
|
||||
description: string;
|
||||
issue?: number;
|
||||
@@ -17,7 +16,8 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
{
|
||||
version: "1.0.0-a.1",
|
||||
date: "11/07/2024",
|
||||
extra: "This release will be the first release considered as stable. It's still in alpha, but it's the first release that we consider to be stable enough for daily use. You can start using it as your main browser right now if you are reading this!",
|
||||
extra:
|
||||
"This release will be the first release considered as stable. It's still in alpha, but it's the first release that we consider to be stable enough for daily use. You can start using it as your main browser right now if you are reading this!",
|
||||
features: [
|
||||
"Stable support for split views.",
|
||||
"Updated firefox to version 128.0",
|
||||
@@ -29,7 +29,8 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
],
|
||||
fixes: [
|
||||
{
|
||||
description: "Fixed a bug where the browser would crash when opening any extension.",
|
||||
description:
|
||||
"Fixed a bug where the browser would crash when opening any extension.",
|
||||
issue: 34,
|
||||
},
|
||||
{
|
||||
@@ -39,20 +40,22 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
{
|
||||
description: "Applied a fix for that affected some linux users.",
|
||||
issue: 36,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.2",
|
||||
date: "12/07/2024",
|
||||
extra: "This release is the second alpha release of the 1.0.0-alpha series. It includes a lot of bug fixes and improvements given the feedback we received from the first alpha release. This release is still not considered stable, but it's a big step towards the first stable release.",
|
||||
extra:
|
||||
"This release is the second alpha release of the 1.0.0-alpha series. It includes a lot of bug fixes and improvements given the feedback we received from the first alpha release. This release is still not considered stable, but it's a big step towards the first stable release.",
|
||||
features: [
|
||||
"Added support for macOS aaarch64!",
|
||||
"Some performance improvements.",
|
||||
],
|
||||
fixes: [
|
||||
{
|
||||
description: "Fixed rounded corners of browser views for some websites.",
|
||||
description:
|
||||
"Fixed rounded corners of browser views for some websites.",
|
||||
issue: 48,
|
||||
},
|
||||
{
|
||||
@@ -62,16 +65,17 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
{
|
||||
description: "Changed to the correct branding for Zen Home.",
|
||||
issue: 50,
|
||||
}
|
||||
},
|
||||
],
|
||||
breakingChanges: [
|
||||
"Removed support window's stub installer, it's under development.",
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.3",
|
||||
date: "14/07/2024",
|
||||
extra: "This release is the third alpha release of the 1.0.0-alpha series. One big feature of this release is the new workspaces feature. This feature allows you to create different workspaces with different tabs and configurations. This release also includes a lot of bug fixes and improvements.",
|
||||
extra:
|
||||
"This release is the third alpha release of the 1.0.0-alpha series. One big feature of this release is the new workspaces feature. This feature allows you to create different workspaces with different tabs and configurations. This release also includes a lot of bug fixes and improvements.",
|
||||
features: [
|
||||
"Added support for workspaces. (Experimental)",
|
||||
"Better support for macOS aarch64.",
|
||||
@@ -92,12 +96,13 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
],
|
||||
breakingChanges: [
|
||||
"Changed the update URL meaning that since 1.0.0-a.2 (previous release) the browser will be able to update itself.",
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.4",
|
||||
date: "14/07/2024",
|
||||
extra: "This release is the fourth alpha release of the 1.0.0-alpha series. This release includes a lot of bug fixes and improvements. The main focus of this release was to improve some small details and the compact view mode.\n\nThis release is very small but it includes some important fixes and I wont be able to work on the browser for the next few days so I decided to release it now.\n\nSorry!",
|
||||
extra:
|
||||
"This release is the fourth alpha release of the 1.0.0-alpha series. This release includes a lot of bug fixes and improvements. The main focus of this release was to improve some small details and the compact view mode.\n\nThis release is very small but it includes some important fixes and I wont be able to work on the browser for the next few days so I decided to release it now.\n\nSorry!",
|
||||
features: [
|
||||
"Improved compact view mode.",
|
||||
"Tabs with no icons will now display a default icon.",
|
||||
@@ -106,23 +111,25 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
],
|
||||
fixes: [
|
||||
{
|
||||
description: "Fixed the compact view mode not displaying correctly when it's not fullscreen.",
|
||||
description:
|
||||
"Fixed the compact view mode not displaying correctly when it's not fullscreen.",
|
||||
issue: 58,
|
||||
},
|
||||
{
|
||||
description: "Fixed \"Weird margins in popup windows\".",
|
||||
description: 'Fixed "Weird margins in popup windows".',
|
||||
issue: 54,
|
||||
},
|
||||
{
|
||||
description: "Fixed pinned tab icons not being displayed correctly.",
|
||||
issue: 52,
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.5",
|
||||
date: "16/07/2024",
|
||||
extra: "This release is a very small release that includes some speed improvements and privacy improvements. This release is the fifth alpha release of the 1.0.0-alpha series.",
|
||||
extra:
|
||||
"This release is a very small release that includes some speed improvements and privacy improvements. This release is the fifth alpha release of the 1.0.0-alpha series.",
|
||||
features: [
|
||||
"Improved performance of the browser, specially for windows users.",
|
||||
"Changed some of firefox's default privacy settings to improve privacy.",
|
||||
@@ -135,7 +142,8 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
issue: 62,
|
||||
},
|
||||
{
|
||||
description: "Fixed Zen not being able to execute on some linux distributions.",
|
||||
description:
|
||||
"Fixed Zen not being able to execute on some linux distributions.",
|
||||
issue: 36,
|
||||
},
|
||||
{
|
||||
@@ -145,13 +153,14 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
{
|
||||
description: "Fixed some background overlapping the browser view.",
|
||||
issue: 48,
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.6",
|
||||
date: "24/07/2024",
|
||||
extra: "Hello there! Sorry for not updating so frequently, i've been busy this week.\n\nWelcome to the sixth alpha release of the 1.0.0-alpha series, gettin' closer to the first stable release!\n\nThis release includes a lot of bug fixes and improvements. The main focus of this release was to improve some small details and performance.",
|
||||
extra:
|
||||
"Hello there! Sorry for not updating so frequently, i've been busy this week.\n\nWelcome to the sixth alpha release of the 1.0.0-alpha series, gettin' closer to the first stable release!\n\nThis release includes a lot of bug fixes and improvements. The main focus of this release was to improve some small details and performance.",
|
||||
features: [
|
||||
"Improved performance of the browser.",
|
||||
"Added support for the latest version of Firefox (128.0.2).",
|
||||
@@ -176,47 +185,51 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
description: "Fixed pinned tabs not being displayed correctly.",
|
||||
},
|
||||
{
|
||||
description: "Fixed pressing \"Bookmarks\" twice in the bottom left doesn't close the bookmarks tab",
|
||||
issue: 74
|
||||
description:
|
||||
'Fixed pressing "Bookmarks" twice in the bottom left doesn\'t close the bookmarks tab',
|
||||
issue: 74,
|
||||
},
|
||||
{
|
||||
description: "Fixed wrong colors for web-content popups.",
|
||||
issue: 70
|
||||
issue: 70,
|
||||
},
|
||||
{
|
||||
description: "Fixed padding when DOM fullscreen is enabled.",
|
||||
issue: 67
|
||||
issue: 67,
|
||||
},
|
||||
{
|
||||
description: "Tab Bar Icons Hidden When Many Tabs are Open",
|
||||
issue: 64
|
||||
issue: 64,
|
||||
},
|
||||
{
|
||||
description: "Disabled Zen Workspaces when private browsing is enabled."
|
||||
description:
|
||||
"Disabled Zen Workspaces when private browsing is enabled.",
|
||||
},
|
||||
{
|
||||
description: "Fixed web view padding when opening a hidden popup.",
|
||||
issue: 54
|
||||
issue: 54,
|
||||
},
|
||||
{
|
||||
description: "The Windows NSIS installer correctly installs the browser in the right path now instead of \"Mozilla Developer Preview\".",
|
||||
description:
|
||||
'The Windows NSIS installer correctly installs the browser in the right path now instead of "Mozilla Developer Preview".',
|
||||
},
|
||||
{
|
||||
description: "Fixed overall windows installer branding.",
|
||||
},
|
||||
{
|
||||
description: "Fixed update URLs and support links.",
|
||||
}
|
||||
},
|
||||
],
|
||||
breakingChanges: [
|
||||
"Updated CPU requirements for x86_64-v3",
|
||||
"Changed the way profile avatars are stored, may not be any issues, please report them if you find any."
|
||||
"Changed the way profile avatars are stored, may not be any issues, please report them if you find any.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.7",
|
||||
date: "27/07/2024",
|
||||
extra: "This release is the seventh alpha release of the 1.0.0-alpha series. This release includes some important bug fixes and improvements. The main focus of this release was to improve some small details and improve stability.",
|
||||
extra:
|
||||
"This release is the seventh alpha release of the 1.0.0-alpha series. This release includes some important bug fixes and improvements. The main focus of this release was to improve some small details and improve stability.",
|
||||
features: [
|
||||
"A grid system for pinned tabs.",
|
||||
"Flatpak support.",
|
||||
@@ -231,25 +244,27 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
fixes: [
|
||||
{
|
||||
description: "Fixed the browser crashing when updating on linux.",
|
||||
issue: 76
|
||||
issue: 76,
|
||||
},
|
||||
{
|
||||
description: "Fixed workspace deleting button, deleting the wrong workspace.",
|
||||
issue: 81
|
||||
description:
|
||||
"Fixed workspace deleting button, deleting the wrong workspace.",
|
||||
issue: 81,
|
||||
},
|
||||
{
|
||||
description: "Fixed the expanded sidebar for the compact view.",
|
||||
issue: 79
|
||||
issue: 79,
|
||||
},
|
||||
{
|
||||
description: "Fixed small margin on hidden windows.",
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.8",
|
||||
date: "29/07/2024",
|
||||
extra: "This release is the eighth alpha release of the 1.0.0-alpha series.\n\nThis release includes some small bug fixes and improvements. The main focus of this release was to improve some small details and improve stability.",
|
||||
extra:
|
||||
"This release is the eighth alpha release of the 1.0.0-alpha series.\n\nThis release includes some small bug fixes and improvements. The main focus of this release was to improve some small details and improve stability.",
|
||||
features: [
|
||||
"Added support for the latest version of Firefox (128.0.3).",
|
||||
"Improved the compact view mode.",
|
||||
@@ -261,11 +276,13 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
description: "Lowered the CPU requirements for Linux users.",
|
||||
},
|
||||
{
|
||||
description: "Fixed browser updater crashing on some linux distributions.",
|
||||
issue: 76
|
||||
description:
|
||||
"Fixed browser updater crashing on some linux distributions.",
|
||||
issue: 76,
|
||||
},
|
||||
{
|
||||
description: "Added more contrast to web context menus on light themes.",
|
||||
description:
|
||||
"Added more contrast to web context menus on light themes.",
|
||||
issue: 88,
|
||||
},
|
||||
{
|
||||
@@ -273,18 +290,20 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
issue: 100,
|
||||
},
|
||||
{
|
||||
description: "Created a small hotfix for themes and some broken extensions.",
|
||||
issue: 89
|
||||
}
|
||||
description:
|
||||
"Created a small hotfix for themes and some broken extensions.",
|
||||
issue: 89,
|
||||
},
|
||||
],
|
||||
breakingChanges: [
|
||||
"Changed the ID for flatpak to io.github.zen_browser.zen",
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.11",
|
||||
date: "02/08/2024",
|
||||
extra: "This release is the eleventh alpha release of the 1.0.0-alpha series.\n\nWe skipped some releases because we were working on some big features and improvements. This release includes some big performance improvements and support for other platforms.\n\nHopefuly things will get a bit more stable from now on.",
|
||||
extra:
|
||||
"This release is the eleventh alpha release of the 1.0.0-alpha series.\n\nWe skipped some releases because we were working on some big features and improvements. This release includes some big performance improvements and support for other platforms.\n\nHopefuly things will get a bit more stable from now on.",
|
||||
features: [
|
||||
"Added support for Windows Generic CPUs.",
|
||||
"Added support for Linux Generic CPUs.",
|
||||
@@ -306,33 +325,36 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
issue: 84,
|
||||
},
|
||||
{
|
||||
description: "Fixed 'Couldn't load XPCOM' error on some windows installations.",
|
||||
issue: 107
|
||||
description:
|
||||
"Fixed 'Couldn't load XPCOM' error on some windows installations.",
|
||||
issue: 107,
|
||||
},
|
||||
{
|
||||
description: "Fixed the browser crashing when updating on Linux.",
|
||||
},
|
||||
{
|
||||
description: "Fixed pinned tabs not being properly aligned.",
|
||||
issue: 110
|
||||
issue: 110,
|
||||
},
|
||||
{
|
||||
description: "Remove padding around close button on Windows.",
|
||||
issue: 134
|
||||
issue: 134,
|
||||
},
|
||||
{
|
||||
description: "Fixed a part of the tab list and the corner border is showing/clipping in Fullscreen",
|
||||
issue: 124
|
||||
description:
|
||||
"Fixed a part of the tab list and the corner border is showing/clipping in Fullscreen",
|
||||
issue: 124,
|
||||
},
|
||||
],
|
||||
breakingChanges: [
|
||||
"Changed the ID for AppImage to io.github.zen_browser.zen",
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.12",
|
||||
date: "04/08/2024",
|
||||
extra: "This release is the twelfth alpha release of the 1.0.0-alpha series.\n\nThis release includes some nice features and improvements. The main focus of this release was to improve some small details and improve stability.",
|
||||
extra:
|
||||
"This release is the twelfth alpha release of the 1.0.0-alpha series.\n\nThis release includes some nice features and improvements. The main focus of this release was to improve some small details and improve stability.",
|
||||
features: [
|
||||
"Added support for the vertical tabs to remember their width.",
|
||||
"Added support for disallowing the sidebar web panels to closed when clicked outside and floating (Settings).",
|
||||
@@ -350,7 +372,8 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
issue: 146,
|
||||
},
|
||||
{
|
||||
description: "Clicking \"Next\" on \"Choose your search engine\" intro page does nothing",
|
||||
description:
|
||||
'Clicking "Next" on "Choose your search engine" intro page does nothing',
|
||||
issue: 145,
|
||||
},
|
||||
{
|
||||
@@ -361,7 +384,7 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
description: "Fixed pinned tabs not being properly aligned.",
|
||||
},
|
||||
{
|
||||
description: "\"Delete Web Panel\" closes the sidebar",
|
||||
description: '"Delete Web Panel" closes the sidebar',
|
||||
issue: 143,
|
||||
},
|
||||
{
|
||||
@@ -377,10 +400,9 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
{
|
||||
version: "1.0.0-a.13",
|
||||
date: "05/08/2024",
|
||||
extra: "This is a smaller release to fix some bugs and improve some small details.\n\nIm going to try doing more frequent releases from now on, see how it goes.",
|
||||
features: [
|
||||
"Allow to remember sidebar width even after collapsing it.",
|
||||
],
|
||||
extra:
|
||||
"This is a smaller release to fix some bugs and improve some small details.\n\nIm going to try doing more frequent releases from now on, see how it goes.",
|
||||
features: ["Allow to remember sidebar width even after collapsing it."],
|
||||
fixes: [
|
||||
{
|
||||
description: "Task Manager Icon Missing in Flatpak Version",
|
||||
@@ -405,7 +427,8 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
{
|
||||
version: "1.0.0-a.15",
|
||||
date: "07/08/2024",
|
||||
extra: "This release is the fifteenth alpha release of the 1.0.0-alpha series.\n\nI've skipped version 1.0.0-a.14 because of the quality of the release, it was not good enough to be released.\n\nThis release includes some bug fixes and improvements.\n\nThanks everyone for the feedback! It may look like a small release but it includes some important fixes and improvements.",
|
||||
extra:
|
||||
"This release is the fifteenth alpha release of the 1.0.0-alpha series.\n\nI've skipped version 1.0.0-a.14 because of the quality of the release, it was not good enough to be released.\n\nThis release includes some bug fixes and improvements.\n\nThanks everyone for the feedback! It may look like a small release but it includes some important fixes and improvements.",
|
||||
features: [
|
||||
"Added support for the latest version of Firefox (129.0).",
|
||||
"Reworked on split views.",
|
||||
@@ -426,7 +449,8 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
{
|
||||
version: "1.0.0-a.17",
|
||||
date: "11/08/2024",
|
||||
extra: "This release is the seventeenth alpha release of the 1.0.0-alpha series.\n\nThis release includes some bug fixes and improvements.\n\nThanks everyone for the feedback! We've skipped version 1.0.0-a.16 because of the size of the releases. One more step closer to the first stable release!",
|
||||
extra:
|
||||
"This release is the seventeenth alpha release of the 1.0.0-alpha series.\n\nThis release includes some bug fixes and improvements.\n\nThanks everyone for the feedback! We've skipped version 1.0.0-a.16 because of the size of the releases. One more step closer to the first stable release!",
|
||||
features: [
|
||||
"Added support for default keyboard shortcuts.",
|
||||
"Added JavaScript Bytecode Cache.",
|
||||
@@ -466,7 +490,8 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
{
|
||||
version: "1.0.0-a.23",
|
||||
date: "18/08/2024",
|
||||
extra: "This release is the twenty-third alpha release of the 1.0.0-alpha series.\n\nWe have made a lot of improvements and bug fixes since the last release note. I will go over the most important changes since version 1.0.0-a.17.\n\nThanks to everyone for the feedback and support!!",
|
||||
extra:
|
||||
"This release is the twenty-third alpha release of the 1.0.0-alpha series.\n\nWe have made a lot of improvements and bug fixes since the last release note. I will go over the most important changes since version 1.0.0-a.17.\n\nThanks to everyone for the feedback and support!!",
|
||||
features: [
|
||||
"Added support for the latest stable version of Firefox (129.0.1)",
|
||||
"Added security warning when changing the language",
|
||||
@@ -490,7 +515,8 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
issue: 225,
|
||||
},
|
||||
{
|
||||
description: "Closed tabs reappear if restore previous tabs on startup is enabled ",
|
||||
description:
|
||||
"Closed tabs reappear if restore previous tabs on startup is enabled ",
|
||||
issue: 230,
|
||||
},
|
||||
{
|
||||
@@ -503,187 +529,197 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
},
|
||||
{
|
||||
description: "Floating URL Bar does not close",
|
||||
issue: 252
|
||||
issue: 252,
|
||||
},
|
||||
{
|
||||
description: "Web panel can go out of bounds if it is not pinned",
|
||||
issue: 267
|
||||
issue: 267,
|
||||
},
|
||||
{
|
||||
description: "Bookmarks bar does not match the selected theme",
|
||||
issue: 264
|
||||
issue: 264,
|
||||
},
|
||||
{
|
||||
description: "Tab backgrounds overlap on hover",
|
||||
issue: 303
|
||||
}
|
||||
issue: 303,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.24",
|
||||
date: "20/08/2024",
|
||||
extra: "This release is the twenty-fourth alpha release of the 1.0.0-alpha series.\n\nThis release brings the long-awaited expand on hover feature, as well as some bug fixes and improvements to the theme store and documentation.",
|
||||
extra:
|
||||
"This release is the twenty-fourth alpha release of the 1.0.0-alpha series.\n\nThis release brings the long-awaited expand on hover feature, as well as some bug fixes and improvements to the theme store and documentation.",
|
||||
features: [
|
||||
"Added a frequently asked questions page to the documentation",
|
||||
"Added platform specific preferences to Zen themes",
|
||||
"Added expand-on-hover feature for the tab sidebar",
|
||||
"Improved scrollbar appearance on Windows",
|
||||
"Improved URL bar background color"
|
||||
"Improved URL bar background color",
|
||||
],
|
||||
fixes: [
|
||||
{
|
||||
description: "Mute button is shown on inactive collapsed tabs",
|
||||
issue: 322
|
||||
issue: 322,
|
||||
},
|
||||
{
|
||||
description: "Visual bug on bottom sidebar buttons",
|
||||
issue: 304
|
||||
issue: 304,
|
||||
},
|
||||
{
|
||||
description: "Closing tabs makes other tabs briefly smaller",
|
||||
issue: 337
|
||||
issue: 337,
|
||||
},
|
||||
{
|
||||
description: "Checkboxes are hard to see",
|
||||
issue: 103
|
||||
}
|
||||
]
|
||||
issue: 103,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.26",
|
||||
date: "20/08/2024",
|
||||
extra: "This release is the twenty-sixth alpha release of the 1.0.0-alpha series.\n\nThis is a short release that addresses some important bugs.",
|
||||
extra:
|
||||
"This release is the twenty-sixth alpha release of the 1.0.0-alpha series.\n\nThis is a short release that addresses some important bugs.",
|
||||
features: [
|
||||
"Updated to the latest stable version of Firefox (129.0.2)",
|
||||
"Updated CSS to improve compact mode and vertical tabs styling",
|
||||
"Updated the browser logo for Windows to a higher resolution",
|
||||
"Fixed severe issue with platform specific preference handling",
|
||||
"Added feature to set a default workspace"
|
||||
"Added feature to set a default workspace",
|
||||
],
|
||||
fixes: [
|
||||
{
|
||||
description: "Issue with edge detection for expand-on-hover sidebar feature",
|
||||
issue: 355
|
||||
}
|
||||
]
|
||||
description:
|
||||
"Issue with edge detection for expand-on-hover sidebar feature",
|
||||
issue: 355,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.28",
|
||||
date: "22/08/2024",
|
||||
extra: "This release is the twenty-eighth alpha release of the 1.0.0-alpha series.",
|
||||
extra:
|
||||
"This release is the twenty-eighth alpha release of the 1.0.0-alpha series.",
|
||||
features: [
|
||||
"Enabled JPEG XL",
|
||||
"Changed Zen Core Components license to CC BY-SA",
|
||||
"Added support for color themes in theme creation",
|
||||
"Fixed sidebar shortcuts",
|
||||
"Started work on browser translations"
|
||||
"Started work on browser translations",
|
||||
],
|
||||
fixes: [
|
||||
{
|
||||
description: "No way to restore native theming to toolbar",
|
||||
issue: 426
|
||||
issue: 426,
|
||||
},
|
||||
{
|
||||
description: "Address bar icons are hidden",
|
||||
issue: 430
|
||||
issue: 430,
|
||||
},
|
||||
{
|
||||
description: "Tabs don't appear in vertical tab bar",
|
||||
issue: 429
|
||||
issue: 429,
|
||||
},
|
||||
{
|
||||
description: "Location bar is not focused when opening new window",
|
||||
issue: 414
|
||||
issue: 414,
|
||||
},
|
||||
{
|
||||
description: "The treshhold for the expanded tab sidebar in compact mode is too high",
|
||||
issue: 389
|
||||
}
|
||||
]
|
||||
description:
|
||||
"The treshhold for the expanded tab sidebar in compact mode is too high",
|
||||
issue: 389,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.29",
|
||||
date: "24/08/2024",
|
||||
extra: "This release is the twenty-ninth alpha release of the 1.0.0-alpha series.",
|
||||
extra:
|
||||
"This release is the twenty-ninth alpha release of the 1.0.0-alpha series.",
|
||||
features: [
|
||||
"Added Spanish translations",
|
||||
"Added documentation for contributing",
|
||||
"Added support for multi-tab splitting with shortcuts",
|
||||
"Fixed sidebar shortcuts"
|
||||
"Fixed sidebar shortcuts",
|
||||
],
|
||||
fixes: [
|
||||
{
|
||||
description: "Text on websites is blurry",
|
||||
issue: 383
|
||||
issue: 383,
|
||||
},
|
||||
{
|
||||
description: "Expanded compact mode triggers too early",
|
||||
issue: 520
|
||||
issue: 520,
|
||||
},
|
||||
{
|
||||
description: "Ampersand in workspace name breaks workspace menu",
|
||||
issue: 439
|
||||
}
|
||||
]
|
||||
issue: 439,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.30",
|
||||
date: "26/08/2024",
|
||||
extra: "This release is the thirtieth alpha release of the 1.0.0-alpha series.",
|
||||
extra:
|
||||
"This release is the thirtieth alpha release of the 1.0.0-alpha series.",
|
||||
features: [
|
||||
"Added support for 24 more languages!",
|
||||
"Update installed themes from the browser settings"
|
||||
"Update installed themes from the browser settings",
|
||||
],
|
||||
fixes: [
|
||||
{
|
||||
description: "Letterboxing option is missing",
|
||||
issue: 475
|
||||
issue: 475,
|
||||
},
|
||||
{
|
||||
description: "Collapsed tabs move when audio is playing",
|
||||
issue: 608
|
||||
issue: 608,
|
||||
},
|
||||
{
|
||||
description: "Screensaver starts while a video is running in fullscreen",
|
||||
issue: 619
|
||||
description:
|
||||
"Screensaver starts while a video is running in fullscreen",
|
||||
issue: 619,
|
||||
},
|
||||
{
|
||||
description: "Can't scroll through list of workspaces",
|
||||
issue: 603
|
||||
issue: 603,
|
||||
},
|
||||
{
|
||||
description: "Can't rename created workspace",
|
||||
issue: 604
|
||||
issue: 604,
|
||||
},
|
||||
{
|
||||
description: "JavaScript won't execute in the browser console",
|
||||
issue: 913
|
||||
}
|
||||
]
|
||||
issue: 913,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.33",
|
||||
date: "30/08/2024",
|
||||
extra: "This release is the thirty-third alpha release of the 1.0.0-alpha series.",
|
||||
extra:
|
||||
"This release is the thirty-third alpha release of the 1.0.0-alpha series.",
|
||||
features: [
|
||||
"Fixed policies for updates",
|
||||
"Enforce HTTPS-Only Mode",
|
||||
"URL bar improvements",
|
||||
"Fixed issue with opening links from external apps",
|
||||
"Compact mode now takes element separation into account",
|
||||
"Added labels to buttons during expand-on-hover"
|
||||
"Added labels to buttons during expand-on-hover",
|
||||
],
|
||||
fixes: [
|
||||
{
|
||||
description: "Tab bar stuck on right side",
|
||||
issue: 1115
|
||||
}
|
||||
]
|
||||
issue: 1115,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.35",
|
||||
date: "02/09/2024",
|
||||
extra: "This release is the thirty-fifth alpha release of the 1.0.0-alpha series. Things are getting stable!",
|
||||
extra:
|
||||
"This release is the thirty-fifth alpha release of the 1.0.0-alpha series. Things are getting stable!",
|
||||
features: [
|
||||
"Added option to restore legacy toolbar interface",
|
||||
"Added profile-guided optimization (Windows)",
|
||||
@@ -698,30 +734,32 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
fixes: [
|
||||
{
|
||||
description: "Zen Browser is damaged and can't be opened on macOS",
|
||||
issue: 53
|
||||
issue: 53,
|
||||
},
|
||||
{
|
||||
description: "Can't reorganize tabs in compact mode",
|
||||
issue: 1168
|
||||
issue: 1168,
|
||||
},
|
||||
{
|
||||
description: "Theme Store settings page doesn't display installed themes",
|
||||
issue: 1125
|
||||
description:
|
||||
"Theme Store settings page doesn't display installed themes",
|
||||
issue: 1125,
|
||||
},
|
||||
{
|
||||
description: "No Homebrew support",
|
||||
issue: 273
|
||||
issue: 273,
|
||||
},
|
||||
{
|
||||
description: "Remember last active workspaces on startup",
|
||||
issue: 240
|
||||
}
|
||||
]
|
||||
issue: 240,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0-a.39",
|
||||
date: "09/09/2024",
|
||||
extra: "This release is the thirty-eighth alpha release of the 1.0.0-alpha series.",
|
||||
extra:
|
||||
"This release is the thirty-eighth alpha release of the 1.0.0-alpha series.",
|
||||
features: [
|
||||
"Successfully added Apple developer certificate (macOS)",
|
||||
"Fixed issue with ffmpeg VA-API decoding",
|
||||
@@ -736,26 +774,27 @@ export const releaseNotes: ReleaseNote[] = [
|
||||
fixes: [
|
||||
{
|
||||
description: "Scrolling between tabs is buggy/slow",
|
||||
issue: 1340
|
||||
issue: 1340,
|
||||
},
|
||||
{
|
||||
description: "Broken hover effect with NewTab button with compact density interface",
|
||||
issue: 1224
|
||||
description:
|
||||
"Broken hover effect with NewTab button with compact density interface",
|
||||
issue: 1224,
|
||||
},
|
||||
{
|
||||
description: "Issue on Google Meet regarding WebRTC",
|
||||
issue: 972
|
||||
issue: 972,
|
||||
},
|
||||
{
|
||||
description: "Zen Browser.app is damaged and can’t be opened on macOS",
|
||||
issue: 1245
|
||||
issue: 1245,
|
||||
},
|
||||
{
|
||||
description: "Fixed issue with ⌘ keyboard shortcuts on macOS",
|
||||
issue: 376
|
||||
}
|
||||
]
|
||||
}
|
||||
issue: 376,
|
||||
},
|
||||
],
|
||||
},
|
||||
].reverse();
|
||||
|
||||
export function releaseNoteIsAlpha(note: ReleaseNote) {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
|
||||
export interface ZenTheme {
|
||||
name: string
|
||||
description: string
|
||||
image: string
|
||||
downloadUrl: string
|
||||
id: string
|
||||
homepage?: string
|
||||
readme: string
|
||||
preferences?: string
|
||||
isColorTheme: boolean
|
||||
author: string
|
||||
version: string
|
||||
name: string;
|
||||
description: string;
|
||||
image: string;
|
||||
downloadUrl: string;
|
||||
id: string;
|
||||
homepage?: string;
|
||||
readme: string;
|
||||
preferences?: string;
|
||||
isColorTheme: boolean;
|
||||
author: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
const THEME_API = "https://zen-browser.github.io/theme-store/themes.json";
|
||||
const CACHE_OPTIONS = { next: {
|
||||
const CACHE_OPTIONS = {
|
||||
next: {
|
||||
revalidate: 60,
|
||||
} } as RequestInit;
|
||||
},
|
||||
} as RequestInit;
|
||||
|
||||
export async function getAllThemes() {
|
||||
// Fetch from the API
|
||||
@@ -30,11 +31,18 @@ export async function getAllThemes() {
|
||||
return themesArray;
|
||||
}
|
||||
|
||||
export function getThemesFromSearch(themes: ZenTheme[], query: string, tags: string[]): ZenTheme[] {
|
||||
let filtered = themes.filter((theme) => theme.name.toLowerCase().includes(query.toLowerCase()));
|
||||
export function getThemesFromSearch(
|
||||
themes: ZenTheme[],
|
||||
query: string,
|
||||
tags: string[],
|
||||
): ZenTheme[] {
|
||||
let filtered = themes.filter((theme) =>
|
||||
theme.name.toLowerCase().includes(query.toLowerCase()),
|
||||
);
|
||||
if (tags.includes("all")) return filtered;
|
||||
const isSearchingForColorScheme = tags.includes("color-scheme");
|
||||
const isSearchingForUtility = !isSearchingForColorScheme && tags.includes("utility");
|
||||
const isSearchingForUtility =
|
||||
!isSearchingForColorScheme && tags.includes("utility");
|
||||
return filtered.filter((theme) => {
|
||||
if (isSearchingForColorScheme && theme.isColorTheme) return true;
|
||||
if (isSearchingForUtility && !theme.isColorTheme) return true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function ny(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
@@ -1,75 +1,77 @@
|
||||
import type { Config } from "tailwindcss"
|
||||
import { fontFamily } from 'tailwindcss/defaultTheme'
|
||||
import type { Config } from "tailwindcss";
|
||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
'2xl': '1400px',
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['var(--font-sans)', ...fontFamily.sans],
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
surface: "var(--surface)",
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
orbit: {
|
||||
"0%": {
|
||||
transform: "rotate(0deg) translateY(calc(var(--radius) * 1px)) rotate(0deg)",
|
||||
transform:
|
||||
"rotate(0deg) translateY(calc(var(--radius) * 1px)) rotate(0deg)",
|
||||
},
|
||||
"100%": {
|
||||
transform: "rotate(360deg) translateY(calc(var(--radius) * 1px)) rotate(-360deg)",
|
||||
transform:
|
||||
"rotate(360deg) translateY(calc(var(--radius) * 1px)) rotate(-360deg)",
|
||||
},
|
||||
},
|
||||
"shine-pulse": {
|
||||
@@ -83,72 +85,72 @@ const config = {
|
||||
"background-position": "0% 0%",
|
||||
},
|
||||
},
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-accordion-content-height)' },
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: '0' },
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
'border-beam': {
|
||||
'100%': {
|
||||
'offset-distance': '100%',
|
||||
"border-beam": {
|
||||
"100%": {
|
||||
"offset-distance": "100%",
|
||||
},
|
||||
},
|
||||
'image-glow': {
|
||||
'0%': {
|
||||
'opacity': '0',
|
||||
'animation-timing-function': 'cubic-bezier(0.74, 0.25, 0.76, 1)',
|
||||
"image-glow": {
|
||||
"0%": {
|
||||
opacity: "0",
|
||||
"animation-timing-function": "cubic-bezier(0.74, 0.25, 0.76, 1)",
|
||||
},
|
||||
'10%': {
|
||||
'opacity': '0.7',
|
||||
'animation-timing-function': 'cubic-bezier(0.12, 0.01, 0.08, 0.99)',
|
||||
"10%": {
|
||||
opacity: "0.7",
|
||||
"animation-timing-function": "cubic-bezier(0.12, 0.01, 0.08, 0.99)",
|
||||
},
|
||||
'100%': {
|
||||
opacity: '0.4',
|
||||
"100%": {
|
||||
opacity: "0.4",
|
||||
},
|
||||
},
|
||||
'fade-in': {
|
||||
from: { opacity: '0', transform: 'translateY(-10px)' },
|
||||
to: { opacity: '1', transform: 'none' },
|
||||
"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' },
|
||||
"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',
|
||||
shimmer: {
|
||||
"0%, 90%, 100%": {
|
||||
"background-position": "calc(-100% - var(--shimmer-width)) 0",
|
||||
},
|
||||
'30%, 60%': {
|
||||
'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: {
|
||||
from: { transform: "translateX(0)" },
|
||||
to: { transform: "translateX(calc(-100% - var(--gap)))" },
|
||||
},
|
||||
'marquee-vertical': {
|
||||
from: { transform: 'translateY(0)' },
|
||||
to: { transform: 'translateY(calc(-100% - var(--gap)))' },
|
||||
"marquee-vertical": {
|
||||
from: { transform: "translateY(0)" },
|
||||
to: { transform: "translateY(calc(-100% - var(--gap)))" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
orbit: "orbit calc(var(--duration)*1s) linear infinite",
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear',
|
||||
'image-glow': 'image-glow 4100ms 600ms ease-out forwards',
|
||||
'fade-in': 'fade-in 1000ms var(--animation-delay, 0ms) ease forwards',
|
||||
'fade-up': 'fade-up 1000ms var(--animation-delay, 0ms) ease forwards',
|
||||
'shimmer': 'shimmer 8s infinite',
|
||||
'marquee': 'marquee var(--duration) infinite linear',
|
||||
'marquee-vertical': 'marquee-vertical var(--duration) linear infinite',
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
|
||||
"image-glow": "image-glow 4100ms 600ms ease-out forwards",
|
||||
"fade-in": "fade-in 1000ms var(--animation-delay, 0ms) ease forwards",
|
||||
"fade-up": "fade-up 1000ms var(--animation-delay, 0ms) ease forwards",
|
||||
shimmer: "shimmer 8s infinite",
|
||||
marquee: "marquee var(--duration) infinite linear",
|
||||
"marquee-vertical": "marquee-vertical var(--duration) linear infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config
|
||||
} satisfies Config;
|
||||
|
||||
export default config
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user