Refactor Prettier configuration to adhere to code style guidelines

This commit is contained in:
mauro 🤙
2024-09-15 14:46:10 +00:00
parent e7b4be867f
commit f71d765d16
91 changed files with 16139 additions and 25613 deletions

View File

@@ -1,7 +1,7 @@
{
"extends": "next/core-web-vitals",
"rules": {
"react/display-name": "off",
"react/no-unescaped-entities": "off"
}
"extends": "next/core-web-vitals",
"rules": {
"react/display-name": "off",
"react/no-unescaped-entities": "off"
}
}

View File

@@ -1,7 +1,7 @@
{
"tabWidth": 2,
"useTabs": true,
"semi": true,
"trailingComma": "all",
"plugins": ["prettier-plugin-tailwindcss"]
"tabWidth": 2,
"useTabs": true,
"semi": true,
"trailingComma": "all",
"plugins": ["prettier-plugin-tailwindcss"]
}

12
.prettierrc.json Normal file
View File

@@ -0,0 +1,12 @@
{
"bracketSameLine": true,
"endOfLine": "lf",
"trailingComma": "es5",
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"jsxSingleQuote": false,
"semi": true,
"printWidth": 128,
"plugins": []
}

View File

@@ -1,45 +1,44 @@
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')
const { PHASE_DEVELOPMENT_SERVER } = require("next/constants");
/** @type {import('next').NextConfig} */
const nextConfig = (phase, { defaultConfig }) => {
const defaultConfigWWW = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "raw.githubusercontent.com",
},
{
protocol: "https",
hostname: "cdn.jsdelivr.net",
port: '',
pathname: '/gh/zen-browser/**',
}
],
domains: ['localhost', 'cdn.jsdelivr.net', "raw.githubusercontent.com"], // Allow images from jsDelivr
},
experimental: {
serverActions: {
// edit: updated to new key. Was previously `allowedForwardedHosts`
allowedOrigins: ["localhost:3000", "get-zen.vercel.app"],
},
},
compiler: {
styledComponents: true,
},
};
if (phase === PHASE_DEVELOPMENT_SERVER) {
return {
...defaultConfigWWW,
// development only config options here
};
}
return {
...defaultConfigWWW,
// production only config options here
output: 'export',
};
const defaultConfigWWW = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "raw.githubusercontent.com",
},
{
protocol: "https",
hostname: "cdn.jsdelivr.net",
port: "",
pathname: "/gh/zen-browser/**",
},
],
domains: ["localhost", "cdn.jsdelivr.net", "raw.githubusercontent.com"], // Allow images from jsDelivr
},
experimental: {
serverActions: {
// edit: updated to new key. Was previously `allowedForwardedHosts`
allowedOrigins: ["localhost:3000", "get-zen.vercel.app"],
},
},
compiler: {
styledComponents: true,
},
};
if (phase === PHASE_DEVELOPMENT_SERVER) {
return {
...defaultConfigWWW,
// development only config options here
};
}
return {
...defaultConfigWWW,
// production only config options here
output: "export",
};
};
module.exports = nextConfig;

View File

@@ -1,17 +1,17 @@
{
"$schema": "https://nyxbui.design/schema.json",
"style": "miami",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
"$schema": "https://nyxbui.design/schema.json",
"style": "miami",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

28797
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +1,72 @@
{
"name": "zen-website",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
"pages:build": "npx @cloudflare/next-on-pages",
"preview": "npm run pages:build && wrangler pages dev",
"deploy": "npm run pages:build && wrangler pages deploy"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@supabase/supabase-js": "^2.45.1",
"@vercel/postgres": "^0.9.0",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cobe": "^0.6.3",
"dotenv": "^16.4.5",
"feed": "^4.2.2",
"framer-motion": "^11.3.24",
"lucide-react": "^0.400.0",
"moment": "^2.30.1",
"next": "14.2.4",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.2",
"react-markdown": "^9.0.1",
"react-spring": "^9.7.4",
"react-sticky-box": "^2.0.5",
"react-sticky-el": "^2.1.0",
"styled-components": "^6.1.12",
"tailwind-merge": "^2.5.1",
"tailwindcss-animate": "^1.0.7",
"zen-website": "file:",
"zod": "^3.23.8"
},
"devDependencies": {
"@cloudflare/next-on-pages": "^1.13.2",
"@types/canvas-confetti": "^1.6.4",
"@types/node": "^20.14.15",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/sync-fetch": "^0.4.3",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.4",
"postcss": "^8.4.41",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.9",
"typescript": "^5.5.4"
}
"name": "zen-website",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
"pages:build": "npx @cloudflare/next-on-pages",
"preview": "npm run pages:build && wrangler pages dev",
"deploy": "npm run pages:build && wrangler pages deploy"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@supabase/supabase-js": "^2.45.1",
"@vercel/postgres": "^0.9.0",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cobe": "^0.6.3",
"dotenv": "^16.4.5",
"feed": "^4.2.2",
"framer-motion": "^11.3.24",
"lucide-react": "^0.400.0",
"moment": "^2.30.1",
"next": "14.2.4",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.2",
"react-markdown": "^9.0.1",
"react-spring": "^9.7.4",
"react-sticky-box": "^2.0.5",
"react-sticky-el": "^2.1.0",
"styled-components": "^6.1.12",
"tailwind-merge": "^2.5.1",
"tailwindcss-animate": "^1.0.7",
"zen-website": "file:",
"zod": "^3.23.8"
},
"devDependencies": {
"@cloudflare/next-on-pages": "^1.13.2",
"@types/canvas-confetti": "^1.6.4",
"@types/node": "^20.14.15",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/sync-fetch": "^0.4.3",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.4",
"postcss": "^8.4.41",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.9",
"typescript": "^5.5.4"
}
}

View File

@@ -1,8 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
plugins: {
tailwindcss: {},
},
};
export default config;

View File

@@ -1,7 +1,6 @@
import { redirect } from "next/navigation";
export default function WhyAreYouEvenHere() {
redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
return null;
redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
return null;
}

View File

@@ -3,15 +3,18 @@ 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">
<Markdown>
{`
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<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
* [**Mauro Baladés**](https://github.com/mauro-balades): Creator, Main Developer, and a funny guy.
@@ -33,8 +36,8 @@ export default function PrivacyPolicy() {
![Contributors](https://contributors-img.web.app/image?repo=zen-browser/www)
`}
</Markdown>
</div>
</main>
)
</Markdown>
</div>
</main>
);
}

View File

@@ -3,9 +3,9 @@ import Footer from "@/components/footer";
import { Navigation } from "@/components/navigation";
export default function BrandingAssetsPage() {
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<BrandingAssets />
</main>
);
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<BrandingAssets />
</main>
);
}

View File

@@ -3,9 +3,9 @@ import Footer from "@/components/footer";
import { Navigation } from "@/components/navigation";
export default function BrandingAssetsPage() {
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<CreateThemePage />
</main>
);
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<CreateThemePage />
</main>
);
}

View File

@@ -1,12 +1,11 @@
import DownloadPage from "@/components/download";
import Footer from "@/components/footer";
import { Navigation } from "@/components/navigation";
export default function Download() {
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<DownloadPage />
</main>
);
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<DownloadPage />
</main>
);
}

View File

@@ -14,38 +14,37 @@ const RSS_ENTRY_LIMIT = 20;
* @returns The RSS feed for the Zen Browser release notes.
*/
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();
// Just in case the release notes array is empty for whatever reason.
const latestDate =
releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date) : new Date();
const feed = new Feed({
id: "https://www.zen-browser.app/release-notes",
link: "https://www.zen-browser.app/release-notes",
title: "Zen Browser Release Notes",
description: "Release Notes for the Zen Browser",
language: "en",
favicon: "https://www.zen-browser.app/favicon.ico",
copyright: `Zen Browser © ${new Date().getFullYear()} - Made with ❤️ by the Zen team.`,
updated: latestDate,
});
const feed = new Feed({
id: "https://www.zen-browser.app/release-notes",
link: "https://www.zen-browser.app/release-notes",
title: "Zen Browser Release Notes",
description: "Release Notes for the Zen Browser",
language: "en",
favicon: "https://www.zen-browser.app/favicon.ico",
copyright: `Zen Browser © ${new Date().getFullYear()} - Made with ❤️ by the Zen team.`,
updated: latestDate,
});
for (const releaseNote of releaseNotes.slice(0, RSS_ENTRY_LIMIT)) {
feed.addItem({
title: `Release notes for version ${releaseNote.version}`,
id: `https://www.zen-browser.app/release-notes/${releaseNote.version}`,
link: `https://www.zen-browser.app/release-notes/${releaseNote.version}`,
date: formatRssDate(releaseNote.date),
description: releaseNote.extra,
content: formatReleaseNote(releaseNote),
});
}
for (const releaseNote of releaseNotes.slice(0, RSS_ENTRY_LIMIT)) {
feed.addItem({
title: `Release notes for version ${releaseNote.version}`,
id: `https://www.zen-browser.app/release-notes/${releaseNote.version}`,
link: `https://www.zen-browser.app/release-notes/${releaseNote.version}`,
date: formatRssDate(releaseNote.date),
description: releaseNote.extra,
content: formatReleaseNote(releaseNote),
});
}
return new Response(feed.rss2(), {
headers: {
'Content-Type': 'application/xml; charset=utf-8',
},
});
return new Response(feed.rss2(), {
headers: {
"Content-Type": "application/xml; charset=utf-8",
},
});
}
/**
@@ -56,15 +55,15 @@ export async function GET() {
* @returns The passed in date string as a Date object.
*/
function formatRssDate(dateStr: string) {
const splitDate = dateStr.split("/");
if (splitDate.length !== 3) {
throw new Error("Invalid date format");
}
const splitDate = dateStr.split("/");
if (splitDate.length !== 3) {
throw new Error("Invalid date format");
}
const day = Number(splitDate[0]);
const month = Number(splitDate[1]) - 1;
const year = Number(splitDate[2]);
return new Date(year, month, day);
const day = Number(splitDate[0]);
const month = Number(splitDate[1]) - 1;
const year = Number(splitDate[2]);
return new Date(year, month, day);
}
/**
@@ -73,38 +72,39 @@ 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>`
}
if (releaseNote.extra) {
content += `<p>${releaseNote.extra.replace(/(\n)/g, "<br />")}</p>`;
}
if (releaseNote.breakingChanges) {
content += `<h2>⚠️ Breaking changes</h2>`
content += `<ul>`
for (const breakingChange of releaseNote.breakingChanges) {
content += `<li>${breakingChange}</li>`
}
content += `</ul>`
}
if (releaseNote.breakingChanges) {
content += `<h2>⚠️ Breaking changes</h2>`;
content += `<ul>`;
for (const breakingChange of releaseNote.breakingChanges) {
content += `<li>${breakingChange}</li>`;
}
content += `</ul>`;
}
if (releaseNote.features) {
content += `<h2>⭐ Features</h2>`
content += `<ul>`
for (const feature of releaseNote.features) {
content += `<li>${feature}</li>`
}
content += `</ul>`
}
if (releaseNote.features) {
content += `<h2>⭐ Features</h2>`;
content += `<ul>`;
for (const feature of releaseNote.features) {
content += `<li>${feature}</li>`;
}
content += `</ul>`;
}
if (releaseNote.fixes) {
content += `<h2>✓ Fixes</h2>`
content += `<ul>`
for (const fix of releaseNote.fixes) {
content += `<li>${fix.description}</li>`
}
content += `</ul>`
}
if (releaseNote.fixes) {
content += `<h2>✓ Fixes</h2>`;
content += `<ul>`;
for (const fix of releaseNote.fixes) {
content += `<li>${fix.description}</li>`;
}
content += `</ul>`;
}
return content;
return content;
}

View File

@@ -9,39 +9,43 @@ import { Navigation } from "@/components/navigation";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Zen Browser",
description: "Download now and experience the Zen Browser",
keywords: ["Zen", "Browser", "Zen Browser", "Web", "Internet", "Fast"],
title: "Zen Browser",
description: "Download now and experience the Zen Browser",
keywords: ["Zen", "Browser", "Zen Browser", "Web", "Internet", "Fast"],
};
export default async function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
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" />
</head>
<body className={inter.className}>
<ThemeProvider
attribute="class"
enableSystem
disableTransitionOnChange
>
<StyledComponentsRegistry>
<div>
{children}
<Footer />
<Navigation /> {/* At the bottom of the page */}
</div>
</StyledComponentsRegistry>
</ThemeProvider>
</body>
</html>
);
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"
/>
</head>
<body className={inter.className}>
<ThemeProvider attribute="class" enableSystem disableTransitionOnChange>
<StyledComponentsRegistry>
<div>
{children}
<Footer />
<Navigation /> {/* At the bottom of the page */}
</div>
</StyledComponentsRegistry>
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -2,19 +2,19 @@ import { Button } from "@/components/ui/button";
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">
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"
>
<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>
</div>
</main>
);
return (
<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 -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>
</div>
</main>
);
}

View File

@@ -6,11 +6,10 @@ import Header from "@/components/header";
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">
<Header />
<Features />
</main>
);
return (
<main className="flex min-h-screen flex-col items-center justify-start overflow-x-hidden">
<Header />
<Features />
</main>
);
}

View File

@@ -1,58 +1,57 @@
#policy h1 {
font-size: 2.5em;
margin: 0.67em 0;
font-weight: bold;
font-size: 2.5em;
margin: 0.67em 0;
font-weight: bold;
}
#policy {
padding-top: 2.5em;
margin-top: 4em;
padding-top: 2.5em;
margin-top: 4em;
}
#policy h1:first-child {
margin-top: 0 !important;
margin-top: 0 !important;
}
#policy h2 {
font-size: 2em;
margin: 0.83em 0;
font-weight: bold;
font-size: 2em;
margin: 0.83em 0;
font-weight: bold;
}
#policy h3 {
font-size: 1.5em;
margin: 1em 0;
font-weight: semi-bold;
font-size: 1.5em;
margin: 1em 0;
font-weight: semi-bold;
}
#policy ul {
margin: 1em 0;
margin: 1em 0;
}
#policy li {
list-style: circle;
margin-left: 1.1em;
font-size: 1.1em;
list-style: circle;
margin-left: 1.1em;
font-size: 1.1em;
}
/* Link styles */
#policy a {
color: #007bff;
transition: color 0.2s ease-in-out;
color: #007bff;
transition: color 0.2s ease-in-out;
}
#policy a:hover {
text-decoration: underline;
color: #0056b3;
text-decoration: underline;
color: #0056b3;
}
#policy hr {
margin: 2em 0;
border: 1px solid #ddd;
margin: 2em 0;
border: 1px solid #ddd;
}
#policy p {
margin: 1em 0;
line-height: 1.6;
margin: 1em 0;
line-height: 1.6;
}

View File

@@ -3,15 +3,18 @@ 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">
<Markdown>
{`
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<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
* Last updated: 2024-08-12
@@ -89,8 +92,8 @@ If you have any questions or concerns about this Privacy Policy or Zen Browser,
---
By using Zen Browser, you agree to this Privacy Policy. Remember, with Zen, your privacy is in your hands.`}
</Markdown>
</div>
</main>
)
</Markdown>
</div>
</main>
);
}

View File

@@ -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,36 +6,45 @@ 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 } }) {
const { version } = params;
export default function ReleaseNotePage({
params,
}: {
params: { version: string };
}) {
const { version } = params;
if (version === "latest") {
return redirect(`/release-notes/${releaseNotes[0].version}`);
}
if (version === "latest") {
return redirect(`/release-notes/${releaseNotes[0].version}`);
}
const currentIndex = releaseNotes.findIndex((note) => note.version === version);
const releaseNote = releaseNotes[currentIndex];
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>
<a href="/release-notes">
<Button className="mt-4 items-center justify-center">
Back to release notes
</Button>
</a>
</div>
</main>
);
}
if (!releaseNote) {
return (
<main className="flex min-h-screen flex-col items-center justify-center">
<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
</Button>
</a>
</div>
</main>
);
}
return redirect(`/release-notes#${version}`);
return redirect(`/release-notes#${version}`);
}

View File

@@ -1,26 +1,47 @@
import ReleaseNoteElement from "@/components/release-note";
import { releaseNotes } from "@/lib/release-notes";
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"],
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",
],
};
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>
<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!
</p>
{releaseNotes.map((releaseNote) => (
<ReleaseNoteElement key={releaseNote.version} data={releaseNote} />
))}
</div>
</main>
)
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<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!
</p>
{releaseNotes.map((releaseNote) => (
<ReleaseNoteElement key={releaseNote.version} data={releaseNote} />
))}
</div>
</main>
);
}

View File

@@ -1,4 +1,3 @@
import Footer from "@/components/footer";
import { Navigation } from "@/components/navigation";
import ThemePage from "@/components/theme-page";
@@ -6,49 +5,53 @@ import { getAllThemes, getThemeFromId } from "@/lib/themes";
import { Metadata, ResolvingMetadata } from "next";
export async function generateMetadata(
{ params, searchParams }: any,
parent: ResolvingMetadata
{ params, searchParams }: any,
parent: ResolvingMetadata,
): Promise<Metadata> {
const theme = params.theme
const themeData = await getThemeFromId(theme);
if (!themeData) {
return {
title: "Theme not found",
description: "Theme not found",
};
}
return {
title: themeData.name,
description: themeData.description,
keywords: [themeData.name, themeData.description],
openGraph: {
title: themeData.name,
description: themeData.description,
images: [
{
url: themeData.image,
width: 500,
height: 500,
alt: themeData.name,
},
],
},
};
const theme = params.theme;
const themeData = await getThemeFromId(theme);
if (!themeData) {
return {
title: "Theme not found",
description: "Theme not found",
};
}
return {
title: themeData.name,
description: themeData.description,
keywords: [themeData.name, themeData.description],
openGraph: {
title: themeData.name,
description: themeData.description,
images: [
{
url: themeData.image,
width: 500,
height: 500,
alt: themeData.name,
},
],
},
};
}
export async function generateStaticParams() {
const themes = await getAllThemes();
console.log(themes);
return themes.map((theme) => ({
theme: theme.id,
}));
const themes = await getAllThemes();
console.log(themes);
return themes.map((theme) => ({
theme: theme.id,
}));
}
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">
<ThemePage themeID={theme} />
</main>
);
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">
<ThemePage themeID={theme} />
</main>
);
}

View File

@@ -1,4 +1,3 @@
import Footer from "@/components/footer";
import MarketplacePage from "@/components/marketplace";
import { Navigation } from "@/components/navigation";
@@ -6,9 +5,9 @@ import { getAllThemes, ZenTheme } from "@/lib/themes";
import { GetStaticProps } from "next";
export default async function ThemesMarketplace() {
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<MarketplacePage themes={await getAllThemes()} />
</main>
);
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<MarketplacePage themes={await getAllThemes()} />
</main>
);
}

View File

@@ -1,12 +1,11 @@
import WelcomePage from "@/components/welcome";
import Footer from "@/components/footer";
import { Navigation } from "@/components/navigation";
export default function Download() {
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<WelcomePage />
</main>
);
return (
<main className="flex min-h-screen flex-col items-center justify-start">
<WelcomePage />
</main>
);
}

View File

@@ -1,9 +1,9 @@
import Image from "next/image";
function imageLoader({ src }: { src: string }) {
return `https://cdn.jsdelivr.net/gh/zen-browser/${src}`;
return `https://cdn.jsdelivr.net/gh/zen-browser/${src}`;
}
export default function CachedImage({ ...props }: any) {
return <Image {...props} loader={imageLoader} />;
return <Image {...props} loader={imageLoader} />;
}

View File

@@ -1,77 +1,93 @@
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 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>
</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">
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">
{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">
<a
href={`/logos/zen-${color}.svg`}
download={`zen-${color}.svg`}
className="text-blue-500 text-md ml-2"
>
{color}
</a>
</div>
</div>
))}
</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.
</p>
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-10 mt-10 w-full">
{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">
<a
href={`/logos/zen-alpha-${color}.svg`}
download={`zen-alpha-${color}.svg`}
className="text-blue-500 text-md ml-2"
>
{color}
</a>
</div>
</div>
))}
</div>
</div>
<div className="mt-10">
<h2 className="text-2xl font-bold">License</h2>
<p className="text-muted-foreground mt-2">
All branding assets are licensed under the{" "}
<a
href="https://creativecommons.org/licenses/by-sa/4.0/"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500"
>
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.
<br />
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.
</p>
</div>
</div>
);
return (
<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 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="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="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="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-md ml-2 text-blue-500"
>
{color}
</a>
</div>
</div>
))}
</div>
</div>
<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="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="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-md ml-2 text-blue-500"
>
{color}
</a>
</div>
</div>
))}
</div>
</div>
<div className="mt-10">
<h2 className="text-2xl font-bold">License</h2>
<p className="mt-2 text-muted-foreground">
All branding assets are licensed under the{" "}
<a
href="https://creativecommons.org/licenses/by-sa/4.0/"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500"
>
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.
<br />
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.
</p>
</div>
</div>
);
}

View 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>
</>
);
}

View File

@@ -4,41 +4,49 @@ 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,
accentColor,
secondaryColor,
tertiaryColor,
colorsBorder,
dialogBg,
}: {
primaryColor: string;
accentColor: string;
secondaryColor: string;
tertiaryColor: string;
colorsBorder: string;
dialogBg: string;
}) => `
${({
primaryColor,
accentColor,
secondaryColor,
tertiaryColor,
colorsBorder,
dialogBg,
}: {
primaryColor: string;
accentColor: string;
secondaryColor: string;
tertiaryColor: string;
colorsBorder: string;
dialogBg: string;
}) => `
--zen-primary-color: ${accentColor};
--zen-colors-primary: ${primaryColor};
@@ -51,180 +59,298 @@ const ThemeFormWrapper = styled.div<{
`;
const defaultStyles = {
primaryColor: {
light: "color-mix(in srgb, var(--zen-primary-color) 50%, black 50%)",
dark: "color-mix(in srgb, var(--zen-primary-color) 50%, black 50%)",
},
secondaryColor: {
light: "color-mix(in srgb, var(--zen-primary-color) 40%, white 60%)",
dark: "color-mix(in srgb, var(--zen-primary-color) 40%, black 60%)",
},
tertiaryColor: {
light: "color-mix(in srgb, var(--zen-primary-color) 7%, white 93%)",
dark: "color-mix(in srgb, var(--zen-primary-color) 15%, black 85%)",
},
colorsBorder: {
light: "color-mix(in srgb, var(--zen-colors-secondary) 90%, black 10%)",
dark: "color-mix(in srgb, var(--zen-colors-secondary) 80%, black 20%)",
},
dialogBg: {
light: "var(--zen-colors-tertiary)",
dark: "color-mix(in srgb, var(--zen-primary-color) 10%, black 90%)",
},
}
primaryColor: {
light: "color-mix(in srgb, var(--zen-primary-color) 50%, black 50%)",
dark: "color-mix(in srgb, var(--zen-primary-color) 50%, black 50%)",
},
secondaryColor: {
light: "color-mix(in srgb, var(--zen-primary-color) 40%, white 60%)",
dark: "color-mix(in srgb, var(--zen-primary-color) 40%, black 60%)",
},
tertiaryColor: {
light: "color-mix(in srgb, var(--zen-primary-color) 7%, white 93%)",
dark: "color-mix(in srgb, var(--zen-primary-color) 15%, black 85%)",
},
colorsBorder: {
light: "color-mix(in srgb, var(--zen-colors-secondary) 90%, black 10%)",
dark: "color-mix(in srgb, var(--zen-colors-secondary) 80%, black 20%)",
},
dialogBg: {
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 [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 [dialogBg, setDialogBg] = React.useState(defaultStyles.dialogBg.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);
}, [isDarkMode]);
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,
);
}, [isDarkMode]);
const generateThemeData = () => {
let theme: any = {
isDarkMode,
};
// Dont add the default values
if (primaryColor !== (isDarkMode ? defaultStyles.primaryColor.dark : defaultStyles.primaryColor.light)) {
theme["primaryColor"] = primaryColor;
}
if (secondaryColor !== (isDarkMode ? defaultStyles.secondaryColor.dark : defaultStyles.secondaryColor.light)) {
theme["secondaryColor"] = secondaryColor;
}
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 (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);
}
const generateThemeData = () => {
let theme: any = {
isDarkMode,
};
// Dont add the default values
if (
primaryColor !==
(isDarkMode
? defaultStyles.primaryColor.dark
: defaultStyles.primaryColor.light)
) {
theme["primaryColor"] = primaryColor;
}
if (
secondaryColor !==
(isDarkMode
? defaultStyles.secondaryColor.dark
: defaultStyles.secondaryColor.light)
) {
theme["secondaryColor"] = secondaryColor;
}
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 (
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
primaryColor={primaryColor}
accentColor={selectedColor}
secondaryColor={secondaryColor}
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.
</div>
<div className="flex items-center mt-2">
{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" : "")}
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>
<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>
<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>
</div>
<h2 className="text-lg mt-8 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>
<h2 className="text-lg mt-8 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>
<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>
</div>
<h2 className="text-lg mt-8 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>
<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>
<Sheet>
<SheetTrigger asChild>
<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.
</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>
<SheetFooter className="mt-4">
<Button onClick={() =>
navigator.clipboard.writeText(generateThemeData())
} 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");
}} >
Submit theme
</Button>
</SheetFooter>
</SheetContent>
</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>
</div>
</div>
</div>
</ThemeFormWrapper>
);
return (
<ThemeFormWrapper
primaryColor={primaryColor}
accentColor={selectedColor}
secondaryColor={secondaryColor}
tertiaryColor={tertiaryColor}
colorsBorder={colorsBorder}
dialogBg={dialogBg}
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="mt-2 flex items-center">
{COLORS.map((color) => (
<div
key={color}
onClick={() => setSelectedColor(color)}
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="h-7 w-9 cursor-pointer rounded outline-none"
/>
</div>
<div className="flex flex-col lg:flex-row">
<div className="w-full">
<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="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="mt-8 text-lg font-bold opacity-70">
Secondary 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={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="mt-8 text-lg font-bold opacity-70">
Tertiary 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={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="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="mt-8 text-lg font-bold opacity-70">
Dialog background 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={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 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>
</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.
</SheetDescription>
</SheetHeader>
<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={() =>
navigator.clipboard.writeText(generateThemeData())
}
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",
);
}}
>
Submit theme
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</div>
{/* Preview */}
<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>
</div>
</ThemeFormWrapper>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,316 +1,561 @@
'use client';
import Sticky from 'react-sticky-el';
"use client";
import Sticky from "react-sticky-el";
import {
BookmarkCheckIcon,
CheckIcon,
ChevronLeft,
ChevronRight,
ExternalLinkIcon,
EyeIcon,
EyeOffIcon,
Github,
HeartHandshake,
HeartPulseIcon,
HomeIcon,
PaintBucket,
PersonStanding,
RabbitIcon,
ShieldAlertIcon,
ShieldCheck,
SidebarCloseIcon,
SidebarIcon,
SidebarOpenIcon,
SpaceIcon,
SplitSquareHorizontal,
SplitSquareVertical,
SplitSquareVerticalIcon,
TableIcon,
XIcon,
} from 'lucide-react';
BookmarkCheckIcon,
CheckIcon,
ChevronLeft,
ChevronRight,
ExternalLinkIcon,
EyeIcon,
EyeOffIcon,
Github,
HeartHandshake,
HeartPulseIcon,
HomeIcon,
PaintBucket,
PersonStanding,
RabbitIcon,
ShieldAlertIcon,
ShieldCheck,
SidebarCloseIcon,
SidebarIcon,
SidebarOpenIcon,
SpaceIcon,
SplitSquareHorizontal,
SplitSquareVertical,
SplitSquareVerticalIcon,
TableIcon,
XIcon,
} from "lucide-react";
import {
Cross1Icon,
EyeClosedIcon,
HeartFilledIcon,
Link1Icon,
LockClosedIcon,
QuestionMarkCircledIcon,
QuestionMarkIcon,
ReloadIcon,
SpaceBetweenHorizontallyIcon,
UpdateIcon,
} from '@radix-ui/react-icons';
Cross1Icon,
EyeClosedIcon,
HeartFilledIcon,
Link1Icon,
LockClosedIcon,
QuestionMarkCircledIcon,
QuestionMarkIcon,
ReloadIcon,
SpaceBetweenHorizontallyIcon,
UpdateIcon,
} 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,
TableCell,
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';
Table,
TableBody,
TableCell,
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";
function Checkmark() {
return (
<CheckIcon className="text-black rounded-full bg-green-500 dark:bg-green-400 p-1 w-7 h-7 flex-none" />
);
return (
<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" />
);
return (
<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" />
);
return (
<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>
<div className="relative">
<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="relative">
<Button className='mt-8' onClick={() => window.open('/download', '_self')}>Download Now</Button>
</div>
</div>
</div>
const [feature, setFeature] = useState("item-1");
return (
<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>
</div>
</div>
<hr />
<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>
</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.
</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>
</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'>
<Checkmark />
<p className='ml-2 text-gray-600 dark:text-gray-300'>Quick Access</p>
</div>
<div className='flex items-center mt-4'>
<Checkmark />
<p className='ml-2 text-gray-600 dark:text-gray-300'>Customizable</p>
</div>
<div className='flex items-center mt-4'>
<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 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 className="relative">
<Button className='mt-8' onClick={() => window.open('/download', '_self')}>What are you waiting for?</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'>
<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.
</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.
</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.
</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" />
{feature == "item-1" && (
<div className='w-full h-full absolute top-0 left-0 grid 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>
<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>
</div>
)}
{feature == "item-2" && (
<div className='w-full h-full absolute top-0 left-0 grid 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>
<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>
</div>
)}
{feature == "item-3" && (
<div className='w-full h-full absolute top-0 left-0 grid 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>
<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>
</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="relative">
<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="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>
</div>
</div>
</div>
</section>
);
<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"
onClick={() =>
window.open("https://github.com/zen-browser", "_blank")
}
>
GitHub Page
</Button>
</div>
<div className="mt-14 w-full">
<div className="flex items-center">
<Checkmark />
<p className="ml-2 text-gray-600 dark:text-gray-300">
Firefox Based
</p>
</div>
<div className="mt-5 flex items-center">
<Checkmark />
<p className="ml-2 text-gray-600 dark:text-gray-300">
Fully Open source
</p>
</div>
<div className="mt-5 flex items-center">
<Checkmark />
<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>
</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">
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")}
>
Download Now
</Button>
</div>
</div>
</div>
<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.
</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.
</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.
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<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="absolute left-0 top-0 grid h-full w-full grid-rows-3">
<div></div>
<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="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="absolute left-0 top-0 grid h-full w-full grid-rows-3">
<div></div>
<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="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="absolute left-0 top-0 grid h-full w-full grid-rows-3">
<div></div>
<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="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="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>
</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 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="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>
</section>
);
}

View File

@@ -1,4 +1,3 @@
import Link from "next/link";
import Logo from "./logo";
import TextReveal from "./ui/text-reveal";
@@ -7,108 +6,103 @@ import { MastodonLogo } from "./icons/mastodon";
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="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">
<a href="https://github.com/zen-browser">
<GitHubLogoIcon className="w-5 h-5" />
</a>
<a href="https://discord.gg/zen-browser" className="ml-5">
<DiscordLogoIcon className="w-5 h-5" />
</a>
<a href="https://fosstodon.org/@zenbrowser" className="ml-5">
<MastodonLogo className="w-5 h-5" />
</a>
</div>
</div>
</div>
<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">
<li>
<a href="/themes">
Themes
</a>
</li>
<li className="mt-2">
<a href="/download">
Download
</a>
</li>
<li className="mt-2">
<a href="/create-theme">
Create a Theme
</a>
</li>
</ul>
</div>
<div className="mt-10 md:mt-0 md:ml-12 lg:ml-24">
<h2 className="text-md font-bold opacity-80">Get Help</h2>
<ul className="mt-4 opacity-70 font-normal">
<li>
<a href="https://discord.com/servers/mauro-s-little-sweatshop-1088172780480114748">
Discord
</a>
</li>
<li className="mt-2 font-normal">
<a href="https://github.com/zen-browser/desktop/issues">
Report an Issue
</a>
</li>
</ul>
<h2 className="text-md font-bold opacity-80 mt-8">About</h2>
<ul className="mt-4 opacity-70 font-normal">
<li className="mt-2">
<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">
<h2 className="text-md font-bold opacity-80">Resources</h2>
<ul className="mt-4 opacity-70 font-normal">
<li>
<a href="/branding-assets">Branding Assets</a>
</li>
<li className="mt-2">
<a href="https://github.com/zen-browser/desktop">Source Code</a>
</li>
<li className="mt-2">
<a href="https://docs.zen-browser.app">Documentation</a>
</li>
<li className="mt-2">
<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">
<li>
<a href="https://patreon.com/zen_browser">Patreon</a>
</li>
<li className="mt-2">
<a href="https://ko-fi.com/zen_browser">Ko-fi</a>
</li>
</ul>
</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>
<a href="/download" className="ml-auto">
<Button className="ml-auto">Download</Button>
</a>
</div>
</div>
);
return (
<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 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="h-5 w-5" />
</a>
<a href="https://discord.gg/zen-browser" className="ml-5">
<DiscordLogoIcon className="h-5 w-5" />
</a>
<a href="https://fosstodon.org/@zenbrowser" className="ml-5">
<MastodonLogo className="h-5 w-5" />
</a>
</div>
</div>
</div>
<div className="flex flex-col md:flex-row">
<div>
<h2 className="text-md font-bold opacity-80">Get Started</h2>
<ul className="mt-4 font-normal opacity-70">
<li>
<a href="/themes">Themes</a>
</li>
<li className="mt-2">
<a href="/download">Download</a>
</li>
<li className="mt-2">
<a href="/create-theme">Create a Theme</a>
</li>
</ul>
</div>
<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 font-normal opacity-70">
<li>
<a href="https://discord.com/servers/mauro-s-little-sweatshop-1088172780480114748">
Discord
</a>
</li>
<li className="mt-2 font-normal">
<a href="https://github.com/zen-browser/desktop/issues">
Report an Issue
</a>
</li>
</ul>
<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>
</li>
<li className="mt-2">
<a href="/privacy-policy">Privacy Policy</a>
</li>
</ul>
</div>
<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 font-normal opacity-70">
<li>
<a href="/branding-assets">Branding Assets</a>
</li>
<li className="mt-2">
<a href="https://github.com/zen-browser/desktop">Source Code</a>
</li>
<li className="mt-2">
<a href="https://docs.zen-browser.app">Documentation</a>
</li>
<li className="mt-2">
<a href="/release-notes">Release Notes</a>
</li>
</ul>
<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>
<li className="mt-2">
<a href="https://ko-fi.com/zen_browser">Ko-fi</a>
</li>
</ul>
</div>
</div>
</div>
<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>
</div>
</div>
);
}

View File

@@ -11,53 +11,41 @@ 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" });
return (
<>
<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"
>
<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">
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">
<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"
>
<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">
Start Exploring <ChevronDown className="ml-1 size-4" />
</Button>
</a>
</div>
</section>
</>
);
const ref = useRef(null);
const inView = useInView(ref, { once: true, margin: "-100px" });
return (
<>
<section
id="hero"
className="relative mx-auto flex min-h-screen max-w-7xl flex-col justify-center px-6 text-center md:px-8"
>
<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 w-full flex-col justify-center md:flex-row">
<a href="/download">
<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="-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>
</div>
</section>
</>
);
}

View File

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

View File

@@ -6,10 +6,19 @@ import React from "react";
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>
);
return (
<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>
);
}

View File

@@ -5,23 +5,28 @@ import { getAllThemes, getThemesFromSearch, ZenTheme } from "@/lib/themes";
import ThemeCard from "./theme-card";
import { Button } from "./ui/button";
export default function MarketplacePage({ themes }: {themes:ZenTheme[]}) {
const [searchInput, setSearchInput] = React.useState("");
const [tags, setTags] = React.useState<string[]>(["all"]);
export default function MarketplacePage({ themes }: { themes: ZenTheme[] }) {
const [searchInput, setSearchInput] = React.useState("");
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>
</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">
{getThemesFromSearch(themes, searchInput, tags).map((theme) => (
<ThemeCard key={theme.name} theme={theme} />
))}
</div>
</div>
);
return (
<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="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} />
))}
</div>
</div>
);
}

View File

@@ -1,120 +1,107 @@
'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);
const [open, setOpen] = React.useState(false);
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" />
<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"
>
<SidebarOpen className="size-6" />
<span className="sr-only">Toggle Menu</span>
</Button>
</div>
</SheetTrigger>
<SheetContent side="left" className="pr-0">
<MobileLink
href="/"
className="flex items-center"
onOpenChange={setOpen}
>
<Logo withText />
</MobileLink>
<ScrollArea className="mt-4 h-[calc(100vh-8rem)] pl-6">
<div className="flex flex-col space-y-3">
<MobileLink
href="/download"
onOpenChange={setOpen}
>
<div>Download</div>
<p className="opacity-60 text-xs">
Get the latest version of Zen Browser.
</p>
</MobileLink>
<MobileLink
href="/themes"
onOpenChange={setOpen}
>
<div>Theme Store</div>
<p className="opacity-60 text-xs">
Customize your browsing experience.
</p>
</MobileLink>
<MobileLink
href="/release-notes"
onOpenChange={setOpen}
>
<div>Release Notes</div>
<p className="opacity-60 text-xs">
Stay up to date with the latest changes.
</p>
</MobileLink>
<MobileLink
href="https://patreon.com/zen_browser?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
onOpenChange={setOpen}
>
<div>Donate {"<"}3</div>
<p className="opacity-60 text-xs">Support the project</p>
</MobileLink>
{components.map(({title, href, description}) => (
<MobileLink
href={href}
key={href}
onOpenChange={setOpen}
>
<div>{title}</div>
<p className='opacity-60 text-xs'>{description}</p>
</MobileLink>
))}
</div>
</ScrollArea>
</SheetContent>
</Sheet>
)
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className="space-between z-40 flex w-full items-center sm:hidden">
<Logo className="ml-4 size-6" />
<Button
variant="ghost"
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>
</Button>
</div>
</SheetTrigger>
<SheetContent side="left" className="pr-0">
<MobileLink
href="/"
className="flex items-center"
onOpenChange={setOpen}
>
<Logo withText />
</MobileLink>
<ScrollArea className="mt-4 h-[calc(100vh-8rem)] pl-6">
<div className="flex flex-col space-y-3">
<MobileLink href="/download" onOpenChange={setOpen}>
<div>Download</div>
<p className="text-xs opacity-60">
Get the latest version of Zen Browser.
</p>
</MobileLink>
<MobileLink href="/themes" onOpenChange={setOpen}>
<div>Theme Store</div>
<p className="text-xs opacity-60">
Customize your browsing experience.
</p>
</MobileLink>
<MobileLink href="/release-notes" onOpenChange={setOpen}>
<div>Release Notes</div>
<p className="text-xs opacity-60">
Stay up to date with the latest changes.
</p>
</MobileLink>
<MobileLink
href="https://patreon.com/zen_browser?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
onOpenChange={setOpen}
>
<div>Donate {"<"}3</div>
<p className="text-xs opacity-60">Support the project</p>
</MobileLink>
{components.map(({ title, href, description }) => (
<MobileLink href={href} key={href} onOpenChange={setOpen}>
<div>{title}</div>
<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({
href,
onOpenChange,
className,
children,
...props
href,
onOpenChange,
className,
children,
...props
}: MobileLinkProps) {
const router = useRouter()
return (
<a
href={href.toString()}
onClick={() => {
router.push(href.toString())
onOpenChange?.(false)
}}
className={ny(className, "my-4")}
{...props}
>
{children}
</a>
)
const router = useRouter();
return (
<a
href={href.toString()}
onClick={() => {
router.push(href.toString());
onOpenChange?.(false);
}}
className={ny(className, "my-4")}
{...props}
>
{children}
</a>
);
}

View File

@@ -5,36 +5,36 @@ import { useTheme } from "next-themes";
import { Button } from "./ui/button";
export function ModeToggle() {
const { setTheme } = useTheme();
const [currentTheme, setCurrentTheme] = useState('light');
const [mounted, setMounted] = useState(false);
const { setTheme } = useTheme();
const [currentTheme, setCurrentTheme] = useState("light");
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
const savedTheme = localStorage.getItem('theme') || 'light';
setCurrentTheme(savedTheme);
setTheme(savedTheme);
}, [setTheme]);
useEffect(() => {
setMounted(true);
const savedTheme = localStorage.getItem("theme") || "light";
setCurrentTheme(savedTheme);
setTheme(savedTheme);
}, [setTheme]);
const toggleTheme = () => {
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setCurrentTheme(newTheme);
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
};
const toggleTheme = () => {
const newTheme = currentTheme === "light" ? "dark" : "light";
setCurrentTheme(newTheme);
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
};
if (!mounted) {
return null;
}
if (!mounted) {
return null;
}
return (
<Button variant="ghost" size="icon" onClick={toggleTheme}>
{currentTheme === 'light' ? (
<SunIcon className="h-[1.2rem] w-[1.2rem]" />
) : (
<MoonIcon className="h-[1.2rem] w-[1.2rem]" />
)}
<span className="sr-only">Toggle theme</span>
</Button>
);
return (
<Button variant="ghost" size="icon" onClick={toggleTheme}>
{currentTheme === "light" ? (
<SunIcon className="h-[1.2rem] w-[1.2rem]" />
) : (
<MoonIcon className="h-[1.2rem] w-[1.2rem]" />
)}
<span className="sr-only">Toggle theme</span>
</Button>
);
}

View File

@@ -1,167 +1,176 @@
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,
NavigationMenuItem,
NavigationMenuLink,
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"
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
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";
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."
},
{
title: "Discord",
href: "https://discord.gg/zen-browser",
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."
},
{
title: "Branding Assets",
href: "/branding-assets",
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."
},
{
title: "Documentation",
href: "https://docs.zen-browser.app/",
description: "Read the documentation to learn more about Zen Browser."
}
]
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.",
},
{
title: "Discord",
href: "https://discord.gg/zen-browser",
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.",
},
{
title: "Branding Assets",
href: "/branding-assets",
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.",
},
{
title: "Documentation",
href: "https://docs.zen-browser.app/",
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">
<MobileNav />
<NavigationMenu>
<NavigationMenuList className="w-full hidden py-3 sm:flex">
<NavigationMenuItem className="cursor-pointer mr-20">
<NavigationMenuLink href="/">
<Logo withText />
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Getting Started</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
<li className="row-span-3">
<NavigationMenuLink asChild>
<a
className="flex h-full w-full select-none flex-col justify-end rounded-md bg-gradient-to-b from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md"
href="/"
>
<Logo />
<div className="mb-2 mt-4 text-lg font-medium">
Zen Browser
</div>
<p className="text-sm leading-tight text-muted-foreground">
Firefox based browser with a focus on privacy and
customization.
</p>
</a>
</NavigationMenuLink>
</li>
<ListItem href="/download" title="Download">
Start using Zen Browser today with just a few clicks.
</ListItem>
<ListItem href="/themes" title="Themes Store">
Customize your browser with a variety of themes!
</ListItem>
<ListItem href="/release-notes" title="Release Notes">
Stay up to date with the latest changes.
</ListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>
<HeartFilledIcon className="text-red-500" />
<span className="ml-2">Donate</span>
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
<ListItem
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.
</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.
</ListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>{"Useful Links"}</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
{components.map((component) => (
<ListItem
key={component.title}
title={component.title}
href={component.href}
>
{component.description}
</ListItem>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<ModeToggle />
</NavigationMenuList>
</NavigationMenu>
</div>
)
return (
<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="hidden w-full py-3 sm:flex">
<NavigationMenuItem className="mr-20 cursor-pointer">
<NavigationMenuLink href="/">
<Logo withText />
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Getting Started</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
<li className="row-span-3">
<NavigationMenuLink asChild>
<a
className="flex h-full w-full select-none flex-col justify-end rounded-md bg-gradient-to-b from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md"
href="/"
>
<Logo />
<div className="mb-2 mt-4 text-lg font-medium">
Zen Browser
</div>
<p className="text-sm leading-tight text-muted-foreground">
Firefox based browser with a focus on privacy and
customization.
</p>
</a>
</NavigationMenuLink>
</li>
<ListItem href="/download" title="Download">
Start using Zen Browser today with just a few clicks.
</ListItem>
<ListItem href="/themes" title="Themes Store">
Customize your browser with a variety of themes!
</ListItem>
<ListItem href="/release-notes" title="Release Notes">
Stay up to date with the latest changes.
</ListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>
<HeartFilledIcon className="text-red-500" />
<span className="ml-2">Donate</span>
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
<ListItem
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.
</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.
</ListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>{"Useful Links"}</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
{components.map((component) => (
<ListItem
key={component.title}
title={component.title}
href={component.href}
>
{component.description}
</ListItem>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<ModeToggle />
</NavigationMenuList>
</NavigationMenu>
</div>
);
}
const ListItem = React.forwardRef<
React.ElementRef<"a">,
React.ComponentPropsWithoutRef<"a">
React.ElementRef<"a">,
React.ComponentPropsWithoutRef<"a">
>(({ className, title, children, ...props }, ref) => {
return (
<li>
<NavigationMenuLink asChild>
<a
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
)}
{...props}
>
<div className="text-sm font-medium leading-none">{title}</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
{children}
</p>
</a>
</NavigationMenuLink>
</li>
)
})
ListItem.displayName = "ListItem"
return (
<li>
<NavigationMenuLink asChild>
<a
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,
)}
{...props}
>
<div className="text-sm font-medium leading-none">{title}</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
{children}
</p>
</a>
</NavigationMenuLink>
</li>
);
});
ListItem.displayName = "ListItem";

View File

@@ -10,113 +10,124 @@ import { AccordionContent, AccordionTrigger } from "./ui/accordion";
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}>
{moment({
year: parseInt(splitDate[2]),
month: parseInt(splitDate[1]) - 1,
day: parseInt(splitDate[0]),
}).format('MMMM Do, YYYY')}
</StickyBox>
<div className="px-5 md:px-10 md:px-0 md:pr-32">
<h1 className="text-3xl font-bold">
Release notes for {data.version} 🎉
</h1>
<p className="text-md mt-4 text-muted-foreground">
If you encounter any issues, please report them on{" "}
<a
href="https://github.com/zen-browser/desktop/issues/"
className="text-underline text-blue-500"
>
the issues page
</a>
. Thanks everyone for your feedback!
</p>
{data.extra && (
<p
className="text-md text-muted-foreground mt-8"
dangerouslySetInnerHTML={{
__html: data.extra.replace(/(\n)/g, "<br />"),
}}
></p>
)}
<Accordion type="single" collapsible className="mt-8">
{data.breakingChanges && (
<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" />
<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>
))}
</ul>
</AccordionContent>
</AccordionItem>
)
}
{data.fixes && (
<AccordionItem value="fixes" title="Fixes">
<AccordionTrigger className="border-b">
<div className="flex items-center">
<CheckCheckIcon className="mr-2 mt-1 size-5 opacity-50" />
<span className="ml-2">Fixes</span>
</div>
</AccordionTrigger>
<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>
{fix.issue && (
<a
href={`https://github.com/zen-browser/desktop/issues/${fix.issue}`}
className="text-blue-500 ml-1 text-underline"
>
#{fix.issue}
</a>
)}
</li>
))}
</ul>
</AccordionContent>
</AccordionItem>
)}
{data.features && (
<AccordionItem value="features" title="Features">
<AccordionTrigger>
<div className="flex items-center">
<StarIcon className="text-yellow-700 mr-2 mt-1 size-5 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>
))}
</ul>
</AccordionContent>
</AccordionItem>
)}
</Accordion>
</div>
</section>
);
const splitDate = data.date.split("/");
return (
<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")}
</StickyBox>
<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>
<p className="text-md mt-4 text-muted-foreground">
If you encounter any issues, please report them on{" "}
<a
href="https://github.com/zen-browser/desktop/issues/"
className="text-underline text-blue-500"
>
the issues page
</a>
. Thanks everyone for your feedback!
</p>
{data.extra && (
<p
className="text-md mt-8 text-muted-foreground"
dangerouslySetInnerHTML={{
__html: data.extra.replace(/(\n)/g, "<br />"),
}}
></p>
)}
<Accordion type="single" collapsible className="mt-8">
{data.breakingChanges && (
<AccordionItem value="breaking-changes" title="Breaking Changes">
<AccordionTrigger className="border-b">
<div className="flex items-center">
<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="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">
<div className="flex items-center">
<CheckCheckIcon className="mr-2 mt-1 size-5 opacity-50" />
<span className="ml-2">Fixes</span>
</div>
</AccordionTrigger>
<AccordionContent>
<ul className="ml-6" style={{ listStyleType: "initial" }}>
{data.fixes.map((fix) => (
<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-underline ml-1 text-blue-500"
>
#{fix.issue}
</a>
)}
</li>
))}
</ul>
</AccordionContent>
</AccordionItem>
)}
{data.features && (
<AccordionItem value="features" title="Features">
<AccordionTrigger>
<div className="flex items-center">
<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="text-md mt-4 text-muted-foreground"
>
<span className="ml-1">{feature}</span>
</li>
))}
</ul>
</AccordionContent>
</AccordionItem>
)}
</Accordion>
</div>
</section>
);
}

View File

@@ -1,50 +1,78 @@
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
theme,
className,
}: {
theme: ZenTheme;
className?: string;
theme: ZenTheme;
className?: string;
}) {
const maxNameLen = 50;
const maxDescLen = 100;
const maxNameLen = 50;
const maxDescLen = 100;
return (
<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">
{theme.homepage && (
<>
<a href={theme.homepage} className="text-blue-500 text-md" target="_blank" rel="noopener noreferrer">
Homepage
</a>
<span className="text-muted-foreground text-md mx-2">
{"·"}
</span>
</>
)}
<a href={getThemeAuthorLink(theme)} className="text-blue-500 text-md" target="_blank" rel="noopener noreferrer">
Author
</a>
</div>
<p className="text-md mt-2 overflow-ellipsis text-muted-foreground text-start">
{theme.description.substring(0, maxDescLen).trim() +
(theme.description.length > maxDescLen ? "..." : "")}
</p>
</ThemeCardWrapper>
);
return (
<ThemeCardWrapper
onClick={(event) => {
if (event.target instanceof HTMLAnchorElement) return;
window.open(`/themes/${theme.id}`, "_self");
}}
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-md text-blue-500"
target="_blank"
rel="noopener noreferrer"
>
Homepage
</a>
<span className="text-md mx-2 text-muted-foreground">{"·"}</span>
</>
)}
<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-start text-muted-foreground">
{theme.description.substring(0, maxDescLen).trim() +
(theme.description.length > maxDescLen ? "..." : "")}
</p>
</ThemeCardWrapper>
);
}

View File

@@ -1,70 +1,100 @@
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>;
}
const theme = await getThemeFromId(themeID);
if (!theme) {
return <div>Theme not found</div>;
}
const readme = await getThemeMarkdown(theme);
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" />
<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>
{theme.homepage && (
<a
href={theme.homepage}
className="text-blue-500 text-md mt-4"
target="_blank"
rel="noopener noreferrer"
>
Visit Homepage
</a>
)}
<hr className="mt-4" />
<Button
className="mt-4 hidden !rounded-lg"
id="install-theme"
zen-theme-id={theme.id}
>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>
</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 id="policy" className="w-full">
{readme === null ? (
<LoaderCircleIcon className="animate-spin w-12 h-12 mx-auto" />
) : (
<Markdown>{`${readme}`}</Markdown>
)}
</div>
<hr className="my-5" />
<p className="text-muted-foreground text-sm">
Theme by{" "}
<a href={getThemeAuthorLink(theme)} className="text-blue-500 text-md mt-4" target="_blank" rel="noopener noreferrer">
{theme.author}
</a>
{` • v${theme.version}`}
</p>
</div>
</div>
);
return (
<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 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-md mt-4 text-blue-500"
target="_blank"
rel="noopener noreferrer"
>
Visit Homepage
</a>
)}
<hr className="mt-4" />
<Button
className="mt-4 hidden !rounded-lg"
id="install-theme"
zen-theme-id={theme.id}
>
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="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 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="mx-auto h-12 w-12 animate-spin" />
) : (
<Markdown>{`${readme}`}</Markdown>
)}
</div>
<hr className="my-5" />
<p className="text-sm text-muted-foreground">
Theme by{" "}
<a
href={getThemeAuthorLink(theme)}
className="text-md mt-4 text-blue-500"
target="_blank"
rel="noopener noreferrer"
>
{theme.author}
</a>
{` • v${theme.version}`}
</p>
</div>
</div>
);
}

View File

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

View File

@@ -2,50 +2,62 @@ 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;
tags: string[];
setTags: (tags: string[]) => void;
input: string;
setInput: (input: string) => void;
tags: string[];
setTags: (tags: string[]) => void;
}) {
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" />
<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"
/>
<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>
<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>
</div>
<div className="flex flex-wrap gap-2 mt-4">
{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>
))}
</div>
</>
);
return (
<>
<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 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="hidden rounded-full rounded-r-none text-muted md:block"
>
Submit a theme
</Button>
<Button
onClick={() => window.open("/create-theme", "_self")}
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="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 text-white dark:bg-white dark:text-black" : ""}`,
)}
>
{tag}
</Button>
))}
</div>
</>
);
}

View File

@@ -1,57 +1,57 @@
'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>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={ny('border-b', className)}
{...props}
/>
))
AccordionItem.displayName = 'AccordionItem'
<AccordionPrimitive.Item
ref={ref}
className={ny("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<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',
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground size-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
<AccordionPrimitive.Header className="flex">
<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",
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ 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"
{...props}
>
<div className={ny('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
<AccordionPrimitive.Content
ref={ref}
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>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -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,
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',
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)]" />
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",
className,
)}
>
<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>
)
{children}
</div>
);
}

View File

@@ -1,146 +1,144 @@
'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({
width = 40,
height = 40,
x = -1,
y = -1,
strokeDasharray = 0,
numSquares = 50,
className,
maxOpacity = 0.5,
duration = 4,
repeatDelay = 0.5,
...props
width = 40,
height = 40,
x = -1,
y = -1,
strokeDasharray = 0,
numSquares = 50,
className,
maxOpacity = 0.5,
duration = 4,
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),
]
}
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
function generateSquares(count: number) {
return Array.from({ length: count }, (_, i) => ({
id: i,
pos: getPos(),
}))
}
// Adjust the generateSquares function to return objects with an id, x, and y
function generateSquares(count: number) {
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 =>
sq.id === id
? {
...sq,
pos: getPos(),
}
: sq,
),
)
}
// Function to update a single square's position
const updateSquarePosition = (id: number) => {
setSquares((currentSquares) =>
currentSquares.map((sq) =>
sq.id === id
? {
...sq,
pos: getPos(),
}
: sq,
),
);
};
// Update squares to animate in
useEffect(() => {
if (dimensions.width && dimensions.height)
setSquares(generateSquares(numSquares))
}, [dimensions, numSquares])
// Update squares to animate in
useEffect(() => {
if (dimensions.width && dimensions.height)
setSquares(generateSquares(numSquares));
}, [dimensions, numSquares]);
// Resize observer to update container dimensions
useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
setDimensions({
width: entry.contentRect.width,
height: entry.contentRect.height,
})
}
})
// Resize observer to update container dimensions
useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
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])
return () => {
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',
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#${id})`} />
<svg x={x} y={y} className="overflow-visible">
{squares.map(({ pos: [x, y], id }, index) => (
<motion.rect
initial={{ opacity: 0 }}
animate={{ opacity: maxOpacity }}
transition={{
duration,
repeat: 1,
delay: index * 0.1,
repeatType: 'reverse',
}}
onAnimationComplete={() => updateSquarePosition(id)}
key={`${x}-${y}-${index}`}
width={width - 1}
height={height - 1}
x={x * width + 1}
y={y * height + 1}
fill="currentColor"
strokeWidth="0"
/>
))}
</svg>
</svg>
)
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",
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#${id})`} />
<svg x={x} y={y} className="overflow-visible">
{squares.map(({ pos: [x, y], id }, index) => (
<motion.rect
initial={{ opacity: 0 }}
animate={{ opacity: maxOpacity }}
transition={{
duration,
repeat: 1,
delay: index * 0.1,
repeatType: "reverse",
}}
onAnimationComplete={() => updateSquarePosition(id)}
key={`${x}-${y}-${index}`}
width={width - 1}
height={height - 1}
x={x * width + 1}
y={y * height + 1}
fill="currentColor"
strokeWidth="0"
/>
))}
</svg>
</svg>
);
}
export default GridPattern
export default GridPattern;

View File

@@ -1,39 +1,39 @@
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> = ({
children,
className,
shimmerWidth = 100,
children,
className,
shimmerWidth = 100,
}) => {
return (
<p
style={
{
'--shimmer-width': `${shimmerWidth}px`,
} as CSSProperties
}
className={ny(
'mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50 ',
return (
<p
style={
{
"--shimmer-width": `${shimmerWidth}px`,
} as CSSProperties
}
className={ny(
"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]',
// 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]",
// Shimmer gradient
'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',
// Shimmer gradient
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
className,
)}
>
{children}
</p>
)
}
className,
)}
>
{children}
</p>
);
};
export default AnimatedShinyText
export default AnimatedShinyText;

View File

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

View File

@@ -1,37 +1,37 @@
'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
variant?: {
hidden: { filter: string, opacity: number }
visible: { filter: string, opacity: number }
}
duration?: number
word: string;
className?: string;
variant?: {
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
const defaultVariants = {
hidden: { filter: "blur(10px)", opacity: 0 },
visible: { filter: "blur(0px)", opacity: 1 },
};
const combinedVariants = variant || defaultVariants;
return (
<motion.h1
initial="hidden"
animate="visible"
transition={{ duration }}
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]',
)}
>
{word}
</motion.h1>
)
return (
<motion.h1
initial="hidden"
animate="visible"
transition={{ duration }}
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]",
)}
>
{word}
</motion.h1>
);
}
export default BlurIn
export default BlurIn;

View File

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

View File

@@ -5,52 +5,52 @@ import { type VariantProps, cva } from "class-variance-authority";
import { ny } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground ",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-12 px-8 py-4",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
"inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground ",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-12 px-8 py-4",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={ny(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={ny(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";

View File

@@ -1,30 +1,30 @@
'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>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<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',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={ny('flex items-center justify-center text-current')}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
<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",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={ny("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox }
export { Checkbox };

View File

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

View File

@@ -1,239 +1,234 @@
'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)
container.setAttribute(
'style',
'overflow:hidden; position:fixed; height:100%; top:0; left:0; right:0; bottom:0; pointer-events:none; z-index:2147483647',
)
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",
);
document.body.appendChild(container)
document.body.appendChild(container);
return container
return container;
}
let instanceCounter = 0
let instanceCounter = 0;
function applyParticleEffect(
element: HTMLElement,
options?: CoolParticleOptions,
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
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 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())
circle.setAttributeNS(
null,
'fill',
`hsl(${Math.random() * 360}, 70%, 50%)`,
)
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",
`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,
element: particle,
left,
size,
speedHorz,
speedUp,
spinSpeed,
spinVal,
top,
})
}
particles.push({
direction,
element: particle,
left,
size,
speedHorz,
speedUp,
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
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;
if (
p.top
>= Math.max(window.innerHeight, document.body.clientHeight) + p.size
) {
particles = particles.filter(o => o !== p)
p.element.remove()
}
if (
p.top >=
Math.max(window.innerHeight, document.body.clientHeight) + p.size
) {
particles = particles.filter((o) => o !== p);
p.element.remove();
}
p.element.setAttribute(
'style',
[
'position:absolute',
'will-change:transform',
`top:${p.top}px`,
`left:${p.left}px`,
`transform:rotate(${p.spinVal}deg)`,
].join(';'),
)
})
}
p.element.setAttribute(
"style",
[
"position:absolute",
"will-change:transform",
`top:${p.top}px`,
`left:${p.left}px`,
`transform:rotate(${p.spinVal}deg)`,
].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()
if (
autoAddParticle
&& particles.length < limit
&& currentTime - lastParticleTimestamp > particleGenerationDelay
) {
generateParticle()
lastParticleTimestamp = currentTime
}
function loop() {
const currentTime = performance.now();
if (
autoAddParticle &&
particles.length < limit &&
currentTime - lastParticleTimestamp > particleGenerationDelay
) {
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
}
}
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;
}
};
const tapHandler = (e: MouseEvent | TouchEvent) => {
updateMousePosition(e)
autoAddParticle = true
}
const tapHandler = (e: MouseEvent | TouchEvent) => {
updateMousePosition(e);
autoAddParticle = true;
};
const disableAutoAddParticle = () => {
autoAddParticle = false
}
const disableAutoAddParticle = () => {
autoAddParticle = false;
};
element.addEventListener(move, updateMousePosition, { passive: true })
element.addEventListener(tap, tapHandler, { passive: true })
element.addEventListener(tapEnd, disableAutoAddParticle, { passive: true })
element.addEventListener('mouseleave', disableAutoAddParticle, {
passive: true,
})
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)
return () => {
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)
const interval = setInterval(() => {
if (animationFrame && particles.length === 0) {
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])
useEffect(() => {
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 });
};

View File

@@ -1,126 +1,126 @@
'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>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<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',
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
<DialogPrimitive.Overlay
ref={ref}
className={ny(
"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;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<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',
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">
<Cross2Icon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={ny(
"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="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;
function DialogHeader({
className,
...props
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={ny(
'flex flex-col space-y-1.5 text-center sm:text-left',
className,
)}
{...props}
/>
)
return (
<div
className={ny(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
}
DialogHeader.displayName = 'DialogHeader'
DialogHeader.displayName = "DialogHeader";
function DialogFooter({
className,
...props
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={ny(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className,
)}
{...props}
/>
)
return (
<div
className={ny(
"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>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={ny(
'text-lg font-semibold leading-none tracking-tight',
className,
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
<DialogPrimitive.Title
ref={ref}
className={ny(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={ny('text-muted-foreground text-sm', className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
<DialogPrimitive.Description
ref={ref}
className={ny("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@@ -1,55 +1,55 @@
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,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
className,
...props
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
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',
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
patternContentUnits="userSpaceOnUse"
x={x}
y={y}
>
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
</svg>
)
return (
<svg
aria-hidden="true"
className={ny(
"pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80",
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
patternContentUnits="userSpaceOnUse"
x={x}
y={y}
>
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
</svg>
);
}
export default DotPattern
export default DotPattern;

View File

@@ -1,205 +1,205 @@
'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'
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} 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
}
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
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',
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName
= DropdownMenuPrimitive.SubTrigger.displayName
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={ny(
"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}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<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',
className,
)}
{...props}
/>
))
DropdownMenuSubContent.displayName
= DropdownMenuPrimitive.SubContent.displayName
<DropdownMenuPrimitive.SubContent
ref={ref}
className={ny(
"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;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
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',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={ny(
"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;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
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',
className,
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
<DropdownMenuPrimitive.Item
ref={ref}
className={ny(
"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;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<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',
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName
= DropdownMenuPrimitive.CheckboxItem.displayName
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={ny(
"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}
{...props}
>
<span className="absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<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',
className,
)}
{...props}
>
<span className="absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="size-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={ny(
"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}
>
<span className="absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="size-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={ny(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className,
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
<DropdownMenuPrimitive.Label
ref={ref}
className={ny(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={ny('bg-muted -mx-1 my-1 h-px', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
<DropdownMenuPrimitive.Separator
ref={ref}
className={ny("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
function DropdownMenuShortcut({
className,
...props
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) {
return (
<span
className={ny('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
)
return (
<span
className={ny("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
}
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View File

@@ -1,60 +1,60 @@
'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',
className,
framerProps = {
hidden: { opacity: 0 },
show: { opacity: 1, transition: { type: 'spring' } },
},
text,
direction = "up",
className,
framerProps = {
hidden: { opacity: 0 },
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 directionOffset = useMemo(() => {
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 }
}
const FADE_ANIMATION_VARIANTS = useMemo(() => {
const { hidden, show, ...rest } = framerProps as {
[name: string]: { [name: string]: number; opacity: number };
};
return {
...rest,
hidden: {
...(hidden ?? {}),
opacity: hidden?.opacity ?? 0,
[axis]: hidden?.[axis] ?? directionOffset,
},
show: {
...(show ?? {}),
opacity: show?.opacity ?? 1,
[axis]: show?.[axis] ?? 0,
},
}
}, [directionOffset, axis, framerProps])
return {
...rest,
hidden: {
...(hidden ?? {}),
opacity: hidden?.opacity ?? 0,
[axis]: hidden?.[axis] ?? directionOffset,
},
show: {
...(show ?? {}),
opacity: show?.opacity ?? 1,
[axis]: show?.[axis] ?? 0,
},
};
}, [directionOffset, axis, framerProps]);
return (
<motion.div
initial="hidden"
animate="show"
viewport={{ once: true }}
variants={FADE_ANIMATION_VARIANTS}
>
<motion.span className={className}>{text}</motion.span>
</motion.div>
)
return (
<motion.div
initial="hidden"
animate="show"
viewport={{ once: true }}
variants={FADE_ANIMATION_VARIANTS}
>
<motion.span className={className}>{text}</motion.span>
</motion.div>
);
}

View File

@@ -1,189 +1,192 @@
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>,
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> {
name: TName
name: TName;
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
)
{} as FormFieldContextValue,
);
function FormField<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({ ...props }: ControllerProps<TFieldValues, TName>) {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
return (
<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>')
if (!fieldContext)
throw new Error("useFormField should be used within <FormField>");
const { id } = itemContext
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
}
interface FormItemContextValue {
id: string
id: string;
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
)
{} as FormItemContextValue,
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
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} />
</FormItemContext.Provider>
)
})
FormItem.displayName = 'FormItem'
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={ny("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
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)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = 'FormLabel'
return (
<Label
ref={ref}
className={ny(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId }
= useFormField()
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = 'FormControl'
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
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)}
{...props}
/>
)
})
FormDescription.displayName = 'FormDescription'
return (
<p
ref={ref}
id={formDescriptionId}
className={ny("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
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)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = 'FormMessage'
return (
<p
ref={ref}
id={formMessageId}
className={ny("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
const FormGlobalError = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
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)}
{...props}
>
{rootError.message}
</p>
)
})
FormGlobalError.displayName = 'FormGlobalError'
return (
<p
ref={ref}
className={ny("text-sm font-medium text-destructive", className)}
{...props}
>
{rootError.message}
</p>
);
});
FormGlobalError.displayName = "FormGlobalError";
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormGlobalError,
FormField,
}
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormGlobalError,
FormField,
};

View File

@@ -1,44 +1,44 @@
'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({
text,
duration = 0.5,
delayMultiple = 0.04,
framerProps = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
},
className,
text,
duration = 0.5,
delayMultiple = 0.04,
framerProps = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
},
className,
}: GradualSpacingProps) {
return (
<div className="flex justify-center space-x-1">
<AnimatePresence>
{text.split('').map((char, i) => (
<motion.h1
key={i}
initial="hidden"
animate="visible"
exit="hidden"
variants={framerProps}
transition={{ duration, delay: i * delayMultiple }}
className={ny('drop-shadow-sm ', className)}
>
{char === ' ' ? <span>&nbsp;</span> : char}
</motion.h1>
))}
</AnimatePresence>
</div>
)
return (
<div className="flex justify-center space-x-1">
<AnimatePresence>
{text.split("").map((char, i) => (
<motion.h1
key={i}
initial="hidden"
animate="visible"
exit="hidden"
variants={framerProps}
transition={{ duration, delay: i * delayMultiple }}
className={ny("drop-shadow-sm", className)}
>
{char === " " ? <span>&nbsp;</span> : char}
</motion.h1>
))}
</AnimatePresence>
</div>
);
}

View File

@@ -1,71 +1,71 @@
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({
width = 40,
height = 40,
x = -1,
y = -1,
strokeDasharray = 0,
squares,
className,
...props
width = 40,
height = 40,
x = -1,
y = -1,
strokeDasharray = 0,
squares,
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',
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
{squares && (
<svg x={x} y={y} className="overflow-visible">
{squares.map(([x, y]) => (
<rect
strokeWidth="0"
key={`${x}-${y}`}
width={width - 1}
height={height - 1}
x={x * width + 1}
y={y * height + 1}
/>
))}
</svg>
)}
</svg>
)
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",
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
{squares && (
<svg x={x} y={y} className="overflow-visible">
{squares.map(([x, y]) => (
<rect
strokeWidth="0"
key={`${x}-${y}`}
width={width - 1}
height={height - 1}
x={x * width + 1}
y={y * height + 1}
/>
))}
</svg>
)}
</svg>
);
}
export default GridPattern
export default GridPattern;

View File

@@ -1,26 +1,26 @@
'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>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={ny(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
<LabelPrimitive.Root
ref={ref}
className={ny(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };

View File

@@ -1,128 +1,128 @@
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>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={ny(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
<NavigationMenuPrimitive.Root
ref={ref}
className={ny(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={ny(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
<NavigationMenuPrimitive.List
ref={ref}
className={ny(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props}
/>
));
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>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={ny(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
<NavigationMenuPrimitive.Trigger
ref={ref}
className={ny(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
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
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
<NavigationMenuPrimitive.Content
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,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={ny("absolute left-0 right-0 top-full flex justify-center")}>
<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
)}
ref={ref}
{...props}
/>
</div>
))
<div className={ny("absolute left-0 right-0 top-full flex justify-center")}>
<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,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
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
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
<NavigationMenuPrimitive.Indicator
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,
)}
{...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,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View File

@@ -1,144 +1,144 @@
'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 {
/**
* @default <div />
* @type ReactElement
* @description
* The component to be rendered as the card
*/
as?: ReactElement
/**
* @default ""
* @type string
* @description
* The className of the card
*/
className?: string
/**
* @default <div />
* @type ReactElement
* @description
* The component to be rendered as the card
*/
as?: ReactElement;
/**
* @default ""
* @type string
* @description
* The className of the card
*/
className?: string;
/**
* @default ""
* @type ReactNode
* @description
* The children of the card
*/
children?: ReactNode
/**
* @default ""
* @type ReactNode
* @description
* The children of the card
*/
children?: ReactNode;
/**
* @default 5
* @type number
* @description
* The size of the border in pixels
*/
borderSize?: number
/**
* @default 5
* @type number
* @description
* The size of the border in pixels
*/
borderSize?: number;
/**
* @default 20
* @type number
* @description
* The size of the radius in pixels
*/
borderRadius?: number
/**
* @default 20
* @type number
* @description
* The size of the radius in pixels
*/
borderRadius?: number;
/**
* @default "{ firstColor: '#ff00aa', secondColor: '#00FFF1' }"
* @type string
* @description
* The colors of the neon gradient
*/
neonColors?: NeonColorsProps
/**
* @default "{ firstColor: '#ff00aa', secondColor: '#00FFF1' }"
* @type string
* @description
* The colors of the neon gradient
*/
neonColors?: NeonColorsProps;
[key: string]: any
[key: string]: any;
}
const NeonGradientCard: React.FC<NeonGradientCardProps> = ({
className,
children,
borderSize = 2,
borderRadius = 20,
neonColors = {
firstColor: '#ff00aa',
secondColor: '#00FFF1',
},
...props
className,
children,
borderSize = 2,
borderRadius = 20,
neonColors = {
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 })
}
}
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
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)
}
}, [])
return () => {
window.removeEventListener("resize", updateDimensions);
};
}, []);
useEffect(() => {
if (containerRef.current) {
const { offsetWidth, offsetHeight } = containerRef.current
setDimensions({ width: offsetWidth, height: offsetHeight })
}
}, [children])
useEffect(() => {
if (containerRef.current) {
const { offsetWidth, offsetHeight } = containerRef.current;
setDimensions({ width: offsetWidth, height: offsetHeight });
}
}, [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`,
} as CSSProperties
}
className={ny(
'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',
)}
>
{children}
</div>
</div>
)
}
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`,
} as CSSProperties
}
className={ny(
"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",
)}
>
{children}
</div>
</div>
);
};
export { NeonGradientCard }
export { NeonGradientCard };

View File

@@ -1,57 +1,57 @@
import { ny } from '@/lib/utils'
import { ny } from "@/lib/utils";
export default function OrbitingCircles({
className,
children,
reverse,
duration = 20,
delay = 10,
radius = 50,
path = true,
className,
children,
reverse,
duration = 20,
delay = 10,
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 (
<>
{path && (
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
className="pointer-events-none absolute inset-0 size-full"
>
<circle
className="stroke-black/10 stroke-1 dark:stroke-white/10"
cx="50%"
cy="50%"
r={radius}
fill="none"
strokeDasharray="4 4"
/>
</svg>
)}
return (
<>
{path && (
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
className="pointer-events-none absolute inset-0 size-full"
>
<circle
className="stroke-black/10 stroke-1 dark:stroke-white/10"
cx="50%"
cy="50%"
r={radius}
fill="none"
strokeDasharray="4 4"
/>
</svg>
)}
<div
style={
{
'--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 },
className,
)}
>
{children}
</div>
</>
)
<div
style={
{
"--duration": duration,
"--radius": radius,
"--delay": -delay,
} as React.CSSProperties
}
className={ny(
"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>
</>
);
}

View File

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

View File

@@ -1,78 +1,76 @@
'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> = ({
text,
pulseColor,
backgroundColor,
textColor,
animationDuration,
buttonWidth,
buttonHeight,
text,
pulseColor,
backgroundColor,
textColor,
animationDuration,
buttonWidth,
buttonHeight,
}) => {
const pulseKeyframes = {
'--tw-pulse-color': pulseColor,
'animation': `pulse ${animationDuration} linear infinite`,
}
const pulseKeyframes = {
"--tw-pulse-color": pulseColor,
animation: `pulse ${animationDuration} linear infinite`,
};
return (
<div
className="flex justify-center items-center"
>
<button
className="relative block text-center cursor-pointer flex justify-center items-center"
style={{
color: textColor,
backgroundColor,
width: buttonWidth,
height: buttonHeight,
borderRadius: '12px',
...pulseKeyframes,
}}
>
<div>{text}</div>
<style jsx>
{`
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(var(--tw-pulse-color), 0);
}
50% {
box-shadow: 0 0 0 8px rgba(var(--tw-pulse-color), 0.5);
}
100% {
box-shadow: 0 0 0 0 rgba(var(--tw-pulse-color), 0);
}
}
button::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
border-radius: 20px;
background: inherit;
animation: inherit;
transform: translate(-50%, -50%);
z-index: -1;
}
`}
</style>
</button>
</div>
)
}
return (
<div className="flex items-center justify-center">
<button
className="relative block flex cursor-pointer items-center justify-center text-center"
style={{
color: textColor,
backgroundColor,
width: buttonWidth,
height: buttonHeight,
borderRadius: "12px",
...pulseKeyframes,
}}
>
<div>{text}</div>
<style jsx>
{`
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(var(--tw-pulse-color), 0);
}
50% {
box-shadow: 0 0 0 8px rgba(var(--tw-pulse-color), 0.5);
}
100% {
box-shadow: 0 0 0 0 rgba(var(--tw-pulse-color), 0);
}
}
button::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
border-radius: 20px;
background: inherit;
animation: inherit;
transform: translate(-50%, -50%);
z-index: -1;
}
`}
</style>
</button>
</div>
);
};
export default PulsatingButton
export default PulsatingButton;

View File

@@ -1,77 +1,77 @@
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 {
/**
* The type of radial gradient
* @default circle
* @type string
*/
type?: Type
/**
* The color to transition from
* @default #00000000
* @type string
*/
from?: string
/**
* The type of radial gradient
* @default circle
* @type string
*/
type?: Type;
/**
* The color to transition from
* @default #00000000
* @type string
*/
from?: string;
/**
* The color to transition to
* @default #290A5C
* @type string
*/
to?: string
/**
* The color to transition to
* @default #290A5C
* @type string
*/
to?: string;
/**
* The size of the gradient in pixels
* @default 300
* @type number
*/
size?: number
/**
* The size of the gradient in pixels
* @default 300
* @type number
*/
size?: number;
/**
* The origin of the gradient
* @default center
* @type string
*/
origin?: Origin
/**
* The origin of the gradient
* @default center
* @type string
*/
origin?: Origin;
/**
* The class name to apply to the gradient
* @default ""
* @type string
*/
className?: string
/**
* The class name to apply to the gradient
* @default ""
* @type string
*/
className?: string;
}
function RadialGradient({
type = 'circle',
from = 'rgba(120,119,198,0.3)',
to = 'hsla(0, 0%, 0%, 0)',
size = 300,
origin = 'center',
className,
type = "circle",
from = "rgba(120,119,198,0.3)",
to = "hsla(0, 0%, 0%, 0)",
size = 300,
origin = "center",
className,
}: RadialProps) {
const styles: CSSProperties = {
position: 'absolute',
pointerEvents: 'none',
inset: 0,
backgroundImage: `radial-gradient(${type} ${size}px at ${origin}, ${from}, ${to})`,
}
const styles: CSSProperties = {
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;

View File

@@ -1,32 +1,32 @@
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]',
className,
)}
>
{/* Grid */}
<div className="absolute inset-0 [transform:rotateX(35deg)]">
<div
className={ny(
'animate-grid',
return (
<div
className={ny(
"pointer-events-none absolute h-full w-full overflow-hidden opacity-50 [perspective:200px]",
className,
)}
>
{/* Grid */}
<div className="absolute inset-0 [transform:rotateX(35deg)]">
<div
className={ny(
"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)]',
// 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)]",
// 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)]',
)}
/>
</div>
// 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)]",
)}
/>
</div>
{/* Background Gradient */}
<div className="absolute inset-0 bg-gradient-to-t from-white to-transparent to-90% dark:from-black" />
</div>
)
{/* Background Gradient */}
<div className="absolute inset-0 bg-gradient-to-t from-white to-transparent to-90% dark:from-black" />
</div>
);
}

View File

@@ -1,48 +1,48 @@
'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>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={ny('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
<ScrollAreaPrimitive.Root
ref={ref}
className={ny("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ 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',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="bg-border relative flex-1 rounded-full" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ 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",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar }
export { ScrollArea, ScrollBar };

View File

@@ -1,168 +1,167 @@
'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'
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} 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>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<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',
className,
)}
onPointerDown={(e) => {
if (e.pointerType === 'touch')
e.preventDefault()
}}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 shrink-0 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
<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 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();
}}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 shrink-0 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={ny(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
<SelectPrimitive.ScrollUpButton
ref={ref}
className={ny(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={ny(
'flex cursor-default items-center justify-center py-1',
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName
= SelectPrimitive.ScrollDownButton.displayName
<SelectPrimitive.ScrollDownButton
ref={ref}
className={ny(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
));
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) => (
<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',
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={ny(
'p-1',
position === 'popper'
&& 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ 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",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={ny(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={ny('px-2 py-1.5 text-sm font-semibold', className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
<SelectPrimitive.Label
ref={ref}
className={ny("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<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',
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
<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",
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={ny('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
<SelectPrimitive.Separator
ref={ref}
className={ny("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View File

@@ -1,144 +1,144 @@
'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>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ 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',
className,
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
<SheetPrimitive.Overlay
className={ny(
"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;
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',
{
variants: {
side: {
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',
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',
},
},
defaultVariants: {
side: 'right',
},
},
)
"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",
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",
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",
},
},
defaultVariants: {
side: "right",
},
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
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">
<Cross2Icon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={ny(sheetVariants({ side }), className)}
{...props}
>
<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;
function SheetHeader({
className,
...props
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={ny(
'flex flex-col space-y-2 text-center sm:text-left',
className,
)}
{...props}
/>
)
return (
<div
className={ny(
"flex flex-col space-y-2 text-center sm:text-left",
className,
)}
{...props}
/>
);
}
SheetHeader.displayName = 'SheetHeader'
SheetHeader.displayName = "SheetHeader";
function SheetFooter({
className,
...props
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={ny(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className,
)}
{...props}
/>
)
return (
<div
className={ny(
"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>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={ny('text-foreground text-lg font-semibold', className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
<SheetPrimitive.Title
ref={ref}
className={ny("text-lg font-semibold text-foreground", className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={ny('text-muted-foreground text-sm', className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
<SheetPrimitive.Description
ref={ref}
className={ny("text-sm text-muted-foreground", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@@ -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;
}
/**
@@ -23,44 +23,43 @@ interface ShineBorderProps {
* @param children contains react node elements.
*/
export default function ShineBorder({
borderRadius = 8,
borderWidth = 1,
duration = 14,
color = '#fff',
className,
children,
borderRadius = 8,
borderWidth = 1,
duration = 14,
color = "#fff",
className,
children,
}: ShineBorderProps) {
return (
<div
style={
{
'--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',
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(',')
},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>
<div className="z-[1] h-full w-full rounded-[--border-radius-child]">
{children}
</div>
</div>
)
return (
<div
style={
{
"--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",
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(",")
},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:[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>
);
}

View File

@@ -1,52 +1,51 @@
'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 },
whileTap: { scale: 0.95 },
transition: {
repeat: Infinity,
repeatType: 'loop',
repeatDelay: 1,
type: 'spring',
stiffness: 20,
damping: 15,
mass: 2,
scale: {
type: 'spring',
stiffness: 200,
damping: 5,
mass: 0.5,
},
},
} as AnimationProps
initial: { "--x": "100%", scale: 0.8 },
animate: { "--x": "-100%", scale: 1 },
whileTap: { scale: 0.95 },
transition: {
repeat: Infinity,
repeatType: "loop",
repeatDelay: 1,
type: "spring",
stiffness: 20,
damping: 15,
mass: 2,
scale: {
type: "spring",
stiffness: 200,
damping: 5,
mass: 0.5,
},
},
} as AnimationProps;
function ShinyButton({ text = 'shiny-button' }) {
return (
<motion.button
{...animationProps}
className="relative rounded-lg px-6 py-2 font-medium backdrop-blur-xl transition-[box-shadow] duration-300 ease-in-out hover:shadow dark:bg-[radial-gradient(circle_at_50%_0%,hsl(var(--primary)/10%)_0%,transparent_60%)] dark:hover:shadow-[0_0_20px_hsl(var(--primary)/10%)]"
>
<span
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%))',
}}
>
{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',
}}
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>
</motion.button>
)
function ShinyButton({ text = "shiny-button" }) {
return (
<motion.button
{...animationProps}
className="relative rounded-lg px-6 py-2 font-medium backdrop-blur-xl transition-[box-shadow] duration-300 ease-in-out hover:shadow dark:bg-[radial-gradient(circle_at_50%_0%,hsl(var(--primary)/10%)_0%,transparent_60%)] dark:hover:shadow-[0_0_20px_hsl(var(--primary)/10%)]"
>
<span
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%))",
}}
>
{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",
}}
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>
</motion.button>
);
}
export default ShinyButton
export default ShinyButton;

View File

@@ -1,136 +1,189 @@
'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)
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,
);
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])
React.useEffect(() => {
if (!isEqual(value, localValues))
setLocalValues(
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
);
}, [min, max, value]);
const handleValueChange = (newValues: number[]) => {
setLocalValues(newValues)
if (onValueChange)
onValueChange(newValues)
}
const handleValueChange = (newValues: number[]) => {
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
}
for (let i = 0; i < array1.length; i++) {
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',
className,
)}
min={min}
max={max}
step={step}
value={localValues}
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.Range className={ny(
'bg-primary absolute',
orientation === 'horizontal' ? 'h-full' : 'w-full',
)}
/>
{showSteps !== undefined && showSteps !== 'none' && stepLines.map((value, index) => {
if (value === min || value === max)
return null
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",
className,
)}
min={min}
max={max}
step={step}
value={localValues}
onValueChange={(value) => handleValueChange(value)}
{...props}
onFocus={() => setHoveredThumbIndex(true)}
onBlur={() => setHoveredThumbIndex(false)}
>
<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(
"absolute bg-primary",
orientation === "horizontal" ? "h-full" : "w-full",
)}
/>
{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
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',
},
)}
style={{
[orientation === 'vertical' ? 'bottom' : 'left']: `${adjustedPosition}%`,
}}
/>
)
})}
const positionPercentage = ((value - min) / (max - min)) * 100;
const adjustedPosition = 50 + (positionPercentage - 50) * 0.96;
return (
<div
key={index}
className={ny(
{
"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}%`,
}}
/>
);
})}
</SliderPrimitive.Track>
{localValues.map((numberStep, index) => (
<SliderPrimitive.Thumb
key={index}
className={ny(
"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",
},
{
"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)}
</div>
)}
</SliderPrimitive.Thumb>
))}
</SliderPrimitive.Root>
);
},
);
</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',
)}
>
{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',
)}
>
{formatLabel(numberStep)}
</div>
)}
</SliderPrimitive.Thumb>
))}
</SliderPrimitive.Root>
)
})
Slider.displayName = SliderPrimitive.Root.displayName;
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }
export { Slider };

View File

@@ -1,152 +1,151 @@
'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 {
/**
* @default <div />
* @type ReactElement
* @description
* The component to be rendered as the text
*/
as?: ReactElement
/**
* @default <div />
* @type ReactElement
* @description
* The component to be rendered as the text
*/
as?: ReactElement;
/**
* @default ""
* @type string
* @description
* The className of the text
*/
className?: string
/**
* @default ""
* @type string
* @description
* The className of the text
*/
className?: string;
/**
* @required
* @type string
* @description
* The text to be displayed
*/
text: string
/**
* @required
* @type string
* @description
* The text to be displayed
*/
text: string;
/**
* @default 10
* @type number
* @description
* The count of sparkles
*/
sparklesCount?: number
/**
* @default 10
* @type number
* @description
* The count of sparkles
*/
sparklesCount?: number;
/**
* @default "{first: '#A07CFE', second: '#FE8FB5'}"
* @type string
* @description
* The colors of the sparkles
*/
colors?: {
first: string
second: string
}
/**
* @default "{first: '#A07CFE', second: '#FE8FB5'}"
* @type string
* @description
* The colors of the sparkles
*/
colors?: {
first: string;
second: string;
};
}
const SparklesText: React.FC<SparklesTextProps> = ({
text,
colors = { first: '#A07CFE', second: '#FE8FB5' },
className,
sparklesCount = 10,
...props
text,
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 }
}
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 initializeStars = () => {
const newSparkles = Array.from({ length: sparklesCount }, generateStar)
setSparkles(newSparkles)
}
const initializeStars = () => {
const newSparkles = Array.from({ length: sparklesCount }, generateStar);
setSparkles(newSparkles);
};
const updateStars = () => {
setSparkles(currentSparkles =>
currentSparkles.map((star) => {
if (star.lifespan <= 0)
return generateStar()
else return { ...star, lifespan: star.lifespan - 0.1 }
}),
)
}
const updateStars = () => {
setSparkles((currentSparkles) =>
currentSparkles.map((star) => {
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)}
{...props}
style={
{
'--sparkles-first-color': `${colors.first}`,
'--sparkles-second-color': `${colors.second}`,
} as CSSProperties
}
>
<span className="relative inline-block">
{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">
{text}
</strong>
</span>
</div>
)
}
return (
<div
className={ny("text-6xl font-bold", className)}
{...props}
style={
{
"--sparkles-first-color": `${colors.first}`,
"--sparkles-second-color": `${colors.second}`,
} as CSSProperties
}
>
<span className="relative inline-block">
{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">
{text}
</strong>
</span>
</div>
);
};
const Sparkle: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {
return (
<motion.svg
key={id}
className="pointer-events-none absolute z-20"
initial={{ opacity: 0, left: x, top: y }}
animate={{
opacity: [0, 1, 0],
scale: [0, scale, 0],
rotate: [75, 120, 150],
}}
transition={{ duration: 0.8, repeat: Infinity, delay }}
width="21"
height="21"
viewBox="0 0 21 21"
>
<path
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
fill={color}
/>
</motion.svg>
)
}
return (
<motion.svg
key={id}
className="pointer-events-none absolute z-20"
initial={{ opacity: 0, left: x, top: y }}
animate={{
opacity: [0, 1, 0],
scale: [0, scale, 0],
rotate: [75, 120, 150],
}}
transition={{ duration: 0.8, repeat: Infinity, delay }}
width="21"
height="21"
viewBox="0 0 21 21"
>
<path
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
fill={color}
/>
</motion.svg>
);
};
export default SparklesText
export default SparklesText;

View File

@@ -1,120 +1,120 @@
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,
React.HTMLAttributes<HTMLTableElement>
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={ny('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
))
Table.displayName = 'Table'
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={ny("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
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,
React.HTMLAttributes<HTMLTableSectionElement>
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={ny('[&_tr:last-child]:border-0', className)}
{...props}
/>
))
TableBody.displayName = 'TableBody'
<tbody
ref={ref}
className={ny("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={ny(
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
className,
)}
{...props}
/>
))
TableFooter.displayName = 'TableFooter'
<tfoot
ref={ref}
className={ny(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={ny(
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
className,
)}
{...props}
/>
))
TableRow.displayName = 'TableRow'
<tr
ref={ref}
className={ny(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<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]',
className,
)}
{...props}
/>
))
TableHead.displayName = 'TableHead'
<th
ref={ref}
className={ny(
"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";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={ny(
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
)}
{...props}
/>
))
TableCell.displayName = 'TableCell'
<td
ref={ref}
className={ny(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={ny('text-muted-foreground mt-4 text-sm', className)}
{...props}
/>
))
TableCaption.displayName = 'TableCaption'
<caption
ref={ref}
className={ny("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@@ -1,55 +1,55 @@
'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>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={ny(
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
<TabsPrimitive.List
ref={ref}
className={ny(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<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',
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
<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",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<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',
className,
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
<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",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -1,64 +1,64 @@
'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,
text,
className,
}) => {
const targetRef = useRef<HTMLDivElement | null>(null)
const targetRef = useRef<HTMLDivElement | null>(null);
const { scrollYProgress } = useScroll({
target: targetRef,
})
const words = text.split(' ')
const { scrollYProgress } = useScroll({
target: targetRef,
});
const words = text.split(" ");
return (
<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
return (
<Word key={i} progress={scrollYProgress} range={[start, end]}>
{word}
</Word>
)
})}
</p>
</div>
</div>
)
}
return (
<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;
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])
return (
<span className="xl:lg-3 relative mx-1 lg:mx-2.5">
<span className="absolute opacity-30">{children}</span>
<motion.span style={{ opacity }} className="text-black dark:text-white">
{children}
</motion.span>
</span>
)
}
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>
<motion.span style={{ opacity }} className="text-black dark:text-white">
{children}
</motion.span>
</span>
);
};
export default TextRevealByWord
export default TextRevealByWord;

View File

@@ -1,46 +1,45 @@
'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({
text,
duration = 200,
className,
text,
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)
}
else {
clearInterval(typingEffect)
}
}, duration)
useEffect(() => {
const typingEffect = setInterval(() => {
if (i < text.length) {
setDisplayedText(text.substring(0, i + 1));
setI(i + 1);
} else {
clearInterval(typingEffect);
}
}, duration);
return () => {
clearInterval(typingEffect)
}
}, [duration, i])
return () => {
clearInterval(typingEffect);
};
}, [duration, i]);
return (
<h1
className={ny(
'font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm',
className,
)}
>
{displayedText || text}
</h1>
)
return (
<h1
className={ny(
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
className,
)}
>
{displayedText || text}
</h1>
);
}

View File

@@ -1,58 +1,58 @@
'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
variant?: {
hidden: { y: number }
visible: { y: number }
}
duration?: number
delay?: number
word: string;
className?: string;
variant?: {
hidden: { y: number };
visible: { y: number };
};
duration?: number;
delay?: number;
}
function WavyText({
word,
className,
variant,
duration = 0.5,
delay = 0.05,
word,
className,
variant,
duration = 0.5,
delay = 0.05,
}: WavyTextProps) {
const defaultVariants = {
hidden: { y: 10 },
visible: { y: -10 },
}
const combinedVariants = variant || defaultVariants
const characters = useMemo(() => word.split(''), [word])
return (
<div className="flex justify-center space-x-2 overflow-hidden p-3">
<AnimatePresence>
{characters.map((char, i) => (
<motion.h1
key={i}
initial="hidden"
animate="visible"
exit="hidden"
variants={combinedVariants}
transition={{
yoyo: Infinity,
duration,
delay: i * delay,
}}
className={ny(
className,
'font-display text-center text-4xl font-bold tracking-[-0.15em] md:text-7xl',
)}
>
{char}
</motion.h1>
))}
</AnimatePresence>
</div>
)
const defaultVariants = {
hidden: { y: 10 },
visible: { y: -10 },
};
const combinedVariants = variant || defaultVariants;
const characters = useMemo(() => word.split(""), [word]);
return (
<div className="flex justify-center space-x-2 overflow-hidden p-3">
<AnimatePresence>
{characters.map((char, i) => (
<motion.h1
key={i}
initial="hidden"
animate="visible"
exit="hidden"
variants={combinedVariants}
transition={{
yoyo: Infinity,
duration,
delay: i * delay,
}}
className={ny(
className,
"font-display text-center text-4xl font-bold tracking-[-0.15em] md:text-7xl",
)}
>
{char}
</motion.h1>
))}
</AnimatePresence>
</div>
);
}
export default WavyText
export default WavyText;

View File

@@ -1,53 +1,53 @@
'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({
words,
wrapperFramerProps = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.2,
},
},
},
framerProps = {
hidden: { y: 20, opacity: 0 },
show: { y: 0, opacity: 1 },
},
className,
words,
wrapperFramerProps = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.2,
},
},
},
framerProps = {
hidden: { y: 20, opacity: 0 },
show: { y: 0, opacity: 1 },
},
className,
}: WordPullUpProps) {
return (
<motion.h1
variants={wrapperFramerProps}
initial="hidden"
animate="show"
className={ny(
'font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm',
className,
)}
>
{words.split(' ').map((word, i) => (
<motion.span
key={i}
variants={framerProps}
style={{ display: 'inline-block', paddingRight: '8px' }}
>
{word === '' ? <span>&nbsp;</span> : word}
</motion.span>
))}
</motion.h1>
)
return (
<motion.h1
variants={wrapperFramerProps}
initial="hidden"
animate="show"
className={ny(
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
className,
)}
>
{words.split(" ").map((word, i) => (
<motion.span
key={i}
variants={framerProps}
style={{ display: "inline-block", paddingRight: "8px" }}
>
{word === "" ? <span>&nbsp;</span> : word}
</motion.span>
))}
</motion.h1>
);
}

View File

@@ -3,10 +3,18 @@
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>
);
return (
<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>
);
}

View File

@@ -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",
];

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,46 @@
export const releases: any = {
WindowsInstaller: "zen.installer.exe",
WindowsInstallerGeneric: "zen.installer-generic.exe",
WindowsInstaller: "zen.installer.exe",
WindowsInstallerGeneric: "zen.installer-generic.exe",
WindowsZip: "zen.win-specific.zip",
WindowsZipGeneric: "zen.win-generic.zip",
WindowsZip: "zen.win-specific.zip",
WindowsZipGeneric: "zen.win-generic.zip",
MacOS: "zen.macos-aarch64.dmg",
MacOSIntel: "zen.macos-x64.dmg",
MacOS: "zen.macos-aarch64.dmg",
MacOSIntel: "zen.macos-x64.dmg",
Linux: "zen.linux-specific.tar.bz2",
LinuxGeneric: "zen.linux-generic.tar.bz2",
Linux: "zen.linux-specific.tar.bz2",
LinuxGeneric: "zen.linux-generic.tar.bz2",
LinuxAppImage: "zen-specific.AppImage",
LinuxAppImageGeneric: "zen-generic.AppImage",
LinuxAppImage: "zen-specific.AppImage",
LinuxAppImageGeneric: "zen-generic.AppImage",
};
// platform
// -> arch
// -> file
export const releaseTree: any = {
windows: {
specific: {
installer: "WindowsInstaller",
portable: "WindowsZip",
},
generic: {
installer: "WindowsInstallerGeneric",
portable: "WindowsZipGeneric",
},
},
macos: {
generic: "MacOSIntel",
specific: "MacOS",
},
linux: {
specific: {
portable: "Linux",
appimage: "LinuxAppImage",
},
generic: {
portable: "LinuxGeneric",
appimage: "LinuxAppImageGeneric",
},
},
windows: {
specific: {
installer: "WindowsInstaller",
portable: "WindowsZip",
},
generic: {
installer: "WindowsInstallerGeneric",
portable: "WindowsZipGeneric",
},
},
macos: {
generic: "MacOSIntel",
specific: "MacOS",
},
linux: {
specific: {
portable: "Linux",
appimage: "LinuxAppImage",
},
generic: {
portable: "LinuxGeneric",
appimage: "LinuxAppImageGeneric",
},
},
};

View File

@@ -5,25 +5,25 @@ import { useServerInsertedHTML } from "next/navigation";
import { ServerStyleSheet, StyleSheetManager } from "styled-components";
export default function StyledComponentsRegistry({
children,
children,
}: {
children: React.ReactNode;
children: React.ReactNode;
}) {
// Only create stylesheet once with lazy initial state
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
// Only create stylesheet once with lazy initial state
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement();
styledComponentsStyleSheet.instance.clearTag();
return <>{styles}</>;
});
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement();
styledComponentsStyleSheet.instance.clearTag();
return <>{styles}</>;
});
if (typeof window !== "undefined") return <>{children}</>;
if (typeof window !== "undefined") return <>{children}</>;
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
);
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
);
}

View File

@@ -1,55 +1,63 @@
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: {
revalidate: 60,
} } as RequestInit;
const CACHE_OPTIONS = {
next: {
revalidate: 60,
},
} as RequestInit;
export async function getAllThemes() {
// Fetch from the API
const response = await fetch(THEME_API, CACHE_OPTIONS);
const themes = await response.json();
// transform in to a ZenTheme[] as it is currently an object
let themesArray: ZenTheme[] = [];
for (let key in themes) {
themesArray.push(themes[key]);
}
return themesArray;
// Fetch from the API
const response = await fetch(THEME_API, CACHE_OPTIONS);
const themes = await response.json();
// transform in to a ZenTheme[] as it is currently an object
let themesArray: ZenTheme[] = [];
for (let key in themes) {
themesArray.push(themes[key]);
}
return themesArray;
}
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");
return filtered.filter((theme) => {
if (isSearchingForColorScheme && theme.isColorTheme) return true;
if (isSearchingForUtility && !theme.isColorTheme) return true;
return false;
});
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");
return filtered.filter((theme) => {
if (isSearchingForColorScheme && theme.isColorTheme) return true;
if (isSearchingForUtility && !theme.isColorTheme) return true;
return false;
});
}
export async function getThemeFromId(id: string) {
return (await getAllThemes()).find((theme) => theme.id === id);
return (await getAllThemes()).find((theme) => theme.id === id);
}
export async function getThemeMarkdown(theme: ZenTheme) {
return (await fetch(theme.readme, CACHE_OPTIONS)).text();
return (await fetch(theme.readme, CACHE_OPTIONS)).text();
}
export function getThemeAuthorLink(theme: ZenTheme): string {
return `https://github.com/${theme.author}`;
return `https://github.com/${theme.author}`;
}

View File

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

View File

@@ -1,154 +1,156 @@
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}',
],
prefix: "",
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
fontFamily: {
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))',
surface: "var(--surface)",
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
orbit: {
"0%": {
transform: "rotate(0deg) translateY(calc(var(--radius) * 1px)) rotate(0deg)",
},
"100%": {
transform: "rotate(360deg) translateY(calc(var(--radius) * 1px)) rotate(-360deg)",
},
},
"shine-pulse": {
"0%": {
"background-position": "0% 0%",
},
"50%": {
"background-position": "100% 100%",
},
to: {
"background-position": "0% 0%",
},
},
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
'border-beam': {
'100%': {
'offset-distance': '100%',
},
},
'image-glow': {
'0%': {
'opacity': '0',
'animation-timing-function': 'cubic-bezier(0.74, 0.25, 0.76, 1)',
},
'10%': {
'opacity': '0.7',
'animation-timing-function': 'cubic-bezier(0.12, 0.01, 0.08, 0.99)',
},
'100%': {
opacity: '0.4',
},
},
'fade-in': {
from: { opacity: '0', transform: 'translateY(-10px)' },
to: { opacity: '1', transform: 'none' },
},
'fade-up': {
from: { opacity: '0', transform: 'translateY(20px)' },
to: { opacity: '1', transform: 'none' },
},
'shimmer': {
'0%, 90%, 100%': {
'background-position': 'calc(-100% - var(--shimmer-width)) 0',
},
'30%, 60%': {
'background-position': 'calc(100% + var(--shimmer-width)) 0',
},
},
'marquee': {
from: { transform: 'translateX(0)' },
to: { transform: 'translateX(calc(-100% - var(--gap)))' },
},
'marquee-vertical': {
from: { transform: 'translateY(0)' },
to: { transform: 'translateY(calc(-100% - var(--gap)))' },
},
},
animation: {
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',
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
fontFamily: {
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))",
surface: "var(--surface)",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
orbit: {
"0%": {
transform:
"rotate(0deg) translateY(calc(var(--radius) * 1px)) rotate(0deg)",
},
"100%": {
transform:
"rotate(360deg) translateY(calc(var(--radius) * 1px)) rotate(-360deg)",
},
},
"shine-pulse": {
"0%": {
"background-position": "0% 0%",
},
"50%": {
"background-position": "100% 100%",
},
to: {
"background-position": "0% 0%",
},
},
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
"border-beam": {
"100%": {
"offset-distance": "100%",
},
},
"image-glow": {
"0%": {
opacity: "0",
"animation-timing-function": "cubic-bezier(0.74, 0.25, 0.76, 1)",
},
"10%": {
opacity: "0.7",
"animation-timing-function": "cubic-bezier(0.12, 0.01, 0.08, 0.99)",
},
"100%": {
opacity: "0.4",
},
},
"fade-in": {
from: { opacity: "0", transform: "translateY(-10px)" },
to: { opacity: "1", transform: "none" },
},
"fade-up": {
from: { opacity: "0", transform: "translateY(20px)" },
to: { opacity: "1", transform: "none" },
},
shimmer: {
"0%, 90%, 100%": {
"background-position": "calc(-100% - var(--shimmer-width)) 0",
},
"30%, 60%": {
"background-position": "calc(100% + var(--shimmer-width)) 0",
},
},
marquee: {
from: { transform: "translateX(0)" },
to: { transform: "translateX(calc(-100% - var(--gap)))" },
},
"marquee-vertical": {
from: { transform: "translateY(0)" },
to: { transform: "translateY(calc(-100% - var(--gap)))" },
},
},
animation: {
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",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;
export default config
export default config;

View File

@@ -1,26 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}