Refactor Prettier configuration to adhere to code style guidelines
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "next/core-web-vitals",
|
"extends": "next/core-web-vitals",
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/display-name": "off",
|
"react/display-name": "off",
|
||||||
"react/no-unescaped-entities": "off"
|
"react/no-unescaped-entities": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
.prettierrc
10
.prettierrc
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"plugins": ["prettier-plugin-tailwindcss"]
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
}
|
}
|
||||||
|
|||||||
12
.prettierrc.json
Normal file
12
.prettierrc.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 128,
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
@@ -1,45 +1,44 @@
|
|||||||
|
const { PHASE_DEVELOPMENT_SERVER } = require("next/constants");
|
||||||
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')
|
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = (phase, { defaultConfig }) => {
|
const nextConfig = (phase, { defaultConfig }) => {
|
||||||
const defaultConfigWWW = {
|
const defaultConfigWWW = {
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "raw.githubusercontent.com",
|
hostname: "raw.githubusercontent.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "cdn.jsdelivr.net",
|
hostname: "cdn.jsdelivr.net",
|
||||||
port: '',
|
port: "",
|
||||||
pathname: '/gh/zen-browser/**',
|
pathname: "/gh/zen-browser/**",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
domains: ['localhost', 'cdn.jsdelivr.net', "raw.githubusercontent.com"], // Allow images from jsDelivr
|
domains: ["localhost", "cdn.jsdelivr.net", "raw.githubusercontent.com"], // Allow images from jsDelivr
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: {
|
serverActions: {
|
||||||
// edit: updated to new key. Was previously `allowedForwardedHosts`
|
// edit: updated to new key. Was previously `allowedForwardedHosts`
|
||||||
allowedOrigins: ["localhost:3000", "get-zen.vercel.app"],
|
allowedOrigins: ["localhost:3000", "get-zen.vercel.app"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
compiler: {
|
compiler: {
|
||||||
styledComponents: true,
|
styledComponents: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (phase === PHASE_DEVELOPMENT_SERVER) {
|
if (phase === PHASE_DEVELOPMENT_SERVER) {
|
||||||
return {
|
return {
|
||||||
...defaultConfigWWW,
|
...defaultConfigWWW,
|
||||||
// development only config options here
|
// development only config options here
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...defaultConfigWWW,
|
...defaultConfigWWW,
|
||||||
// production only config options here
|
// production only config options here
|
||||||
output: 'export',
|
output: "export",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
|
|||||||
32
nyxbui.json
32
nyxbui.json
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://nyxbui.design/schema.json",
|
"$schema": "https://nyxbui.design/schema.json",
|
||||||
"style": "miami",
|
"style": "miami",
|
||||||
"rsc": true,
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.ts",
|
"config": "tailwind.config.ts",
|
||||||
"css": "src/app/globals.css",
|
"css": "src/app/globals.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils"
|
"utils": "@/lib/utils"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28797
package-lock.json
generated
28797
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
140
package.json
140
package.json
@@ -1,72 +1,72 @@
|
|||||||
{
|
{
|
||||||
"name": "zen-website",
|
"name": "zen-website",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"pages:build": "npx @cloudflare/next-on-pages",
|
"pages:build": "npx @cloudflare/next-on-pages",
|
||||||
"preview": "npm run pages:build && wrangler pages dev",
|
"preview": "npm run pages:build && wrangler pages dev",
|
||||||
"deploy": "npm run pages:build && wrangler pages deploy"
|
"deploy": "npm run pages:build && wrangler pages deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-slider": "^1.2.0",
|
"@radix-ui/react-slider": "^1.2.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@supabase/supabase-js": "^2.45.1",
|
"@supabase/supabase-js": "^2.45.1",
|
||||||
"@vercel/postgres": "^0.9.0",
|
"@vercel/postgres": "^0.9.0",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cobe": "^0.6.3",
|
"cobe": "^0.6.3",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"feed": "^4.2.2",
|
"feed": "^4.2.2",
|
||||||
"framer-motion": "^11.3.24",
|
"framer-motion": "^11.3.24",
|
||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"next": "14.2.4",
|
"next": "14.2.4",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.52.2",
|
"react-hook-form": "^7.52.2",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-spring": "^9.7.4",
|
"react-spring": "^9.7.4",
|
||||||
"react-sticky-box": "^2.0.5",
|
"react-sticky-box": "^2.0.5",
|
||||||
"react-sticky-el": "^2.1.0",
|
"react-sticky-el": "^2.1.0",
|
||||||
"styled-components": "^6.1.12",
|
"styled-components": "^6.1.12",
|
||||||
"tailwind-merge": "^2.5.1",
|
"tailwind-merge": "^2.5.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zen-website": "file:",
|
"zen-website": "file:",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/next-on-pages": "^1.13.2",
|
"@cloudflare/next-on-pages": "^1.13.2",
|
||||||
"@types/canvas-confetti": "^1.6.4",
|
"@types/canvas-confetti": "^1.6.4",
|
||||||
"@types/node": "^20.14.15",
|
"@types/node": "^20.14.15",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/sync-fetch": "^0.4.3",
|
"@types/sync-fetch": "^0.4.3",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-next": "14.2.4",
|
"eslint-config-next": "14.2.4",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default function WhyAreYouEvenHere() {
|
export default function WhyAreYouEvenHere() {
|
||||||
redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
|
redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ import Footer from "@/components/footer";
|
|||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
import { releaseNoteIsAlpha, releaseNotes } from "@/lib/release-notes";
|
import { releaseNoteIsAlpha, releaseNotes } from "@/lib/release-notes";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Markdown from 'react-markdown'
|
import Markdown from "react-markdown";
|
||||||
import '../privacy-policy/markdown.css';
|
import "../privacy-policy/markdown.css";
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||||
<div id="policy" className="min-h-screen py-42 flex mx-auto my-52 p-10 lg:p-0 w-full lg:w-1/3 flex-col">
|
<div
|
||||||
<Markdown>
|
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
|
# Main Developer Team
|
||||||
|
|
||||||
* [**Mauro Baladés**](https://github.com/mauro-balades): Creator, Main Developer, and a funny guy.
|
* [**Mauro Baladés**](https://github.com/mauro-balades): Creator, Main Developer, and a funny guy.
|
||||||
@@ -33,8 +36,8 @@ export default function PrivacyPolicy() {
|
|||||||
|
|
||||||

|

|
||||||
`}
|
`}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import Footer from "@/components/footer";
|
|||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
|
|
||||||
export default function BrandingAssetsPage() {
|
export default function BrandingAssetsPage() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||||
<BrandingAssets />
|
<BrandingAssets />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import Footer from "@/components/footer";
|
|||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
|
|
||||||
export default function BrandingAssetsPage() {
|
export default function BrandingAssetsPage() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||||
<CreateThemePage />
|
<CreateThemePage />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
|
|
||||||
import DownloadPage from "@/components/download";
|
import DownloadPage from "@/components/download";
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
|
|
||||||
export default function Download() {
|
export default function Download() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||||
<DownloadPage />
|
<DownloadPage />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,57 +14,56 @@ const RSS_ENTRY_LIMIT = 20;
|
|||||||
* @returns The RSS feed for the Zen Browser release notes.
|
* @returns The RSS feed for the Zen Browser release notes.
|
||||||
*/
|
*/
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
// Just in case the release notes array is empty for whatever reason.
|
// Just in case the release notes array is empty for whatever reason.
|
||||||
const latestDate = releaseNotes.length > 0
|
const latestDate =
|
||||||
? formatRssDate(releaseNotes[0].date)
|
releaseNotes.length > 0 ? formatRssDate(releaseNotes[0].date) : new Date();
|
||||||
: new Date();
|
|
||||||
|
|
||||||
const feed = new Feed({
|
const feed = new Feed({
|
||||||
id: "https://www.zen-browser.app/release-notes",
|
id: "https://www.zen-browser.app/release-notes",
|
||||||
link: "https://www.zen-browser.app/release-notes",
|
link: "https://www.zen-browser.app/release-notes",
|
||||||
title: "Zen Browser Release Notes",
|
title: "Zen Browser Release Notes",
|
||||||
description: "Release Notes for the Zen Browser",
|
description: "Release Notes for the Zen Browser",
|
||||||
language: "en",
|
language: "en",
|
||||||
favicon: "https://www.zen-browser.app/favicon.ico",
|
favicon: "https://www.zen-browser.app/favicon.ico",
|
||||||
copyright: `Zen Browser © ${new Date().getFullYear()} - Made with ❤️ by the Zen team.`,
|
copyright: `Zen Browser © ${new Date().getFullYear()} - Made with ❤️ by the Zen team.`,
|
||||||
updated: latestDate,
|
updated: latestDate,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const releaseNote of releaseNotes.slice(0, RSS_ENTRY_LIMIT)) {
|
for (const releaseNote of releaseNotes.slice(0, RSS_ENTRY_LIMIT)) {
|
||||||
feed.addItem({
|
feed.addItem({
|
||||||
title: `Release notes for version ${releaseNote.version}`,
|
title: `Release notes for version ${releaseNote.version}`,
|
||||||
id: `https://www.zen-browser.app/release-notes/${releaseNote.version}`,
|
id: `https://www.zen-browser.app/release-notes/${releaseNote.version}`,
|
||||||
link: `https://www.zen-browser.app/release-notes/${releaseNote.version}`,
|
link: `https://www.zen-browser.app/release-notes/${releaseNote.version}`,
|
||||||
date: formatRssDate(releaseNote.date),
|
date: formatRssDate(releaseNote.date),
|
||||||
description: releaseNote.extra,
|
description: releaseNote.extra,
|
||||||
content: formatReleaseNote(releaseNote),
|
content: formatReleaseNote(releaseNote),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(feed.rss2(), {
|
return new Response(feed.rss2(), {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/xml; charset=utf-8',
|
"Content-Type": "application/xml; charset=utf-8",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a date string in the format day/month/year.
|
* Formats a date string in the format day/month/year.
|
||||||
*
|
*
|
||||||
* Note: If release notes change to ISO format, this will need to be updated.
|
* Note: If release notes change to ISO format, this will need to be updated.
|
||||||
* @param dateStr The date string to format.
|
* @param dateStr The date string to format.
|
||||||
* @returns The passed in date string as a Date object.
|
* @returns The passed in date string as a Date object.
|
||||||
*/
|
*/
|
||||||
function formatRssDate(dateStr: string) {
|
function formatRssDate(dateStr: string) {
|
||||||
const splitDate = dateStr.split("/");
|
const splitDate = dateStr.split("/");
|
||||||
if (splitDate.length !== 3) {
|
if (splitDate.length !== 3) {
|
||||||
throw new Error("Invalid date format");
|
throw new Error("Invalid date format");
|
||||||
}
|
}
|
||||||
|
|
||||||
const day = Number(splitDate[0]);
|
const day = Number(splitDate[0]);
|
||||||
const month = Number(splitDate[1]) - 1;
|
const month = Number(splitDate[1]) - 1;
|
||||||
const year = Number(splitDate[2]);
|
const year = Number(splitDate[2]);
|
||||||
return new Date(year, month, day);
|
return new Date(year, month, day);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,38 +72,39 @@ function formatRssDate(dateStr: string) {
|
|||||||
* @returns The formatted release note as a HTML string.
|
* @returns The formatted release note as a HTML string.
|
||||||
*/
|
*/
|
||||||
function formatReleaseNote(releaseNote: ReleaseNote) {
|
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) {
|
if (releaseNote.extra) {
|
||||||
content += `<p>${releaseNote.extra.replace(/(\n)/g, "<br />")}</p>`
|
content += `<p>${releaseNote.extra.replace(/(\n)/g, "<br />")}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (releaseNote.breakingChanges) {
|
if (releaseNote.breakingChanges) {
|
||||||
content += `<h2>⚠️ Breaking changes</h2>`
|
content += `<h2>⚠️ Breaking changes</h2>`;
|
||||||
content += `<ul>`
|
content += `<ul>`;
|
||||||
for (const breakingChange of releaseNote.breakingChanges) {
|
for (const breakingChange of releaseNote.breakingChanges) {
|
||||||
content += `<li>${breakingChange}</li>`
|
content += `<li>${breakingChange}</li>`;
|
||||||
}
|
}
|
||||||
content += `</ul>`
|
content += `</ul>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (releaseNote.features) {
|
if (releaseNote.features) {
|
||||||
content += `<h2>⭐ Features</h2>`
|
content += `<h2>⭐ Features</h2>`;
|
||||||
content += `<ul>`
|
content += `<ul>`;
|
||||||
for (const feature of releaseNote.features) {
|
for (const feature of releaseNote.features) {
|
||||||
content += `<li>${feature}</li>`
|
content += `<li>${feature}</li>`;
|
||||||
}
|
}
|
||||||
content += `</ul>`
|
content += `</ul>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (releaseNote.fixes) {
|
if (releaseNote.fixes) {
|
||||||
content += `<h2>✓ Fixes</h2>`
|
content += `<h2>✓ Fixes</h2>`;
|
||||||
content += `<ul>`
|
content += `<ul>`;
|
||||||
for (const fix of releaseNote.fixes) {
|
for (const fix of releaseNote.fixes) {
|
||||||
content += `<li>${fix.description}</li>`
|
content += `<li>${fix.description}</li>`;
|
||||||
}
|
}
|
||||||
content += `</ul>`
|
content += `</ul>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,39 +9,43 @@ import { Navigation } from "@/components/navigation";
|
|||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Zen Browser",
|
title: "Zen Browser",
|
||||||
description: "Download now and experience the Zen Browser",
|
description: "Download now and experience the Zen Browser",
|
||||||
keywords: ["Zen", "Browser", "Zen Browser", "Web", "Internet", "Fast"],
|
keywords: ["Zen", "Browser", "Zen Browser", "Web", "Internet", "Fast"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
return (
|
||||||
return (
|
<html suppressHydrationWarning>
|
||||||
<html suppressHydrationWarning>
|
<head>
|
||||||
<head>
|
<link rel="me" href="https://fosstodon.org/@zenbrowser"></link>
|
||||||
<link rel="me" href="https://fosstodon.org/@zenbrowser"></link>
|
<script
|
||||||
<script defer data-domain="zen-browser.app" src="https://plausible.io/js/script.js"></script>
|
defer
|
||||||
<link rel="alternate" type="application/rss+xml" title="Zen Browser Release Notes" href="https://www.zen-browser.app/feed.xml" />
|
data-domain="zen-browser.app"
|
||||||
</head>
|
src="https://plausible.io/js/script.js"
|
||||||
<body className={inter.className}>
|
></script>
|
||||||
<ThemeProvider
|
<link
|
||||||
attribute="class"
|
rel="alternate"
|
||||||
enableSystem
|
type="application/rss+xml"
|
||||||
disableTransitionOnChange
|
title="Zen Browser Release Notes"
|
||||||
>
|
href="https://www.zen-browser.app/feed.xml"
|
||||||
<StyledComponentsRegistry>
|
/>
|
||||||
<div>
|
</head>
|
||||||
{children}
|
<body className={inter.className}>
|
||||||
<Footer />
|
<ThemeProvider attribute="class" enableSystem disableTransitionOnChange>
|
||||||
<Navigation /> {/* At the bottom of the page */}
|
<StyledComponentsRegistry>
|
||||||
</div>
|
<div>
|
||||||
</StyledComponentsRegistry>
|
{children}
|
||||||
</ThemeProvider>
|
<Footer />
|
||||||
</body>
|
<Navigation /> {/* At the bottom of the page */}
|
||||||
</html>
|
</div>
|
||||||
);
|
</StyledComponentsRegistry>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { HomeIcon } from "@radix-ui/react-icons";
|
import { HomeIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
export default function NotFoundPage() {
|
export default function NotFoundPage() {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen grid place-items-center">
|
<main className="grid min-h-screen place-items-center">
|
||||||
<div className="flex flex-col justify-center items-center text-center">
|
<div className="flex flex-col items-center justify-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">
|
<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!
|
Page Not Found!
|
||||||
</h1>
|
</h1>
|
||||||
<a href="/"><Button
|
<a href="/">
|
||||||
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"
|
<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>
|
||||||
<span>Back to Home</span>
|
<HomeIcon className="ml-1 size-4 transition-transform duration-300 ease-in-out group-hover:translate-x-1" />
|
||||||
<HomeIcon className="ml-1 size-4 transition-transform duration-300 ease-in-out group-hover:translate-x-1" />
|
</Button>
|
||||||
</Button></a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import Header from "@/components/header";
|
|||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen overflow-x-hidden flex-col items-center justify-start">
|
<main className="flex min-h-screen flex-col items-center justify-start overflow-x-hidden">
|
||||||
<Header />
|
<Header />
|
||||||
<Features />
|
<Features />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,58 +1,57 @@
|
|||||||
#policy h1 {
|
#policy h1 {
|
||||||
font-size: 2.5em;
|
font-size: 2.5em;
|
||||||
margin: 0.67em 0;
|
margin: 0.67em 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#policy {
|
#policy {
|
||||||
padding-top: 2.5em;
|
padding-top: 2.5em;
|
||||||
margin-top: 4em;
|
margin-top: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#policy h1:first-child {
|
#policy h1:first-child {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#policy h2 {
|
#policy h2 {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
margin: 0.83em 0;
|
margin: 0.83em 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#policy h3 {
|
#policy h3 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
font-weight: semi-bold;
|
font-weight: semi-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#policy ul {
|
#policy ul {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#policy li {
|
#policy li {
|
||||||
list-style: circle;
|
list-style: circle;
|
||||||
margin-left: 1.1em;
|
margin-left: 1.1em;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Link styles */
|
/* Link styles */
|
||||||
#policy a {
|
#policy a {
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
transition: color 0.2s ease-in-out;
|
transition: color 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#policy a:hover {
|
#policy a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
color: #0056b3;
|
color: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
#policy hr {
|
#policy hr {
|
||||||
margin: 2em 0;
|
margin: 2em 0;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
#policy p {
|
#policy p {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ import Footer from "@/components/footer";
|
|||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
import { releaseNoteIsAlpha, releaseNotes } from "@/lib/release-notes";
|
import { releaseNoteIsAlpha, releaseNotes } from "@/lib/release-notes";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Markdown from 'react-markdown'
|
import Markdown from "react-markdown";
|
||||||
import './markdown.css';
|
import "./markdown.css";
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||||
<div id="policy" className="min-h-screen py-42 flex mx-auto my-52 p-10 lg:p-0 w-full lg:w-1/3 flex-col">
|
<div
|
||||||
<Markdown>
|
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
|
# Privacy Policy
|
||||||
* Last updated: 2024-08-12
|
* 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.`}
|
By using Zen Browser, you agree to this Privacy Policy. Remember, with Zen, your privacy is in your hands.`}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import React from 'react';
|
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
import ReleaseNote from "@/components/release-note";
|
import ReleaseNote from "@/components/release-note";
|
||||||
@@ -7,36 +6,45 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { releaseNotes } from "@/lib/release-notes";
|
import { releaseNotes } from "@/lib/release-notes";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
return [{version: "latest"}, ...releaseNotes.map((note) => ({ version: note.version }))];
|
return [
|
||||||
|
{ version: "latest" },
|
||||||
|
...releaseNotes.map((note) => ({ version: note.version })),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ReleaseNotePage({ params }: { params: { version: string } }) {
|
export default function ReleaseNotePage({
|
||||||
const { version } = params;
|
params,
|
||||||
|
}: {
|
||||||
|
params: { version: string };
|
||||||
|
}) {
|
||||||
|
const { version } = params;
|
||||||
|
|
||||||
if (version === "latest") {
|
if (version === "latest") {
|
||||||
return redirect(`/release-notes/${releaseNotes[0].version}`);
|
return redirect(`/release-notes/${releaseNotes[0].version}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentIndex = releaseNotes.findIndex((note) => note.version === version);
|
const currentIndex = releaseNotes.findIndex(
|
||||||
const releaseNote = releaseNotes[currentIndex];
|
(note) => note.version === version,
|
||||||
|
);
|
||||||
if (!releaseNote) {
|
const releaseNote = releaseNotes[currentIndex];
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(`/release-notes#${version}`);
|
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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,47 @@
|
|||||||
|
|
||||||
import ReleaseNoteElement from "@/components/release-note";
|
import ReleaseNoteElement from "@/components/release-note";
|
||||||
import { releaseNotes } from "@/lib/release-notes";
|
import { releaseNotes } from "@/lib/release-notes";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Release Notes",
|
title: "Release Notes",
|
||||||
description: "Stay up to date with the latest changes to Zen Browser",
|
description: "Stay up to date with the latest changes to Zen Browser",
|
||||||
keywords: ["Zen", "Browser", "Zen Browser", "Web", "Internet", "Fast", "Release", "Notes"],
|
keywords: [
|
||||||
|
"Zen",
|
||||||
|
"Browser",
|
||||||
|
"Zen Browser",
|
||||||
|
"Web",
|
||||||
|
"Internet",
|
||||||
|
"Fast",
|
||||||
|
"Release",
|
||||||
|
"Notes",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ReleaseNotes() {
|
export default function ReleaseNotes() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
<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">
|
<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="text-4xl font-bold mt-48">Release Notes</h1>
|
<h1 className="mt-48 text-4xl font-bold">Release Notes</h1>
|
||||||
<p className="mt-8 text-lg text-muted-foreground">
|
<p className="mt-8 text-lg text-muted-foreground">
|
||||||
Stay up to date with the latest changes to Zen Browser! Since the <a className="text-blue-500" href="#1.0.0-a.1">first release</a> till <a className="text-blue-500" href={`/release-notes/${releaseNotes[0].version}`}>{releaseNotes[0].version}</a>, we've been working hard to make Zen Browser the best it can be.<br /><br /> Thanks everyone for your feedback! ❤️
|
Stay up to date with the latest changes to Zen Browser! Since the{" "}
|
||||||
</p>
|
<a className="text-blue-500" href="#1.0.0-a.1">
|
||||||
{releaseNotes.map((releaseNote) => (
|
first release
|
||||||
<ReleaseNoteElement key={releaseNote.version} data={releaseNote} />
|
</a>{" "}
|
||||||
))}
|
till{" "}
|
||||||
</div>
|
<a
|
||||||
</main>
|
className="text-blue-500"
|
||||||
)
|
href={`/release-notes/${releaseNotes[0].version}`}
|
||||||
|
>
|
||||||
|
{releaseNotes[0].version}
|
||||||
|
</a>
|
||||||
|
, we've been working hard to make Zen Browser the best it can be.
|
||||||
|
<br />
|
||||||
|
<br /> Thanks everyone for your feedback! ❤️
|
||||||
|
</p>
|
||||||
|
{releaseNotes.map((releaseNote) => (
|
||||||
|
<ReleaseNoteElement key={releaseNote.version} data={releaseNote} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
import ThemePage from "@/components/theme-page";
|
import ThemePage from "@/components/theme-page";
|
||||||
@@ -6,49 +5,53 @@ import { getAllThemes, getThemeFromId } from "@/lib/themes";
|
|||||||
import { Metadata, ResolvingMetadata } from "next";
|
import { Metadata, ResolvingMetadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata(
|
||||||
{ params, searchParams }: any,
|
{ params, searchParams }: any,
|
||||||
parent: ResolvingMetadata
|
parent: ResolvingMetadata,
|
||||||
): Promise<Metadata> {
|
): Promise<Metadata> {
|
||||||
const theme = params.theme
|
const theme = params.theme;
|
||||||
const themeData = await getThemeFromId(theme);
|
const themeData = await getThemeFromId(theme);
|
||||||
if (!themeData) {
|
if (!themeData) {
|
||||||
return {
|
return {
|
||||||
title: "Theme not found",
|
title: "Theme not found",
|
||||||
description: "Theme not found",
|
description: "Theme not found",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
title: themeData.name,
|
title: themeData.name,
|
||||||
description: themeData.description,
|
description: themeData.description,
|
||||||
keywords: [themeData.name, themeData.description],
|
keywords: [themeData.name, themeData.description],
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: themeData.name,
|
title: themeData.name,
|
||||||
description: themeData.description,
|
description: themeData.description,
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: themeData.image,
|
url: themeData.image,
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 500,
|
height: 500,
|
||||||
alt: themeData.name,
|
alt: themeData.name,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const themes = await getAllThemes();
|
const themes = await getAllThemes();
|
||||||
console.log(themes);
|
console.log(themes);
|
||||||
return themes.map((theme) => ({
|
return themes.map((theme) => ({
|
||||||
theme: theme.id,
|
theme: theme.id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ThemeInfoPage({ params }: { params: { theme: string } }) {
|
export default async function ThemeInfoPage({
|
||||||
const { theme } = params;
|
params,
|
||||||
return (
|
}: {
|
||||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
params: { theme: string };
|
||||||
<ThemePage themeID={theme} />
|
}) {
|
||||||
</main>
|
const { theme } = params;
|
||||||
);
|
return (
|
||||||
}
|
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||||
|
<ThemePage themeID={theme} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
import MarketplacePage from "@/components/marketplace";
|
import MarketplacePage from "@/components/marketplace";
|
||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
@@ -6,9 +5,9 @@ import { getAllThemes, ZenTheme } from "@/lib/themes";
|
|||||||
import { GetStaticProps } from "next";
|
import { GetStaticProps } from "next";
|
||||||
|
|
||||||
export default async function ThemesMarketplace() {
|
export default async function ThemesMarketplace() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||||
<MarketplacePage themes={await getAllThemes()} />
|
<MarketplacePage themes={await getAllThemes()} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
|
|
||||||
import WelcomePage from "@/components/welcome";
|
import WelcomePage from "@/components/welcome";
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
|
|
||||||
export default function Download() {
|
export default function Download() {
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-start">
|
<main className="flex min-h-screen flex-col items-center justify-start">
|
||||||
<WelcomePage />
|
<WelcomePage />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
function imageLoader({ src }: { src: string }) {
|
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) {
|
export default function CachedImage({ ...props }: any) {
|
||||||
return <Image {...props} loader={imageLoader} />;
|
return <Image {...props} loader={imageLoader} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,93 @@
|
|||||||
|
|
||||||
import { LOGO_COLORS } from "@/lib/logos";
|
import { LOGO_COLORS } from "@/lib/logos";
|
||||||
|
|
||||||
export function BrandingAssets() {
|
export function BrandingAssets() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full mx-auto p-5 lg:w-1/2 lg:p-0 items-center justify-center h-full mt-36">
|
<div className="mx-auto mt-36 flex h-full w-full flex-col items-center justify-center p-5 lg:w-1/2 lg:p-0">
|
||||||
<div className="mx-auto w-full text-center">
|
<div className="mx-auto w-full text-center">
|
||||||
<h1 className="text-4xl lg:text-7xl font-bold">Branding Assets</h1>
|
<h1 className="text-4xl font-bold lg:text-7xl">Branding Assets</h1>
|
||||||
<p className="text-muted-foreground mt-2">Download Zen Browser branding assets for your website or project.</p>
|
<p className="mt-2 text-muted-foreground">
|
||||||
</div>
|
Download Zen Browser branding assets for your website or project.
|
||||||
<div className="flex w-full lg:w-2/3 flex-col mt-10">
|
</p>
|
||||||
<h2 className="text-2xl font-bold mt-10">Logos</h2>
|
</div>
|
||||||
<p className="text-muted-foreground mt-2">
|
<div className="mt-10 flex w-full flex-col lg:w-2/3">
|
||||||
Download the Zen Browser logo in different colors.
|
<h2 className="mt-10 text-2xl font-bold">Logos</h2>
|
||||||
</p>
|
<p className="mt-2 text-muted-foreground">
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-10 mt-10 w-full">
|
Download the Zen Browser logo in different colors.
|
||||||
{LOGO_COLORS.map((color) => (
|
</p>
|
||||||
<div key={color} className="flex flex-col items-center">
|
<div className="mt-10 grid w-full grid-cols-2 gap-10 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
<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" />
|
{LOGO_COLORS.map((color) => (
|
||||||
<div className="flex items-center my-2">
|
<div key={color} className="flex flex-col items-center">
|
||||||
<a
|
<img
|
||||||
href={`/logos/zen-${color}.svg`}
|
src={`https://cdn.jsdelivr.net/gh/zen-browser/www/public/logos/zen-${color}.svg`}
|
||||||
download={`zen-${color}.svg`}
|
alt={`Zen Browser ${color} logo`}
|
||||||
className="text-blue-500 text-md ml-2"
|
className="mt-4 h-24 w-24"
|
||||||
>
|
/>
|
||||||
{color}
|
<div className="my-2 flex items-center">
|
||||||
</a>
|
<a
|
||||||
</div>
|
href={`/logos/zen-${color}.svg`}
|
||||||
</div>
|
download={`zen-${color}.svg`}
|
||||||
))}
|
className="text-md ml-2 text-blue-500"
|
||||||
</div>
|
>
|
||||||
</div>
|
{color}
|
||||||
<div className="flex w-full lg:w-2/3 flex-col mt-10">
|
</a>
|
||||||
<h2 className="text-2xl font-bold mt-10">Empty Logos</h2>
|
</div>
|
||||||
<p className="text-muted-foreground mt-2">
|
</div>
|
||||||
Download the Zen Browser logo in different colors without a filled Zen letter.
|
))}
|
||||||
</p>
|
</div>
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-10 mt-10 w-full">
|
</div>
|
||||||
{LOGO_COLORS.map((color) => (
|
<div className="mt-10 flex w-full flex-col lg:w-2/3">
|
||||||
<div key={color} className="flex flex-col items-center">
|
<h2 className="mt-10 text-2xl font-bold">Empty Logos</h2>
|
||||||
<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" />
|
<p className="mt-2 text-muted-foreground">
|
||||||
<div className="flex items-center my-2">
|
Download the Zen Browser logo in different colors without a filled Zen
|
||||||
<a
|
letter.
|
||||||
href={`/logos/zen-alpha-${color}.svg`}
|
</p>
|
||||||
download={`zen-alpha-${color}.svg`}
|
<div className="mt-10 grid w-full grid-cols-2 gap-10 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
className="text-blue-500 text-md ml-2"
|
{LOGO_COLORS.map((color) => (
|
||||||
>
|
<div key={color} className="flex flex-col items-center">
|
||||||
{color}
|
<img
|
||||||
</a>
|
src={`https://cdn.jsdelivr.net/gh/zen-browser/www/public/logos/zen-alpha-${color}.svg`}
|
||||||
</div>
|
alt={`Zen Browser ${color} logo`}
|
||||||
</div>
|
className="mt-4 h-24 w-24"
|
||||||
))}
|
/>
|
||||||
</div>
|
<div className="my-2 flex items-center">
|
||||||
</div>
|
<a
|
||||||
<div className="mt-10">
|
href={`/logos/zen-alpha-${color}.svg`}
|
||||||
<h2 className="text-2xl font-bold">License</h2>
|
download={`zen-alpha-${color}.svg`}
|
||||||
<p className="text-muted-foreground mt-2">
|
className="text-md ml-2 text-blue-500"
|
||||||
All branding assets are licensed under the{" "}
|
>
|
||||||
<a
|
{color}
|
||||||
href="https://creativecommons.org/licenses/by-sa/4.0/"
|
</a>
|
||||||
target="_blank"
|
</div>
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
className="text-blue-500"
|
))}
|
||||||
>
|
</div>
|
||||||
CC BY-SA 4.0
|
</div>
|
||||||
</a>
|
<div className="mt-10">
|
||||||
. Thanks to <a href="https://www.onnno.nl/" className="text-blue-500">Donno (mr. Logos)</a> for the assets.
|
<h2 className="text-2xl font-bold">License</h2>
|
||||||
<br />
|
<p className="mt-2 text-muted-foreground">
|
||||||
These logos however shall not be modified in a way that suggests the licensor endorses you or your use.
|
All branding assets are licensed under the{" "}
|
||||||
<br />
|
<a
|
||||||
<br />
|
href="https://creativecommons.org/licenses/by-sa/4.0/"
|
||||||
You are free to share and adapt the assets for any purpose, even commercially.
|
target="_blank"
|
||||||
</p>
|
rel="noopener noreferrer"
|
||||||
</div>
|
className="text-blue-500"
|
||||||
</div>
|
>
|
||||||
);
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/components/cool-header-text.tsx
Normal file
35
src/components/cool-header-text.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import styled, { keyframes } from "styled-components";
|
||||||
|
|
||||||
|
const hueShift = keyframes`
|
||||||
|
0% {
|
||||||
|
filter: hue-rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
filter: hue-rotate(170deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
filter: hue-rotate(0deg);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TextTitle = styled.h1`
|
||||||
|
background-clip: text;
|
||||||
|
background-image: linear-gradient(90deg, #0077e7, #01d8d1);
|
||||||
|
filter: hue-rotate(0deg);
|
||||||
|
animation: ${hueShift} 10s infinite linear 1s;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function CoolHeaderText() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="relative font-extrabold mt-5 mb-5 -translate-y-4 animate-fade-in text-balance bg-gradient-to-br from-black from-30% to-black/40 bg-clip-text py-6 text-5xl font-semibold leading-none tracking-tighter text-transparent opacity-0 [--animation-delay:200ms] dark:from-white dark:to-white/40 sm:text-6xl md:text-7xl lg:text-8xl">
|
||||||
|
<TextTitle>
|
||||||
|
Beautiful. Fast. Private.<br />Your Browser, Your Way.
|
||||||
|
</TextTitle>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-[-5px] right-[-20px] transform shadow rotate-[15deg] rounded-full mt-12 pointer-events-none hidden md:block bg-blue-500 px-3 py-1 w-fit h-fit">
|
||||||
|
Alpha Version
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,41 +4,49 @@ import { ny } from "@/lib/utils";
|
|||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
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 { 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 = [
|
export const COLORS = ["#ffaa40", "#9c40ff", "#ff40aa", "#40ffaa", "#40aaff"];
|
||||||
"#ffaa40",
|
|
||||||
"#9c40ff",
|
|
||||||
"#ff40aa",
|
|
||||||
"#40ffaa",
|
|
||||||
"#40aaff",
|
|
||||||
];
|
|
||||||
|
|
||||||
const ThemeFormWrapper = styled.div<{
|
const ThemeFormWrapper = styled.div<{
|
||||||
primaryColor: string,
|
primaryColor: string;
|
||||||
accentColor: string,
|
accentColor: string;
|
||||||
secondaryColor: string,
|
secondaryColor: string;
|
||||||
tertiaryColor: string,
|
tertiaryColor: string;
|
||||||
colorsBorder: string,
|
colorsBorder: string;
|
||||||
dialogBg: string,
|
dialogBg: string;
|
||||||
}>`
|
}>`
|
||||||
${({
|
${({
|
||||||
primaryColor,
|
primaryColor,
|
||||||
accentColor,
|
accentColor,
|
||||||
secondaryColor,
|
secondaryColor,
|
||||||
tertiaryColor,
|
tertiaryColor,
|
||||||
colorsBorder,
|
colorsBorder,
|
||||||
dialogBg,
|
dialogBg,
|
||||||
}: {
|
}: {
|
||||||
primaryColor: string;
|
primaryColor: string;
|
||||||
accentColor: string;
|
accentColor: string;
|
||||||
secondaryColor: string;
|
secondaryColor: string;
|
||||||
tertiaryColor: string;
|
tertiaryColor: string;
|
||||||
colorsBorder: string;
|
colorsBorder: string;
|
||||||
dialogBg: string;
|
dialogBg: string;
|
||||||
}) => `
|
}) => `
|
||||||
--zen-primary-color: ${accentColor};
|
--zen-primary-color: ${accentColor};
|
||||||
|
|
||||||
--zen-colors-primary: ${primaryColor};
|
--zen-colors-primary: ${primaryColor};
|
||||||
@@ -51,180 +59,298 @@ const ThemeFormWrapper = styled.div<{
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const defaultStyles = {
|
const defaultStyles = {
|
||||||
primaryColor: {
|
primaryColor: {
|
||||||
light: "color-mix(in srgb, var(--zen-primary-color) 50%, black 50%)",
|
light: "color-mix(in srgb, var(--zen-primary-color) 50%, black 50%)",
|
||||||
dark: "color-mix(in srgb, var(--zen-primary-color) 50%, black 50%)",
|
dark: "color-mix(in srgb, var(--zen-primary-color) 50%, black 50%)",
|
||||||
},
|
},
|
||||||
secondaryColor: {
|
secondaryColor: {
|
||||||
light: "color-mix(in srgb, var(--zen-primary-color) 40%, white 60%)",
|
light: "color-mix(in srgb, var(--zen-primary-color) 40%, white 60%)",
|
||||||
dark: "color-mix(in srgb, var(--zen-primary-color) 40%, black 60%)",
|
dark: "color-mix(in srgb, var(--zen-primary-color) 40%, black 60%)",
|
||||||
},
|
},
|
||||||
tertiaryColor: {
|
tertiaryColor: {
|
||||||
light: "color-mix(in srgb, var(--zen-primary-color) 7%, white 93%)",
|
light: "color-mix(in srgb, var(--zen-primary-color) 7%, white 93%)",
|
||||||
dark: "color-mix(in srgb, var(--zen-primary-color) 15%, black 85%)",
|
dark: "color-mix(in srgb, var(--zen-primary-color) 15%, black 85%)",
|
||||||
},
|
},
|
||||||
colorsBorder: {
|
colorsBorder: {
|
||||||
light: "color-mix(in srgb, var(--zen-colors-secondary) 90%, black 10%)",
|
light: "color-mix(in srgb, var(--zen-colors-secondary) 90%, black 10%)",
|
||||||
dark: "color-mix(in srgb, var(--zen-colors-secondary) 80%, black 20%)",
|
dark: "color-mix(in srgb, var(--zen-colors-secondary) 80%, black 20%)",
|
||||||
},
|
},
|
||||||
dialogBg: {
|
dialogBg: {
|
||||||
light: "var(--zen-colors-tertiary)",
|
light: "var(--zen-colors-tertiary)",
|
||||||
dark: "color-mix(in srgb, var(--zen-primary-color) 10%, black 90%)",
|
dark: "color-mix(in srgb, var(--zen-primary-color) 10%, black 90%)",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function CreateThemePage() {
|
export default function CreateThemePage() {
|
||||||
const [selectedColor, setSelectedColor] = React.useState(COLORS[0]);
|
const [selectedColor, setSelectedColor] = React.useState(COLORS[0]);
|
||||||
const [isDarkMode, setIsDarkMode] = React.useState(false);
|
const [isDarkMode, setIsDarkMode] = React.useState(false);
|
||||||
|
|
||||||
const [primaryColor, setPrimaryColor] = React.useState(defaultStyles.primaryColor.dark);
|
const [primaryColor, setPrimaryColor] = React.useState(
|
||||||
const [secondaryColor, setSecondaryColor] = React.useState(defaultStyles.secondaryColor.dark);
|
defaultStyles.primaryColor.dark,
|
||||||
const [tertiaryColor, setTertiaryColor] = React.useState(defaultStyles.tertiaryColor.dark);
|
);
|
||||||
const [colorsBorder, setColorsBorder] = React.useState(defaultStyles.colorsBorder.dark);
|
const [secondaryColor, setSecondaryColor] = React.useState(
|
||||||
const [dialogBg, setDialogBg] = React.useState(defaultStyles.dialogBg.dark);
|
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(() => {
|
React.useEffect(() => {
|
||||||
setPrimaryColor(isDarkMode ? defaultStyles.primaryColor.dark : defaultStyles.primaryColor.light);
|
setPrimaryColor(
|
||||||
setSecondaryColor(isDarkMode ? defaultStyles.secondaryColor.dark : defaultStyles.secondaryColor.light);
|
isDarkMode
|
||||||
setTertiaryColor(isDarkMode ? defaultStyles.tertiaryColor.dark : defaultStyles.tertiaryColor.light);
|
? defaultStyles.primaryColor.dark
|
||||||
setColorsBorder(isDarkMode ? defaultStyles.colorsBorder.dark : defaultStyles.colorsBorder.light);
|
: defaultStyles.primaryColor.light,
|
||||||
setDialogBg(isDarkMode ? defaultStyles.dialogBg.dark : defaultStyles.dialogBg.light);
|
);
|
||||||
}, [isDarkMode]);
|
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 = () => {
|
const generateThemeData = () => {
|
||||||
let theme: any = {
|
let theme: any = {
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
};
|
};
|
||||||
// Dont add the default values
|
// Dont add the default values
|
||||||
if (primaryColor !== (isDarkMode ? defaultStyles.primaryColor.dark : defaultStyles.primaryColor.light)) {
|
if (
|
||||||
theme["primaryColor"] = primaryColor;
|
primaryColor !==
|
||||||
}
|
(isDarkMode
|
||||||
if (secondaryColor !== (isDarkMode ? defaultStyles.secondaryColor.dark : defaultStyles.secondaryColor.light)) {
|
? defaultStyles.primaryColor.dark
|
||||||
theme["secondaryColor"] = secondaryColor;
|
: defaultStyles.primaryColor.light)
|
||||||
}
|
) {
|
||||||
if (tertiaryColor !== (isDarkMode ? defaultStyles.tertiaryColor.dark : defaultStyles.tertiaryColor.light)) {
|
theme["primaryColor"] = primaryColor;
|
||||||
theme["tertiaryColor"] = tertiaryColor;
|
}
|
||||||
}
|
if (
|
||||||
if (colorsBorder !== (isDarkMode ? defaultStyles.colorsBorder.dark : defaultStyles.colorsBorder.light)) {
|
secondaryColor !==
|
||||||
theme["colorsBorder"] = colorsBorder
|
(isDarkMode
|
||||||
}
|
? defaultStyles.secondaryColor.dark
|
||||||
if (dialogBg !== (isDarkMode ? defaultStyles.dialogBg.dark : defaultStyles.dialogBg.light)) {
|
: defaultStyles.secondaryColor.light)
|
||||||
theme["dialogBg"] = dialogBg
|
) {
|
||||||
}
|
theme["secondaryColor"] = secondaryColor;
|
||||||
if (COLORS.indexOf(selectedColor) !== 0) {
|
}
|
||||||
theme["accentColor"] = selectedColor;
|
if (
|
||||||
}
|
tertiaryColor !==
|
||||||
return JSON.stringify(theme, null, 4);
|
(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 (
|
return (
|
||||||
<ThemeFormWrapper
|
<ThemeFormWrapper
|
||||||
primaryColor={primaryColor}
|
primaryColor={primaryColor}
|
||||||
accentColor={selectedColor}
|
accentColor={selectedColor}
|
||||||
secondaryColor={secondaryColor}
|
secondaryColor={secondaryColor}
|
||||||
tertiaryColor={tertiaryColor}
|
tertiaryColor={tertiaryColor}
|
||||||
colorsBorder={colorsBorder}
|
colorsBorder={colorsBorder}
|
||||||
dialogBg={dialogBg}
|
dialogBg={dialogBg}
|
||||||
className="flex flex-col mt-32 items-center justify-center w-full min-h-screen">
|
className="mt-32 flex min-h-screen w-full flex-col items-center justify-center"
|
||||||
<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>
|
<div className="lg:px-none mx-auto w-full px-2 lg:w-1/2 xl:w-1/2">
|
||||||
<p className="text-lg opacity-40 mt-2">Create your own theme for Zen Browser and share it with the community.</p>
|
<h1 className="text-4xl font-bold lg:text-7xl">Create your theme</h1>
|
||||||
<div className="text-xs text-muted-foreground mt-8">
|
<p className="mt-2 text-lg opacity-40">
|
||||||
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.
|
Create your own theme for Zen Browser and share it with the community.
|
||||||
</div>
|
</p>
|
||||||
<div className="flex items-center mt-2">
|
<div className="mt-8 text-xs text-muted-foreground">
|
||||||
{COLORS.map((color) => (
|
If the color is chosen from the palette, the accent color will be set
|
||||||
<div
|
to the user's selection in the preferences. However, if the color is
|
||||||
key={color}
|
chosen from the color picker, the accent color will be the color
|
||||||
onClick={() => setSelectedColor(color)}
|
selected.
|
||||||
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" : "")}
|
</div>
|
||||||
style={{ backgroundColor: color }}
|
<div className="mt-2 flex items-center">
|
||||||
></div>
|
{COLORS.map((color) => (
|
||||||
))}
|
<div
|
||||||
<div className="mx-4">
|
key={color}
|
||||||
or
|
onClick={() => setSelectedColor(color)}
|
||||||
</div>
|
className={ny(
|
||||||
<input type="color" value={selectedColor} onChange={(e) => setSelectedColor(e.target.value)} className="w-9 h-7 rounded cursor-pointer outline-none" />
|
`mx-2 h-6 w-6 cursor-pointer rounded-md text-white shadow-sm`,
|
||||||
</div>
|
selectedColor === color
|
||||||
<div className="flex flex-col lg:flex-row">
|
? "ring-2 ring-black dark:ring-white"
|
||||||
<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" />
|
style={{ backgroundColor: color }}
|
||||||
<label htmlFor="dark-mode" className="text-md font-bold opacity-60">Dark mode</label>
|
></div>
|
||||||
</div>
|
))}
|
||||||
<h2 className="text-lg mt-8 font-bold opacity-70">
|
<div className="mx-4">or</div>
|
||||||
Primary color
|
<input
|
||||||
</h2>
|
type="color"
|
||||||
<div className="flex items-center mt-2">
|
value={selectedColor}
|
||||||
<input type="text" className="border text-gray-500 rounded-lg p-2 w-2/3" value={primaryColor} onChange={(e) => setPrimaryColor(e.target.value)} />
|
onChange={(e) => setSelectedColor(e.target.value)}
|
||||||
<div className="w-11 h-11 ml-4 rounded-lg border bg-[var(--zen-colors-primary)]"></div>
|
className="h-7 w-9 cursor-pointer rounded outline-none"
|
||||||
</div>
|
/>
|
||||||
<h2 className="text-lg mt-8 font-bold opacity-70">
|
</div>
|
||||||
Secondary color
|
<div className="flex flex-col lg:flex-row">
|
||||||
</h2>
|
<div className="w-full">
|
||||||
<div className="flex items-center mt-2">
|
<div className="mt-10 flex select-none items-center">
|
||||||
<input type="text" className="border text-gray-500 rounded-lg p-2 w-2/3" value={secondaryColor} onChange={(e) => setSecondaryColor(e.target.value)} />
|
<input
|
||||||
<div className="w-11 h-11 ml-4 rounded-lg border bg-[var(--zen-colors-secondary)]"></div>
|
type="checkbox"
|
||||||
</div>
|
className="mr-2"
|
||||||
<h2 className="text-lg mt-8 font-bold opacity-70">
|
checked={isDarkMode}
|
||||||
Tertiary color
|
onChange={(e) => setIsDarkMode(e.target.checked)}
|
||||||
</h2>
|
id="dark-mode"
|
||||||
<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)} />
|
<label
|
||||||
<div className="w-11 h-11 ml-4 rounded-lg border bg-[var(--zen-colors-tertiary)]"></div>
|
htmlFor="dark-mode"
|
||||||
</div>
|
className="text-md font-bold opacity-60"
|
||||||
<h2 className="text-lg mt-8 font-bold opacity-70">
|
>
|
||||||
Border color
|
Dark mode
|
||||||
</h2>
|
</label>
|
||||||
<div className="flex items-center mt-2">
|
</div>
|
||||||
<input type="text" className="border text-gray-500 rounded-lg p-2 w-2/3" value={colorsBorder} onChange={(e) => setColorsBorder(e.target.value)} />
|
<h2 className="mt-8 text-lg font-bold opacity-70">Primary color</h2>
|
||||||
<div className="w-11 h-11 ml-4 rounded-lg border bg-[var(--zen-colors-border)]"></div>
|
<div className="mt-2 flex items-center">
|
||||||
</div>
|
<input
|
||||||
<h2 className="text-lg mt-8 font-bold opacity-70">
|
type="text"
|
||||||
Dialog background color
|
className="w-2/3 rounded-lg border p-2 text-gray-500"
|
||||||
</h2>
|
value={primaryColor}
|
||||||
<div className="flex items-center mt-2">
|
onChange={(e) => setPrimaryColor(e.target.value)}
|
||||||
<input type="text" className="border text-gray-500 rounded-lg p-2 w-2/3" value={dialogBg} onChange={(e) => setDialogBg(e.target.value)} />
|
/>
|
||||||
<div className="w-11 h-11 ml-4 rounded-lg border bg-[var(--zen-dialog-background)]"></div>
|
<div className="ml-4 h-11 w-11 rounded-lg border bg-[var(--zen-colors-primary)]"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-md font-bold text-muted-foreground mt-8">
|
<h2 className="mt-8 text-lg font-bold opacity-70">
|
||||||
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.
|
Secondary color
|
||||||
</div>
|
</h2>
|
||||||
<Sheet>
|
<div className="mt-2 flex items-center">
|
||||||
<SheetTrigger asChild>
|
<input
|
||||||
<Button disabled className="mt-8">Create theme</Button>
|
type="text"
|
||||||
</SheetTrigger>
|
className="w-2/3 rounded-lg border p-2 text-gray-500"
|
||||||
<SheetContent className="!w-[600px] !max-w-lg">
|
value={secondaryColor}
|
||||||
<SheetHeader>
|
onChange={(e) => setSecondaryColor(e.target.value)}
|
||||||
<SheetTitle>Theme data</SheetTitle>
|
/>
|
||||||
<SheetDescription>
|
<div className="ml-4 h-11 w-11 rounded-lg border bg-[var(--zen-colors-secondary)]"></div>
|
||||||
Copy the following JSON object and paste it into your Zen Browser theme format.
|
</div>
|
||||||
</SheetDescription>
|
<h2 className="mt-8 text-lg font-bold opacity-70">
|
||||||
</SheetHeader>
|
Tertiary color
|
||||||
<pre className="text-sm mt-6 text-wrap font-mono p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">{generateThemeData()}</pre>
|
</h2>
|
||||||
<SheetFooter className="mt-4">
|
<div className="mt-2 flex items-center">
|
||||||
<Button onClick={() =>
|
<input
|
||||||
navigator.clipboard.writeText(generateThemeData())
|
type="text"
|
||||||
} variant="ghost">
|
className="w-2/3 rounded-lg border p-2 text-gray-500"
|
||||||
Copy to clipboard
|
value={tertiaryColor}
|
||||||
</Button>
|
onChange={(e) => setTertiaryColor(e.target.value)}
|
||||||
<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");
|
<div className="ml-4 h-11 w-11 rounded-lg border bg-[var(--zen-colors-tertiary)]"></div>
|
||||||
}} >
|
</div>
|
||||||
Submit theme
|
<h2 className="mt-8 text-lg font-bold opacity-70">Border color</h2>
|
||||||
</Button>
|
<div className="mt-2 flex items-center">
|
||||||
</SheetFooter>
|
<input
|
||||||
</SheetContent>
|
type="text"
|
||||||
</Sheet>
|
className="w-2/3 rounded-lg border p-2 text-gray-500"
|
||||||
</div>
|
value={colorsBorder}
|
||||||
{/* Preview */}
|
onChange={(e) => setColorsBorder(e.target.value)}
|
||||||
<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">
|
<div className="ml-4 h-11 w-11 rounded-lg border bg-[var(--zen-colors-border)]"></div>
|
||||||
<Button className={ny("bg-[var(--zen-colors-secondary)]", isDarkMode ? "text-white" : "text-black")}>Button</Button>
|
</div>
|
||||||
</div>
|
<h2 className="mt-8 text-lg font-bold opacity-70">
|
||||||
</div>
|
Dialog background color
|
||||||
</div>
|
</h2>
|
||||||
</div>
|
<div className="mt-2 flex items-center">
|
||||||
</ThemeFormWrapper>
|
<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
@@ -1,316 +1,561 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import Sticky from 'react-sticky-el';
|
import Sticky from "react-sticky-el";
|
||||||
import {
|
import {
|
||||||
BookmarkCheckIcon,
|
BookmarkCheckIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
EyeOffIcon,
|
EyeOffIcon,
|
||||||
Github,
|
Github,
|
||||||
HeartHandshake,
|
HeartHandshake,
|
||||||
HeartPulseIcon,
|
HeartPulseIcon,
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
PaintBucket,
|
PaintBucket,
|
||||||
PersonStanding,
|
PersonStanding,
|
||||||
RabbitIcon,
|
RabbitIcon,
|
||||||
ShieldAlertIcon,
|
ShieldAlertIcon,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
SidebarCloseIcon,
|
SidebarCloseIcon,
|
||||||
SidebarIcon,
|
SidebarIcon,
|
||||||
SidebarOpenIcon,
|
SidebarOpenIcon,
|
||||||
SpaceIcon,
|
SpaceIcon,
|
||||||
SplitSquareHorizontal,
|
SplitSquareHorizontal,
|
||||||
SplitSquareVertical,
|
SplitSquareVertical,
|
||||||
SplitSquareVerticalIcon,
|
SplitSquareVerticalIcon,
|
||||||
TableIcon,
|
TableIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from 'lucide-react';
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Cross1Icon,
|
Cross1Icon,
|
||||||
EyeClosedIcon,
|
EyeClosedIcon,
|
||||||
HeartFilledIcon,
|
HeartFilledIcon,
|
||||||
Link1Icon,
|
Link1Icon,
|
||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
QuestionMarkCircledIcon,
|
QuestionMarkCircledIcon,
|
||||||
QuestionMarkIcon,
|
QuestionMarkIcon,
|
||||||
ReloadIcon,
|
ReloadIcon,
|
||||||
SpaceBetweenHorizontallyIcon,
|
SpaceBetweenHorizontallyIcon,
|
||||||
UpdateIcon,
|
UpdateIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from "@radix-ui/react-icons";
|
||||||
import Image from "next/legacy/image";
|
import Image from "next/legacy/image";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import { Button } from './ui/button';
|
import { Button } from "./ui/button";
|
||||||
import { COLORS } from './create-theme';
|
import { COLORS } from "./create-theme";
|
||||||
import { Slider } from './ui/slider';
|
import { Slider } from "./ui/slider";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from './ui/table';
|
} from "./ui/table";
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { ny } from '@/lib/utils';
|
import { ny } from "@/lib/utils";
|
||||||
import ThemeCard from './theme-card';
|
import ThemeCard from "./theme-card";
|
||||||
import { getAllThemes, ZenTheme } from '@/lib/themes';
|
import { getAllThemes, ZenTheme } from "@/lib/themes";
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './ui/accordion';
|
import {
|
||||||
import Logo from './logo';
|
Accordion,
|
||||||
import CachedImage from './CachedImage';
|
AccordionContent,
|
||||||
import { transform } from 'next/dist/build/swc';
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "./ui/accordion";
|
||||||
|
import Logo from "./logo";
|
||||||
|
import CachedImage from "./CachedImage";
|
||||||
|
import { transform } from "next/dist/build/swc";
|
||||||
|
|
||||||
function Checkmark() {
|
function Checkmark() {
|
||||||
return (
|
return (
|
||||||
<CheckIcon className="text-black rounded-full bg-green-500 dark:bg-green-400 p-1 w-7 h-7 flex-none" />
|
<CheckIcon className="h-7 w-7 flex-none rounded-full bg-green-500 p-1 text-black dark:bg-green-400" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Cross() {
|
function Cross() {
|
||||||
return (
|
return (
|
||||||
<XIcon className="text-black rounded-full bg-red-500 dark:bg-red-400 p-1 w-7 h-7" />
|
<XIcon className="h-7 w-7 rounded-full bg-red-500 p-1 text-black dark:bg-red-400" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Question() {
|
function Question() {
|
||||||
return (
|
return (
|
||||||
<QuestionMarkIcon className="text-black rounded-full bg-yellow-500 dark:bg-yellow-400 p-1 w-7 h-7" />
|
<QuestionMarkIcon className="h-7 w-7 rounded-full bg-yellow-500 p-1 text-black dark:bg-yellow-400" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Features() {
|
export default function Features() {
|
||||||
const [feature, setFeature] = useState("item-1");
|
const [feature, setFeature] = useState("item-1");
|
||||||
return (
|
return (
|
||||||
<section className="flex-col w-full" id="features">
|
<section className="w-full flex-col" 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="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="p-5 lg:p-12 flex-1">
|
<div className="flex-1 p-5 lg:p-12">
|
||||||
<div className="flex p-12 flex-col justify-center">
|
<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 w-10 h-10'></PaintBucket></h3>
|
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||||
<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>
|
Your Browser, your way{" "}
|
||||||
<div className="relative">
|
<PaintBucket className="inline h-10 w-10"></PaintBucket>
|
||||||
<Button className='mt-8' onClick={() => window.open('/themes', '_self')}>View Theme Store</Button>
|
</h3>
|
||||||
</div>
|
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||||
</div>
|
With Zen's Theme Store, you can customize your browsing experience
|
||||||
<hr />
|
to reflect your unique style and preferences. Choose from a wide
|
||||||
<div className="flex p-12 flex-col justify-center">
|
array of themes, colors, and layouts to make Zen truly your own,
|
||||||
<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>
|
transforming your browser into a personalized digital space.
|
||||||
<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>
|
</p>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Button className='mt-8' onClick={() => window.open('/download', '_self')}>Download Now</Button>
|
<Button
|
||||||
</div>
|
className="mt-8"
|
||||||
</div>
|
onClick={() => window.open("/themes", "_self")}
|
||||||
</div>
|
>
|
||||||
|
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="mx-2 h-[1px] border-t lg:h-[unset] lg:w-[1px] lg:border-l lg:border-t-0"></div>
|
||||||
<div className="p-5 lg:p-12 flex-1">
|
<div className="flex-1 p-5 lg:p-12">
|
||||||
<div className="flex p-12 flex-col justify-center">
|
<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 w-10 h-10'></Link1Icon></h3>
|
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||||
<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>
|
Community driven and Open Source{" "}
|
||||||
<div className='relative'>
|
<Link1Icon className="inline h-10 w-10"></Link1Icon>
|
||||||
<Button className='mt-8' onClick={() => window.open('https://github.com/zen-browser', '_blank')}>GitHub Page</Button>
|
</h3>
|
||||||
</div>
|
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||||
<div className='w-full mt-14'>
|
Zen thrives on the contributions of its vibrant community. As an
|
||||||
<div className='flex items-center'>
|
open-source project, Zen encourages collaboration and innovation,
|
||||||
<Checkmark />
|
allowing users and developers alike to shape the future of the
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Firefox Based</p>
|
browser.
|
||||||
</div>
|
</p>
|
||||||
<div className='flex items-center mt-5'>
|
<div className="relative">
|
||||||
<Checkmark />
|
<Button
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Fully Open source</p>
|
className="mt-8"
|
||||||
</div>
|
onClick={() =>
|
||||||
<div className='flex items-center mt-5'>
|
window.open("https://github.com/zen-browser", "_blank")
|
||||||
<Checkmark />
|
}
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Automated Releases to ensure security</p>
|
>
|
||||||
</div>
|
GitHub Page
|
||||||
<div className='flex items-center mt-5'>
|
</Button>
|
||||||
<Checkmark />
|
</div>
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Community driven</p>
|
<div className="mt-14 w-full">
|
||||||
</div>
|
<div className="flex items-center">
|
||||||
<div className='flex items-center mt-5'>
|
<Checkmark />
|
||||||
<Checkmark />
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Constantly improving</p>
|
Firefox Based
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-5 flex items-center">
|
||||||
</div>
|
<Checkmark />
|
||||||
</div>
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
<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'>
|
Fully Open source
|
||||||
<div className='p-16 lg:w-1/2 flex flex-col justify-center'>
|
</p>
|
||||||
<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>
|
</div>
|
||||||
<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="mt-5 flex items-center">
|
||||||
<div className='w-full mt-8'>
|
<Checkmark />
|
||||||
<div className='flex items-center'>
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
<Checkmark />
|
Automated Releases to ensure security
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Completely Customizable</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center mt-4'>
|
<div className="mt-5 flex items-center">
|
||||||
<Checkmark />
|
<Checkmark />
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Vertical Tabs</p>
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
</div>
|
Community driven
|
||||||
<div className='flex items-center mt-4'>
|
</p>
|
||||||
<Checkmark />
|
</div>
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Thoughtful Design</p>
|
<div className="mt-5 flex items-center">
|
||||||
</div>
|
<Checkmark />
|
||||||
</div>
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
</div>
|
Constantly improving
|
||||||
<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" />
|
</p>
|
||||||
</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>
|
||||||
<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>
|
||||||
<div className='p-16 lg:w-1/2 flex flex-col justify-center'>
|
</div>
|
||||||
<h1 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Split Views <SplitSquareHorizontal className='inline w-8 h-8'></SplitSquareHorizontal></h1>
|
</div>
|
||||||
<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="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">
|
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||||
<Button className='mt-8' onClick={() => window.open('/download', '_self')}>Download Now</Button>
|
<h1 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||||
</div>
|
Built for simplicity <EyeIcon className="inline h-8 w-8"></EyeIcon>
|
||||||
</div>
|
</h1>
|
||||||
</div>
|
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||||
<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'>
|
Zen Browser is designed to be simple and easy to use. It's built
|
||||||
<div className="flex p-16 lg:w-1/2 flex-col justify-center">
|
with the user in mind, so you can focus on what matters most.
|
||||||
<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>
|
||||||
<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="mt-8 w-full">
|
||||||
<div className='w-full mt-8'>
|
<div className="flex items-center">
|
||||||
<div className='flex items-center'>
|
<Checkmark />
|
||||||
<Checkmark />
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Workspaces</p>
|
Completely Customizable
|
||||||
</div>
|
</p>
|
||||||
<div className='flex items-center mt-4'>
|
</div>
|
||||||
<Checkmark />
|
<div className="mt-4 flex items-center">
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Fast profile switcher</p>
|
<Checkmark />
|
||||||
</div>
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
<div className='flex items-center mt-4'>
|
Vertical Tabs
|
||||||
<Checkmark />
|
</p>
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Container Tabs</p>
|
</div>
|
||||||
</div>
|
<div className="mt-4 flex items-center">
|
||||||
<div className='flex items-center mt-4'>
|
<Checkmark />
|
||||||
<Question />
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Tab Groups (Coming Soon)</p>
|
Thoughtful Design
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
<div className="flex p-16 lg:w-1/2 flex-col">
|
<CachedImage
|
||||||
<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>
|
width={1350}
|
||||||
<p className='text-lg mt-4 text-gray-600 dark:text-gray-300'>
|
height={900}
|
||||||
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.
|
src="www/public/browser-1.png"
|
||||||
</p>
|
alt="Zen Browser"
|
||||||
<div className="relative">
|
className="rounded-md object-cover object-right lg:w-1/2"
|
||||||
<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 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>
|
<CachedImage
|
||||||
</div>
|
width={1350}
|
||||||
<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'>
|
height={900}
|
||||||
<div className='p-16 lg:w-1/2 flex flex-col justify-center'>
|
src="www/public/browser-2.png"
|
||||||
<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>
|
alt="Zen Browser"
|
||||||
<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>
|
className="rounded-md object-cover object-left lg:w-1/2"
|
||||||
<div className='w-full mt-8'>
|
/>
|
||||||
<div className='flex items-center'>
|
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||||
<Checkmark />
|
<h1 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Quick Access</p>
|
Split Views{" "}
|
||||||
</div>
|
<SplitSquareHorizontal className="inline h-8 w-8"></SplitSquareHorizontal>
|
||||||
<div className='flex items-center mt-4'>
|
</h1>
|
||||||
<Checkmark />
|
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Customizable</p>
|
Zen Browser allows you to split your view into multiple panes, so
|
||||||
</div>
|
you can work on multiple things at once. It's perfect for
|
||||||
<div className='flex items-center mt-4'>
|
multitasking.
|
||||||
<Checkmark />
|
</p>
|
||||||
<p className='ml-2 text-gray-600 dark:text-gray-300'>Easy to Use</p>
|
<div className="relative">
|
||||||
</div>
|
<Button
|
||||||
</div>
|
className="mt-8"
|
||||||
</div>
|
onClick={() => window.open("/download", "_self")}
|
||||||
<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>
|
Download Now
|
||||||
<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'>
|
</Button>
|
||||||
<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>
|
||||||
<div className='p-16 lg:w-1/2 flex flex-col justify-center'>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
<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="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="relative">
|
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||||
<Button className='mt-8' onClick={() => window.open('/download', '_self')}>What are you waiting for?</Button>
|
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||||
</div>
|
Better tab management{" "}
|
||||||
</div>
|
<BookmarkCheckIcon className="inline h-8 w-8"></BookmarkCheckIcon>
|
||||||
</div>
|
</h3>
|
||||||
<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'>
|
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||||
<div className="relative w-full lg:w-1/2 p-5 lg:p-12 flex flex-col justify-center">
|
Better tab management helps you stay organized and focused, reducing
|
||||||
<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>
|
clutter and enhancing productivity
|
||||||
<Accordion type="single" value={feature} onValueChange={setFeature} defaultValue="item-1" className='mt-8'>
|
</p>
|
||||||
<AccordionItem value="item-1">
|
<div className="mt-8 w-full">
|
||||||
<AccordionTrigger>Is it Firefox based?</AccordionTrigger>
|
<div className="flex items-center">
|
||||||
<AccordionContent>
|
<Checkmark />
|
||||||
Yes, Zen Browser is focused on being always at the latest version of Firefox, ensuring that you have the latest security updates and features.
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
</AccordionContent>
|
Workspaces
|
||||||
</AccordionItem>
|
</p>
|
||||||
<AccordionItem value="item-2">
|
</div>
|
||||||
<AccordionTrigger>Does it track me?</AccordionTrigger>
|
<div className="mt-4 flex items-center">
|
||||||
<AccordionContent>
|
<Checkmark />
|
||||||
<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.
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
</AccordionContent>
|
Fast profile switcher
|
||||||
</AccordionItem>
|
</p>
|
||||||
<AccordionItem value="item-3">
|
</div>
|
||||||
<AccordionTrigger>How secure is Zen Browser?</AccordionTrigger>
|
<div className="mt-4 flex items-center">
|
||||||
<AccordionContent>
|
<Checkmark />
|
||||||
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.
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
</AccordionContent>
|
Container Tabs
|
||||||
</AccordionItem>
|
</p>
|
||||||
</Accordion>
|
</div>
|
||||||
</div>
|
<div className="mt-4 flex items-center">
|
||||||
<div className="lg:w-1/2 h-auto rounded-md relative overflow-hidden">
|
<Question />
|
||||||
<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" />
|
<p className="ml-2 text-gray-600 dark:text-gray-300">
|
||||||
{feature == "item-1" && (
|
Tab Groups (Coming Soon)
|
||||||
<div className='w-full h-full absolute top-0 left-0 grid grid-rows-3'>
|
</p>
|
||||||
<div></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">
|
</div>
|
||||||
<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>
|
||||||
</div>
|
<div className="mx-2 h-[1px] border-t lg:h-[unset] lg:w-[1px] lg:border-l lg:border-t-0"></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]">
|
<div className="flex flex-col p-16 lg:w-1/2">
|
||||||
See what version of Firefox Zen uses <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' />
|
<h3 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||||
</a>
|
Security and Privacy is{" "}
|
||||||
</div>
|
<span className="font-bold text-purple-500">important</span> to us{" "}
|
||||||
)}
|
<LockClosedIcon className="inline h-8 w-8"></LockClosedIcon>
|
||||||
{feature == "item-2" && (
|
</h3>
|
||||||
<div className='w-full h-full absolute top-0 left-0 grid grid-rows-3'>
|
<p className="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||||
<div></div>
|
Zen is based on Firefox, ensuring that your browsing experience
|
||||||
<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">
|
prioritizes security and privacy. With advanced tracking protection
|
||||||
<LockClosedIcon className='w-10 h-10' /> <span className='text-4xl mx-4'>+</span> <EyeClosedIcon className='w-10 h-10' />
|
and minimal data collection, Zen keeps your online activity safe and
|
||||||
</div>
|
secure, giving you peace of mind as you explore the web.
|
||||||
<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]">
|
</p>
|
||||||
Learn about Zen's privacy policy <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' />
|
<div className="relative">
|
||||||
</a>
|
<Button
|
||||||
</div>
|
className="mt-8"
|
||||||
)}
|
variant="ghost"
|
||||||
{feature == "item-3" && (
|
onClick={() =>
|
||||||
<div className='w-full h-full absolute top-0 left-0 grid grid-rows-3'>
|
window.open("https://docs.zen-browser.app/security", "_blank")
|
||||||
<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' />
|
Security in Zen{" "}
|
||||||
</div>
|
<ExternalLinkIcon className="ml-4 h-4 w-4 opacity-50" />
|
||||||
<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]">
|
</Button>
|
||||||
See how Zen keeps you safe <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' />
|
<Button
|
||||||
</a>
|
className="mt-8"
|
||||||
</div>
|
variant="ghost"
|
||||||
)}
|
onClick={() => window.open("/privacy-policy", "_blank")}
|
||||||
</div>
|
>
|
||||||
</div>
|
Your Privacy{" "}
|
||||||
<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'>
|
<ExternalLinkIcon className="ml-4 h-4 w-4 opacity-50" />
|
||||||
<div className="flex p-16 lg:w-1/2 flex-col justify-center">
|
</Button>
|
||||||
<h3 className='text-4xl font-medium text-gray-800 dark:text-gray-100'>Convinced?</h3>
|
</div>
|
||||||
<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>
|
||||||
<div className="relative">
|
</div>
|
||||||
<Button className='mt-8' onClick={() => window.open('/download', '_self')}>Download Now</Button>
|
<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>
|
<div className="flex flex-col justify-center p-16 lg:w-1/2">
|
||||||
</div>
|
<h1 className="text-4xl font-medium text-gray-800 dark:text-gray-100">
|
||||||
<div className="border-t lg:border-t-0 lg:border-l h-[1px] lg:h-[unset] lg:w-[1px] mx-2"></div>
|
Sidebar <SidebarIcon className="ml-1 inline h-8 w-8"></SidebarIcon>
|
||||||
<div className="flex p-16 lg:w-1/2 flex-col justify-center">
|
</h1>
|
||||||
<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="mt-4 text-lg text-gray-600 dark:text-gray-300">
|
||||||
<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>
|
Zen Browser has a built-in sidebar that lets you quickly access your
|
||||||
<div className="relative mt-8 flex">
|
favorite websites, bookmarks, and more. It's the perfect way to stay
|
||||||
<Button variant="ghost" onClick={() => window.open('https://patreon.com/zen_browser', '_blank')}>Patreon <ExternalLinkIcon className='opacity-50 h-4 w-4 ml-4' /></Button>
|
organized.
|
||||||
<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>
|
</p>
|
||||||
</div>
|
<div className="mt-8 w-full">
|
||||||
</div>
|
<div className="flex items-center">
|
||||||
</div>
|
<Checkmark />
|
||||||
</section>
|
<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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Logo from "./logo";
|
import Logo from "./logo";
|
||||||
import TextReveal from "./ui/text-reveal";
|
import TextReveal from "./ui/text-reveal";
|
||||||
@@ -7,108 +6,103 @@ import { MastodonLogo } from "./icons/mastodon";
|
|||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
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="border-grey align-center mt-10 flex w-full flex-col border-t px-10 py-10 font-medium md:px-0">
|
||||||
<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="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">
|
<div className="flex flex-col">
|
||||||
<Logo />
|
<Logo />
|
||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
<h1 className="text-2xl font-bold opacity-80">Zen Browser</h1>
|
<h1 className="text-2xl font-bold opacity-80">Zen Browser</h1>
|
||||||
<h2 className="text-md font-bold opacity-80 mt-6">Follow Us</h2>
|
<h2 className="text-md mt-6 font-bold opacity-80">Follow Us</h2>
|
||||||
<div className="flex mt-4 opacity-70">
|
<div className="mt-4 flex opacity-70">
|
||||||
<a href="https://github.com/zen-browser">
|
<a href="https://github.com/zen-browser">
|
||||||
<GitHubLogoIcon className="w-5 h-5" />
|
<GitHubLogoIcon className="h-5 w-5" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/zen-browser" className="ml-5">
|
<a href="https://discord.gg/zen-browser" className="ml-5">
|
||||||
<DiscordLogoIcon className="w-5 h-5" />
|
<DiscordLogoIcon className="h-5 w-5" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://fosstodon.org/@zenbrowser" className="ml-5">
|
<a href="https://fosstodon.org/@zenbrowser" className="ml-5">
|
||||||
<MastodonLogo className="w-5 h-5" />
|
<MastodonLogo className="h-5 w-5" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col md:flex-row">
|
<div className="flex flex-col md:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-md font-bold opacity-80">Get Started</h2>
|
<h2 className="text-md font-bold opacity-80">Get Started</h2>
|
||||||
<ul className="mt-4 opacity-70 font-normal">
|
<ul className="mt-4 font-normal opacity-70">
|
||||||
<li>
|
<li>
|
||||||
<a href="/themes">
|
<a href="/themes">Themes</a>
|
||||||
Themes
|
</li>
|
||||||
</a>
|
<li className="mt-2">
|
||||||
</li>
|
<a href="/download">Download</a>
|
||||||
<li className="mt-2">
|
</li>
|
||||||
<a href="/download">
|
<li className="mt-2">
|
||||||
Download
|
<a href="/create-theme">Create a Theme</a>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li className="mt-2">
|
</div>
|
||||||
<a href="/create-theme">
|
<div className="mt-10 md:ml-12 md:mt-0 lg:ml-24">
|
||||||
Create a Theme
|
<h2 className="text-md font-bold opacity-80">Get Help</h2>
|
||||||
</a>
|
<ul className="mt-4 font-normal opacity-70">
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<a href="https://discord.com/servers/mauro-s-little-sweatshop-1088172780480114748">
|
||||||
</div>
|
Discord
|
||||||
<div className="mt-10 md:mt-0 md:ml-12 lg:ml-24">
|
</a>
|
||||||
<h2 className="text-md font-bold opacity-80">Get Help</h2>
|
</li>
|
||||||
<ul className="mt-4 opacity-70 font-normal">
|
<li className="mt-2 font-normal">
|
||||||
<li>
|
<a href="https://github.com/zen-browser/desktop/issues">
|
||||||
<a href="https://discord.com/servers/mauro-s-little-sweatshop-1088172780480114748">
|
Report an Issue
|
||||||
Discord
|
</a>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
<li className="mt-2 font-normal">
|
<h2 className="text-md mt-8 font-bold opacity-80">About</h2>
|
||||||
<a href="https://github.com/zen-browser/desktop/issues">
|
<ul className="mt-4 font-normal opacity-70">
|
||||||
Report an Issue
|
<li className="mt-2">
|
||||||
</a>
|
<a href="/about">About Us</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
<li className="mt-2">
|
||||||
<h2 className="text-md font-bold opacity-80 mt-8">About</h2>
|
<a href="/privacy-policy">Privacy Policy</a>
|
||||||
<ul className="mt-4 opacity-70 font-normal">
|
</li>
|
||||||
<li className="mt-2">
|
</ul>
|
||||||
<a href="/about">
|
</div>
|
||||||
About Us
|
<div className="mt-10 md:ml-12 md:mt-0 lg:ml-24">
|
||||||
</a>
|
<h2 className="text-md font-bold opacity-80">Resources</h2>
|
||||||
</li>
|
<ul className="mt-4 font-normal opacity-70">
|
||||||
<li className="mt-2">
|
<li>
|
||||||
<a href="/privacy-policy">Privacy Policy</a>
|
<a href="/branding-assets">Branding Assets</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
<li className="mt-2">
|
||||||
</div>
|
<a href="https://github.com/zen-browser/desktop">Source Code</a>
|
||||||
<div className="mt-10 md:mt-0 md:ml-12 lg:ml-24">
|
</li>
|
||||||
<h2 className="text-md font-bold opacity-80">Resources</h2>
|
<li className="mt-2">
|
||||||
<ul className="mt-4 opacity-70 font-normal">
|
<a href="https://docs.zen-browser.app">Documentation</a>
|
||||||
<li>
|
</li>
|
||||||
<a href="/branding-assets">Branding Assets</a>
|
<li className="mt-2">
|
||||||
</li>
|
<a href="/release-notes">Release Notes</a>
|
||||||
<li className="mt-2">
|
</li>
|
||||||
<a href="https://github.com/zen-browser/desktop">Source Code</a>
|
</ul>
|
||||||
</li>
|
<h2 className="text-md mt-8 font-bold opacity-80">Support Us</h2>
|
||||||
<li className="mt-2">
|
<ul className="mt-4 font-normal opacity-70">
|
||||||
<a href="https://docs.zen-browser.app">Documentation</a>
|
<li>
|
||||||
</li>
|
<a href="https://patreon.com/zen_browser">Patreon</a>
|
||||||
<li className="mt-2">
|
</li>
|
||||||
<a href="/release-notes">Release Notes</a>
|
<li className="mt-2">
|
||||||
</li>
|
<a href="https://ko-fi.com/zen_browser">Ko-fi</a>
|
||||||
</ul>
|
</li>
|
||||||
<h2 className="text-md font-bold opacity-80 mt-8">Support Us</h2>
|
</ul>
|
||||||
<ul className="mt-4 opacity-70 font-normal">
|
</div>
|
||||||
<li>
|
</div>
|
||||||
<a href="https://patreon.com/zen_browser">Patreon</a>
|
</div>
|
||||||
</li>
|
<div className="mx-auto flex w-full items-center pl-3 pr-5 pt-10 lg:!w-2/3">
|
||||||
<li className="mt-2">
|
<p className="text-xs font-normal opacity-30">
|
||||||
<a href="https://ko-fi.com/zen_browser">Ko-fi</a>
|
Crafted with ❤️ by the community - Copyright ©{" "}
|
||||||
</li>
|
{new Date().getFullYear()} Zen Browser
|
||||||
</ul>
|
</p>
|
||||||
</div>
|
<a href="/download" className="ml-auto">
|
||||||
</div>
|
<Button className="ml-auto">Download</Button>
|
||||||
</div>
|
</a>
|
||||||
<div className="flex w-full pt-10 pr-5 pl-3 mx-auto lg:!w-2/3 items-center">
|
</div>
|
||||||
<p className="text-xs font-normal opacity-30">Crafted with ❤️ by the community - Copyright © {new Date().getFullYear()} Zen Browser</p>
|
</div>
|
||||||
<a href="/download" className="ml-auto">
|
);
|
||||||
<Button className="ml-auto">Download</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,53 +11,41 @@ import { ChevronDown, ChevronRight } from "lucide-react";
|
|||||||
import Particles from "./ui/particles";
|
import Particles from "./ui/particles";
|
||||||
import Image from "next/legacy/image";
|
import Image from "next/legacy/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import CoolHeaderText from "./cool-header-text";
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const inView = useInView(ref, { once: true, margin: "-100px" });
|
const inView = useInView(ref, { once: true, margin: "-100px" });
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section
|
<section
|
||||||
id="hero"
|
id="hero"
|
||||||
className="relative mx-auto min-h-screen flex justify-center flex-col max-w-7xl px-6 text-center md:px-8"
|
className="relative mx-auto flex min-h-screen max-w-7xl flex-col justify-center px-6 text-center md:px-8"
|
||||||
>
|
>
|
||||||
<a href="/download">
|
<div className="relative">
|
||||||
<AnimatedGradientText>
|
<CoolHeaderText />
|
||||||
🎉 <hr className="mx-2 h-4 w-[1px] shrink-0 bg-gray-300" />{" "}
|
</div>
|
||||||
<span
|
<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">
|
||||||
className={ny(
|
Beautifully designed, privacy-focused, and packed with features.
|
||||||
`inline animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`
|
<br className="hidden md:block" /> We care about your experience, not
|
||||||
)}
|
your data.
|
||||||
>
|
</p>
|
||||||
Introducing Zen Alpha
|
<div className="flex w-full flex-col justify-center md:flex-row">
|
||||||
</span>
|
<a href="/download">
|
||||||
<ChevronRight className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
<Button className="-translate-y-4 animate-fade-in gap-1 text-white opacity-0 ease-in-out [--animation-delay:600ms] dark:text-black">
|
||||||
</AnimatedGradientText>
|
<span>Download Zen Now </span>
|
||||||
</a>
|
<ArrowRightIcon className="ml-1 size-4 transition-transform duration-300 ease-in-out group-hover:translate-x-1" />
|
||||||
<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">
|
</Button>
|
||||||
Zen is the best way
|
</a>
|
||||||
<br className="hidden md:block" /> to browse the web.
|
<a
|
||||||
</h1>
|
href="#features"
|
||||||
<p className="animate-fade-in mb-12 -translate-y-4 text-balance text-lg tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms] md:text-xl">
|
className="-translate-y-4 animate-fade-up opacity-0 [--animation-delay:800ms]"
|
||||||
Beautifully designed, privacy-focused, and packed with features.
|
>
|
||||||
<br className="hidden md:block" /> We care about your experience, not
|
<Button variant="ghost" className="mt-4 md:ml-4 md:mt-0">
|
||||||
your data.
|
Start Exploring <ChevronDown className="ml-1 size-4" />
|
||||||
</p>
|
</Button>
|
||||||
<div className="flex flex-col md:flex-row justify-center w-full">
|
</a>
|
||||||
<a href="/download">
|
</div>
|
||||||
<Button
|
</section>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
export const MastodonLogo = (props: any) => (
|
export const MastodonLogo = (props: any) => (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256" {...props}>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<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>
|
width="1em"
|
||||||
|
height="1em"
|
||||||
</svg>
|
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>
|
||||||
|
);
|
||||||
|
|||||||
@@ -6,10 +6,19 @@ import React from "react";
|
|||||||
import CachedImage from "./CachedImage";
|
import CachedImage from "./CachedImage";
|
||||||
|
|
||||||
export default function Logo({ withText, ...props }: any) {
|
export default function Logo({ withText, ...props }: any) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center m-0" {...props}>
|
<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")} />
|
<CachedImage
|
||||||
{withText && <span className="text-2xl font-bold ml-2">zen</span>}
|
src={`www/public/logos/zen-black.svg`}
|
||||||
</div>
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,28 @@ import { getAllThemes, getThemesFromSearch, ZenTheme } from "@/lib/themes";
|
|||||||
import ThemeCard from "./theme-card";
|
import ThemeCard from "./theme-card";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
export default function MarketplacePage({ themes }: {themes:ZenTheme[]}) {
|
export default function MarketplacePage({ themes }: { themes: ZenTheme[] }) {
|
||||||
const [searchInput, setSearchInput] = React.useState("");
|
const [searchInput, setSearchInput] = React.useState("");
|
||||||
const [tags, setTags] = React.useState<string[]>(["all"]);
|
const [tags, setTags] = React.useState<string[]>(["all"]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full mx-auto items-center justify-center h-full">
|
<div className="mx-auto flex h-full w-full flex-col items-center justify-center">
|
||||||
<div className="mx-auto w-full text-center border-b pt-48 pb-24 mb-12 bg-surface dark:bg-[#121212]">
|
<div className="mx-auto mb-12 w-full border-b bg-surface pb-24 pt-48 text-center dark:bg-[#121212]">
|
||||||
<div className="w-full lg:w-1/2 xl:w-1/2 mx-auto px-2 lg:px-none">
|
<div className="lg:px-none mx-auto w-full px-2 lg:w-1/2 xl:w-1/2">
|
||||||
<h1 className="text-4xl lg:text-7xl font-bold">Themes Store</h1>
|
<h1 className="text-4xl font-bold lg:text-7xl">Themes Store</h1>
|
||||||
<ThemesSearch input={searchInput} setInput={setSearchInput} tags={tags} setTags={setTags} />
|
<ThemesSearch
|
||||||
</div>
|
input={searchInput}
|
||||||
</div>
|
setInput={setSearchInput}
|
||||||
<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">
|
tags={tags}
|
||||||
{getThemesFromSearch(themes, searchInput, tags).map((theme) => (
|
setTags={setTags}
|
||||||
<ThemeCard key={theme.name} theme={theme} />
|
/>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,120 +1,107 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { SidebarOpen } from 'lucide-react'
|
import { SidebarOpen } from "lucide-react";
|
||||||
import type { LinkProps } from 'next/link'
|
import type { LinkProps } from "next/link";
|
||||||
import Link from 'next/link'
|
import Link from "next/link";
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from "next/navigation";
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import { Sheet, SheetContent, SheetTrigger } from './ui/sheet'
|
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet";
|
||||||
import { Button } from './ui/button'
|
import { Button } from "./ui/button";
|
||||||
import { ScrollArea } from './ui/scroll-area'
|
import { ScrollArea } from "./ui/scroll-area";
|
||||||
import Logo from './logo'
|
import Logo from "./logo";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
import { components } from './navigation'
|
import { components } from "./navigation";
|
||||||
|
|
||||||
export function MobileNav() {
|
export function MobileNav() {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<div className="z-40 flex w-full items-center space-between sm:hidden">
|
<div className="space-between z-40 flex w-full items-center sm:hidden">
|
||||||
<Logo className="size-6 ml-4" />
|
<Logo className="ml-4 size-6" />
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="mr-2 px-0 ml-auto text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0"
|
className="ml-auto mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
>
|
>
|
||||||
<SidebarOpen className="size-6" />
|
<SidebarOpen className="size-6" />
|
||||||
<span className="sr-only">Toggle Menu</span>
|
<span className="sr-only">Toggle Menu</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="left" className="pr-0">
|
<SheetContent side="left" className="pr-0">
|
||||||
<MobileLink
|
<MobileLink
|
||||||
href="/"
|
href="/"
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
>
|
>
|
||||||
<Logo withText />
|
<Logo withText />
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
<ScrollArea className="mt-4 h-[calc(100vh-8rem)] pl-6">
|
<ScrollArea className="mt-4 h-[calc(100vh-8rem)] pl-6">
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<MobileLink
|
<MobileLink href="/download" onOpenChange={setOpen}>
|
||||||
href="/download"
|
<div>Download</div>
|
||||||
onOpenChange={setOpen}
|
<p className="text-xs opacity-60">
|
||||||
>
|
Get the latest version of Zen Browser.
|
||||||
<div>Download</div>
|
</p>
|
||||||
<p className="opacity-60 text-xs">
|
</MobileLink>
|
||||||
Get the latest version of Zen Browser.
|
<MobileLink href="/themes" onOpenChange={setOpen}>
|
||||||
</p>
|
<div>Theme Store</div>
|
||||||
</MobileLink>
|
<p className="text-xs opacity-60">
|
||||||
<MobileLink
|
Customize your browsing experience.
|
||||||
href="/themes"
|
</p>
|
||||||
onOpenChange={setOpen}
|
</MobileLink>
|
||||||
>
|
<MobileLink href="/release-notes" onOpenChange={setOpen}>
|
||||||
<div>Theme Store</div>
|
<div>Release Notes</div>
|
||||||
<p className="opacity-60 text-xs">
|
<p className="text-xs opacity-60">
|
||||||
Customize your browsing experience.
|
Stay up to date with the latest changes.
|
||||||
</p>
|
</p>
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
<MobileLink
|
<MobileLink
|
||||||
href="/release-notes"
|
href="https://patreon.com/zen_browser?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
|
||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
>
|
>
|
||||||
<div>Release Notes</div>
|
<div>Donate {"<"}3</div>
|
||||||
<p className="opacity-60 text-xs">
|
<p className="text-xs opacity-60">Support the project</p>
|
||||||
Stay up to date with the latest changes.
|
</MobileLink>
|
||||||
</p>
|
{components.map(({ title, href, description }) => (
|
||||||
</MobileLink>
|
<MobileLink href={href} key={href} onOpenChange={setOpen}>
|
||||||
<MobileLink
|
<div>{title}</div>
|
||||||
href="https://patreon.com/zen_browser?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
|
<p className="text-xs opacity-60">{description}</p>
|
||||||
onOpenChange={setOpen}
|
</MobileLink>
|
||||||
>
|
))}
|
||||||
<div>Donate {"<"}3</div>
|
</div>
|
||||||
<p className="opacity-60 text-xs">Support the project</p>
|
</ScrollArea>
|
||||||
</MobileLink>
|
</SheetContent>
|
||||||
{components.map(({title, href, description}) => (
|
</Sheet>
|
||||||
<MobileLink
|
);
|
||||||
href={href}
|
|
||||||
key={href}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
>
|
|
||||||
<div>{title}</div>
|
|
||||||
<p className='opacity-60 text-xs'>{description}</p>
|
|
||||||
</MobileLink>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MobileLinkProps extends LinkProps {
|
interface MobileLinkProps extends LinkProps {
|
||||||
onOpenChange?: (open: boolean) => void
|
onOpenChange?: (open: boolean) => void;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
className?: string
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MobileLink({
|
function MobileLink({
|
||||||
href,
|
href,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: MobileLinkProps) {
|
}: MobileLinkProps) {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={href.toString()}
|
href={href.toString()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(href.toString())
|
router.push(href.toString());
|
||||||
onOpenChange?.(false)
|
onOpenChange?.(false);
|
||||||
}}
|
}}
|
||||||
className={ny(className, "my-4")}
|
className={ny(className, "my-4")}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,36 +5,36 @@ import { useTheme } from "next-themes";
|
|||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
const [currentTheme, setCurrentTheme] = useState('light');
|
const [currentTheme, setCurrentTheme] = useState("light");
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
const savedTheme = localStorage.getItem("theme") || "light";
|
||||||
setCurrentTheme(savedTheme);
|
setCurrentTheme(savedTheme);
|
||||||
setTheme(savedTheme);
|
setTheme(savedTheme);
|
||||||
}, [setTheme]);
|
}, [setTheme]);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
const newTheme = currentTheme === "light" ? "dark" : "light";
|
||||||
setCurrentTheme(newTheme);
|
setCurrentTheme(newTheme);
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
localStorage.setItem('theme', newTheme);
|
localStorage.setItem("theme", newTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant="ghost" size="icon" onClick={toggleTheme}>
|
<Button variant="ghost" size="icon" onClick={toggleTheme}>
|
||||||
{currentTheme === 'light' ? (
|
{currentTheme === "light" ? (
|
||||||
<SunIcon className="h-[1.2rem] w-[1.2rem]" />
|
<SunIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||||
) : (
|
) : (
|
||||||
<MoonIcon className="h-[1.2rem] w-[1.2rem]" />
|
<MoonIcon className="h-[1.2rem] w-[1.2rem]" />
|
||||||
)}
|
)}
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,167 +1,176 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import * as React from "react"
|
import { ny } from "@/lib/utils";
|
||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
import { ny } from "@/lib/utils"
|
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
NavigationMenuContent,
|
NavigationMenuContent,
|
||||||
NavigationMenuItem,
|
NavigationMenuItem,
|
||||||
NavigationMenuLink,
|
NavigationMenuLink,
|
||||||
NavigationMenuList,
|
NavigationMenuList,
|
||||||
NavigationMenuTrigger,
|
NavigationMenuTrigger,
|
||||||
navigationMenuTriggerStyle,
|
navigationMenuTriggerStyle,
|
||||||
} from "@/components/ui/navigation-menu"
|
} from "@/components/ui/navigation-menu";
|
||||||
import Logo from "./logo"
|
import Logo from "./logo";
|
||||||
import { ModeToggle } from "./mode-toggle"
|
import { ModeToggle } from "./mode-toggle";
|
||||||
import { MobileNav } from "./mobile-nav"
|
import { MobileNav } from "./mobile-nav";
|
||||||
import { HeartIcon } from "lucide-react"
|
import { HeartIcon } from "lucide-react";
|
||||||
import { HeartFilledIcon } from "@radix-ui/react-icons"
|
import { HeartFilledIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
export const components: { title: string; href: string; description: string }[] = [
|
export const components: {
|
||||||
{
|
title: string;
|
||||||
title: "Privacy Policy",
|
href: string;
|
||||||
href: "/privacy-policy",
|
description: string;
|
||||||
description: "Read our privacy policy to learn more about how we handle your data."
|
}[] = [
|
||||||
},
|
{
|
||||||
{
|
title: "Privacy Policy",
|
||||||
title: "Discord",
|
href: "/privacy-policy",
|
||||||
href: "https://discord.gg/zen-browser",
|
description:
|
||||||
description: "Join our Discord server to chat with the community and get support."
|
"Read our privacy policy to learn more about how we handle your data.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Source Code",
|
title: "Discord",
|
||||||
href: "https://github.com/zen-browser",
|
href: "https://discord.gg/zen-browser",
|
||||||
description: "View the source code on GitHub and contribute to the project."
|
description:
|
||||||
},
|
"Join our Discord server to chat with the community and get support.",
|
||||||
{
|
},
|
||||||
title: "Branding Assets",
|
{
|
||||||
href: "/branding-assets",
|
title: "Source Code",
|
||||||
description: "Download our branding assets to use in your projects."
|
href: "https://github.com/zen-browser",
|
||||||
},
|
description:
|
||||||
{
|
"View the source code on GitHub and contribute to the project.",
|
||||||
title: "About",
|
},
|
||||||
href: "/about",
|
{
|
||||||
description: "Learn more about the Zen Browser project and the team behind it."
|
title: "Branding Assets",
|
||||||
},
|
href: "/branding-assets",
|
||||||
{
|
description: "Download our branding assets to use in your projects.",
|
||||||
title: "Documentation",
|
},
|
||||||
href: "https://docs.zen-browser.app/",
|
{
|
||||||
description: "Read the documentation to learn more about Zen Browser."
|
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() {
|
export function Navigation() {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background z-40 top-0 left-0 w-full flex fixed border-b border-grey p-2 items-center justify-center">
|
<div className="border-grey fixed left-0 top-0 z-40 flex w-full items-center justify-center border-b bg-background p-2">
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
<NavigationMenu>
|
<NavigationMenu>
|
||||||
<NavigationMenuList className="w-full hidden py-3 sm:flex">
|
<NavigationMenuList className="hidden w-full py-3 sm:flex">
|
||||||
<NavigationMenuItem className="cursor-pointer mr-20">
|
<NavigationMenuItem className="mr-20 cursor-pointer">
|
||||||
<NavigationMenuLink href="/">
|
<NavigationMenuLink href="/">
|
||||||
<Logo withText />
|
<Logo withText />
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuTrigger>Getting Started</NavigationMenuTrigger>
|
<NavigationMenuTrigger>Getting Started</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
|
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
|
||||||
<li className="row-span-3">
|
<li className="row-span-3">
|
||||||
<NavigationMenuLink asChild>
|
<NavigationMenuLink asChild>
|
||||||
<a
|
<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"
|
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="/"
|
href="/"
|
||||||
>
|
>
|
||||||
<Logo />
|
<Logo />
|
||||||
<div className="mb-2 mt-4 text-lg font-medium">
|
<div className="mb-2 mt-4 text-lg font-medium">
|
||||||
Zen Browser
|
Zen Browser
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm leading-tight text-muted-foreground">
|
<p className="text-sm leading-tight text-muted-foreground">
|
||||||
Firefox based browser with a focus on privacy and
|
Firefox based browser with a focus on privacy and
|
||||||
customization.
|
customization.
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
</li>
|
</li>
|
||||||
<ListItem href="/download" title="Download">
|
<ListItem href="/download" title="Download">
|
||||||
Start using Zen Browser today with just a few clicks.
|
Start using Zen Browser today with just a few clicks.
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem href="/themes" title="Themes Store">
|
<ListItem href="/themes" title="Themes Store">
|
||||||
Customize your browser with a variety of themes!
|
Customize your browser with a variety of themes!
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem href="/release-notes" title="Release Notes">
|
<ListItem href="/release-notes" title="Release Notes">
|
||||||
Stay up to date with the latest changes.
|
Stay up to date with the latest changes.
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</ul>
|
</ul>
|
||||||
</NavigationMenuContent>
|
</NavigationMenuContent>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
<NavigationMenuTrigger>
|
<NavigationMenuTrigger>
|
||||||
<HeartFilledIcon className="text-red-500" />
|
<HeartFilledIcon className="text-red-500" />
|
||||||
<span className="ml-2">Donate</span>
|
<span className="ml-2">Donate</span>
|
||||||
</NavigationMenuTrigger>
|
</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
|
||||||
<ListItem
|
<ListItem
|
||||||
title="Patreon"
|
title="Patreon"
|
||||||
href="https://patreon.com/zen_browser?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
|
href="https://patreon.com/zen_browser?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
|
||||||
>
|
>
|
||||||
Support us on Patreon and get exclusive rewards and keep the project alive.
|
Support us on Patreon and get exclusive rewards and keep the
|
||||||
</ListItem>
|
project alive.
|
||||||
<ListItem
|
</ListItem>
|
||||||
title="Ko-Fi"
|
<ListItem
|
||||||
href="https://ko-fi.com/zen_browser?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink"
|
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>
|
Ko-fi is a way to support us with a one-time donation and help
|
||||||
</ul>
|
us keep the project alive.
|
||||||
</NavigationMenuContent>
|
</ListItem>
|
||||||
</NavigationMenuItem>
|
</ul>
|
||||||
<NavigationMenuItem>
|
</NavigationMenuContent>
|
||||||
<NavigationMenuTrigger>{"Useful Links"}</NavigationMenuTrigger>
|
</NavigationMenuItem>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuItem>
|
||||||
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
<NavigationMenuTrigger>{"Useful Links"}</NavigationMenuTrigger>
|
||||||
{components.map((component) => (
|
<NavigationMenuContent>
|
||||||
<ListItem
|
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
|
||||||
key={component.title}
|
{components.map((component) => (
|
||||||
title={component.title}
|
<ListItem
|
||||||
href={component.href}
|
key={component.title}
|
||||||
>
|
title={component.title}
|
||||||
{component.description}
|
href={component.href}
|
||||||
</ListItem>
|
>
|
||||||
))}
|
{component.description}
|
||||||
</ul>
|
</ListItem>
|
||||||
</NavigationMenuContent>
|
))}
|
||||||
</NavigationMenuItem>
|
</ul>
|
||||||
<ModeToggle />
|
</NavigationMenuContent>
|
||||||
</NavigationMenuList>
|
</NavigationMenuItem>
|
||||||
</NavigationMenu>
|
<ModeToggle />
|
||||||
</div>
|
</NavigationMenuList>
|
||||||
)
|
</NavigationMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListItem = React.forwardRef<
|
const ListItem = React.forwardRef<
|
||||||
React.ElementRef<"a">,
|
React.ElementRef<"a">,
|
||||||
React.ComponentPropsWithoutRef<"a">
|
React.ComponentPropsWithoutRef<"a">
|
||||||
>(({ className, title, children, ...props }, ref) => {
|
>(({ className, title, children, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<NavigationMenuLink asChild>
|
<NavigationMenuLink asChild>
|
||||||
<a
|
<a
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
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",
|
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="text-sm font-medium leading-none">{title}</div>
|
<div className="text-sm font-medium leading-none">{title}</div>
|
||||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
ListItem.displayName = "ListItem"
|
ListItem.displayName = "ListItem";
|
||||||
|
|||||||
@@ -10,113 +10,124 @@ import { AccordionContent, AccordionTrigger } from "./ui/accordion";
|
|||||||
import { ny } from "@/lib/utils";
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
export default function ReleaseNoteElement({ data }: { data: ReleaseNote }) {
|
export default function ReleaseNoteElement({ data }: { data: ReleaseNote }) {
|
||||||
const splitDate = data.date.split("/");
|
const splitDate = data.date.split("/");
|
||||||
return (
|
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}>
|
<section
|
||||||
<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}>
|
className={ny(
|
||||||
{moment({
|
"relative flex flex-col border-t lg:flex-row",
|
||||||
year: parseInt(splitDate[2]),
|
data.version == releaseNotes[0].version ? "mt-24 pt-24" : "mt-36 pt-36",
|
||||||
month: parseInt(splitDate[1]) - 1,
|
)}
|
||||||
day: parseInt(splitDate[0]),
|
id={data.version}
|
||||||
}).format('MMMM Do, YYYY')}
|
>
|
||||||
</StickyBox>
|
<StickyBox
|
||||||
<div className="px-5 md:px-10 md:px-0 md:pr-32">
|
className="mb-6 ml-10 mr-24 mt-1 h-fit min-w-52 text-xs text-muted-foreground lg:mb-0"
|
||||||
<h1 className="text-3xl font-bold">
|
offsetTop={120}
|
||||||
Release notes for {data.version} 🎉
|
>
|
||||||
</h1>
|
{moment({
|
||||||
<p className="text-md mt-4 text-muted-foreground">
|
year: parseInt(splitDate[2]),
|
||||||
If you encounter any issues, please report them on{" "}
|
month: parseInt(splitDate[1]) - 1,
|
||||||
<a
|
day: parseInt(splitDate[0]),
|
||||||
href="https://github.com/zen-browser/desktop/issues/"
|
}).format("MMMM Do, YYYY")}
|
||||||
className="text-underline text-blue-500"
|
</StickyBox>
|
||||||
>
|
<div className="px-5 md:px-0 md:px-10 md:pr-32">
|
||||||
the issues page
|
<h1 className="text-3xl font-bold">
|
||||||
</a>
|
Release notes for {data.version} 🎉
|
||||||
. Thanks everyone for your feedback! ❤️
|
</h1>
|
||||||
</p>
|
<p className="text-md mt-4 text-muted-foreground">
|
||||||
{data.extra && (
|
If you encounter any issues, please report them on{" "}
|
||||||
<p
|
<a
|
||||||
className="text-md text-muted-foreground mt-8"
|
href="https://github.com/zen-browser/desktop/issues/"
|
||||||
dangerouslySetInnerHTML={{
|
className="text-underline text-blue-500"
|
||||||
__html: data.extra.replace(/(\n)/g, "<br />"),
|
>
|
||||||
}}
|
the issues page
|
||||||
></p>
|
</a>
|
||||||
)}
|
. Thanks everyone for your feedback! ❤️
|
||||||
<Accordion type="single" collapsible className="mt-8">
|
</p>
|
||||||
{data.breakingChanges && (
|
{data.extra && (
|
||||||
<AccordionItem value="breaking-changes" title="Breaking Changes">
|
<p
|
||||||
<AccordionTrigger className="border-b">
|
className="text-md mt-8 text-muted-foreground"
|
||||||
<div className="flex items-center">
|
dangerouslySetInnerHTML={{
|
||||||
<ExclamationTriangleIcon className="text-red-500 mr-2 mt-1 size-5 opacity-50" />
|
__html: data.extra.replace(/(\n)/g, "<br />"),
|
||||||
<span className="ml-2">Breaking Changes</span>
|
}}
|
||||||
</div>
|
></p>
|
||||||
</AccordionTrigger>
|
)}
|
||||||
<AccordionContent>
|
<Accordion type="single" collapsible className="mt-8">
|
||||||
<ul className="ml-6" style={{ listStyleType: "initial" }}>
|
{data.breakingChanges && (
|
||||||
{data.breakingChanges.map((change) => (
|
<AccordionItem value="breaking-changes" title="Breaking Changes">
|
||||||
<li key={change} className="mt-4 text-md text-muted-foreground">
|
<AccordionTrigger className="border-b">
|
||||||
<span className="ml-1">
|
<div className="flex items-center">
|
||||||
{change}
|
<ExclamationTriangleIcon className="mr-2 mt-1 size-5 text-red-500 opacity-50" />
|
||||||
</span>
|
<span className="ml-2">Breaking Changes</span>
|
||||||
</li>
|
</div>
|
||||||
))}
|
</AccordionTrigger>
|
||||||
</ul>
|
<AccordionContent>
|
||||||
</AccordionContent>
|
<ul className="ml-6" style={{ listStyleType: "initial" }}>
|
||||||
</AccordionItem>
|
{data.breakingChanges.map((change) => (
|
||||||
)
|
<li
|
||||||
}
|
key={change}
|
||||||
{data.fixes && (
|
className="text-md mt-4 text-muted-foreground"
|
||||||
<AccordionItem value="fixes" title="Fixes">
|
>
|
||||||
<AccordionTrigger className="border-b">
|
<span className="ml-1">{change}</span>
|
||||||
<div className="flex items-center">
|
</li>
|
||||||
<CheckCheckIcon className="mr-2 mt-1 size-5 opacity-50" />
|
))}
|
||||||
<span className="ml-2">Fixes</span>
|
</ul>
|
||||||
</div>
|
</AccordionContent>
|
||||||
</AccordionTrigger>
|
</AccordionItem>
|
||||||
<AccordionContent>
|
)}
|
||||||
<ul className="ml-6" style={{ listStyleType: "initial" }}>
|
{data.fixes && (
|
||||||
{data.fixes.map((fix) => (
|
<AccordionItem value="fixes" title="Fixes">
|
||||||
<li key={fix.description} className="mt-4 text-md text-muted-foreground">
|
<AccordionTrigger className="border-b">
|
||||||
<span className="ml-1">
|
<div className="flex items-center">
|
||||||
{fix.description}
|
<CheckCheckIcon className="mr-2 mt-1 size-5 opacity-50" />
|
||||||
</span>
|
<span className="ml-2">Fixes</span>
|
||||||
{fix.issue && (
|
</div>
|
||||||
<a
|
</AccordionTrigger>
|
||||||
href={`https://github.com/zen-browser/desktop/issues/${fix.issue}`}
|
<AccordionContent>
|
||||||
className="text-blue-500 ml-1 text-underline"
|
<ul className="ml-6" style={{ listStyleType: "initial" }}>
|
||||||
>
|
{data.fixes.map((fix) => (
|
||||||
#{fix.issue}
|
<li
|
||||||
</a>
|
key={fix.description}
|
||||||
)}
|
className="text-md mt-4 text-muted-foreground"
|
||||||
</li>
|
>
|
||||||
))}
|
<span className="ml-1">{fix.description}</span>
|
||||||
</ul>
|
{fix.issue && (
|
||||||
</AccordionContent>
|
<a
|
||||||
</AccordionItem>
|
href={`https://github.com/zen-browser/desktop/issues/${fix.issue}`}
|
||||||
)}
|
className="text-underline ml-1 text-blue-500"
|
||||||
{data.features && (
|
>
|
||||||
<AccordionItem value="features" title="Features">
|
#{fix.issue}
|
||||||
<AccordionTrigger>
|
</a>
|
||||||
<div className="flex items-center">
|
)}
|
||||||
<StarIcon className="text-yellow-700 mr-2 mt-1 size-5 opacity-50" />
|
</li>
|
||||||
<span className="ml-2">Features</span>
|
))}
|
||||||
</div>
|
</ul>
|
||||||
</AccordionTrigger>
|
</AccordionContent>
|
||||||
<AccordionContent>
|
</AccordionItem>
|
||||||
<ul className="ml-6" style={{ listStyleType: "initial" }}>
|
)}
|
||||||
{data.features.map((feature) => (
|
{data.features && (
|
||||||
<li key={feature} className="mt-4 text-md text-muted-foreground">
|
<AccordionItem value="features" title="Features">
|
||||||
<span className="ml-1">
|
<AccordionTrigger>
|
||||||
{feature}
|
<div className="flex items-center">
|
||||||
</span>
|
<StarIcon className="mr-2 mt-1 size-5 text-yellow-700 opacity-50" />
|
||||||
</li>
|
<span className="ml-2">Features</span>
|
||||||
))}
|
</div>
|
||||||
</ul>
|
</AccordionTrigger>
|
||||||
</AccordionContent>
|
<AccordionContent>
|
||||||
</AccordionItem>
|
<ul className="ml-6" style={{ listStyleType: "initial" }}>
|
||||||
)}
|
{data.features.map((feature) => (
|
||||||
</Accordion>
|
<li
|
||||||
</div>
|
key={feature}
|
||||||
</section>
|
className="text-md mt-4 text-muted-foreground"
|
||||||
);
|
>
|
||||||
|
<span className="ml-1">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
)}
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,78 @@
|
|||||||
|
|
||||||
import { getThemeAuthorLink, ZenTheme } from "@/lib/themes";
|
import { getThemeAuthorLink, ZenTheme } from "@/lib/themes";
|
||||||
import styled from "styled-components";
|
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";
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
const ThemeCardWrapper = styled.div`
|
const ThemeCardWrapper = styled.div``;
|
||||||
`;
|
|
||||||
|
|
||||||
export default function ThemeCard({
|
export default function ThemeCard({
|
||||||
theme,
|
theme,
|
||||||
className
|
className,
|
||||||
}: {
|
}: {
|
||||||
theme: ZenTheme;
|
theme: ZenTheme;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const maxNameLen = 50;
|
||||||
|
const maxDescLen = 100;
|
||||||
|
|
||||||
const maxNameLen = 50;
|
return (
|
||||||
const maxDescLen = 100;
|
<ThemeCardWrapper
|
||||||
|
onClick={(event) => {
|
||||||
return (
|
if (event.target instanceof HTMLAnchorElement) return;
|
||||||
<ThemeCardWrapper onClick={(event) => {
|
window.open(`/themes/${theme.id}`, "_self");
|
||||||
if (event.target instanceof HTMLAnchorElement) return;
|
}}
|
||||||
window.open(`/themes/${theme.id}`, "_self");
|
className={ny(
|
||||||
}} 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)}>
|
"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",
|
||||||
<img src={theme.image} alt={theme.name} width={500} height={500}
|
className,
|
||||||
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">
|
<img
|
||||||
{theme.homepage && (
|
src={theme.image}
|
||||||
<>
|
alt={theme.name}
|
||||||
<a href={theme.homepage} className="text-blue-500 text-md" target="_blank" rel="noopener noreferrer">
|
width={500}
|
||||||
Homepage
|
height={500}
|
||||||
</a>
|
className="h-32 w-full rounded-lg border object-cover shadow"
|
||||||
<span className="text-muted-foreground text-md mx-2">
|
/>
|
||||||
{"·"}
|
<h2 className="mt-4 overflow-ellipsis text-start text-xl font-bold">
|
||||||
</span>
|
{theme.name.substring(0, maxNameLen).trim() +
|
||||||
</>
|
(theme.name.length > maxNameLen ? "..." : "")}
|
||||||
)}
|
</h2>
|
||||||
<a href={getThemeAuthorLink(theme)} className="text-blue-500 text-md" target="_blank" rel="noopener noreferrer">
|
<div className="mt-2 flex">
|
||||||
Author
|
{theme.homepage && (
|
||||||
</a>
|
<>
|
||||||
</div>
|
<a
|
||||||
<p className="text-md mt-2 overflow-ellipsis text-muted-foreground text-start">
|
href={theme.homepage}
|
||||||
{theme.description.substring(0, maxDescLen).trim() +
|
className="text-md text-blue-500"
|
||||||
(theme.description.length > maxDescLen ? "..." : "")}
|
target="_blank"
|
||||||
</p>
|
rel="noopener noreferrer"
|
||||||
</ThemeCardWrapper>
|
>
|
||||||
);
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +1,100 @@
|
|||||||
|
import {
|
||||||
|
getThemeAuthorLink,
|
||||||
import { getThemeAuthorLink, getThemeFromId, getThemeMarkdown, ZenTheme } from "@/lib/themes";
|
getThemeFromId,
|
||||||
|
getThemeMarkdown,
|
||||||
|
ZenTheme,
|
||||||
|
} from "@/lib/themes";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import '../app/privacy-policy/markdown.css';
|
import "../app/privacy-policy/markdown.css";
|
||||||
import { ChevronLeft, LoaderCircleIcon } from "lucide-react";
|
import { ChevronLeft, LoaderCircleIcon } from "lucide-react";
|
||||||
|
|
||||||
export default async function ThemePage({ themeID }: { themeID: string }) {
|
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);
|
const readme = await getThemeMarkdown(theme);
|
||||||
if (!theme) {
|
|
||||||
return <div>Theme not found</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const readme = await getThemeMarkdown(theme);
|
return (
|
||||||
|
<div className="relative mx-auto mt-24 flex flex-col items-start lg:mt-56 lg:flex-row">
|
||||||
return (
|
<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">
|
||||||
<div className="mt-24 lg:mt-56 flex-col lg:flex-row flex mx-auto items-start relative">
|
<a
|
||||||
<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">
|
className="mb-4 mt-2 flex cursor-pointer items-center opacity-70"
|
||||||
<a className="flex mt-2 mb-4 items-center cursor-pointer opacity-70" href="/themes">
|
href="/themes"
|
||||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
>
|
||||||
<h3 className="text-md">Go back</h3>
|
<ChevronLeft className="mr-1 h-4 w-4" />
|
||||||
</a>
|
<h3 className="text-md">Go back</h3>
|
||||||
<img src={theme.image} alt={theme.name} width={500} height={500} className="w-full object-cover rounded-lg border-2 shadow" />
|
</a>
|
||||||
<h1 className="text-2xl mt-5 font-bold">{theme.name}</h1>
|
<img
|
||||||
<p className="text-sm text-muted-foreground mt-2">{theme.description}</p>
|
src={theme.image}
|
||||||
{theme.homepage && (
|
alt={theme.name}
|
||||||
<a
|
width={500}
|
||||||
href={theme.homepage}
|
height={500}
|
||||||
className="text-blue-500 text-md mt-4"
|
className="w-full rounded-lg border-2 object-cover shadow"
|
||||||
target="_blank"
|
/>
|
||||||
rel="noopener noreferrer"
|
<h1 className="mt-5 text-2xl font-bold">{theme.name}</h1>
|
||||||
>
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
Visit Homepage
|
{theme.description}
|
||||||
</a>
|
</p>
|
||||||
)}
|
{theme.homepage && (
|
||||||
<hr className="mt-4" />
|
<a
|
||||||
<Button
|
href={theme.homepage}
|
||||||
className="mt-4 hidden !rounded-lg"
|
className="text-md mt-4 text-blue-500"
|
||||||
id="install-theme"
|
target="_blank"
|
||||||
zen-theme-id={theme.id}
|
rel="noopener noreferrer"
|
||||||
>Install Theme 🎉</Button>
|
>
|
||||||
<Button
|
Visit Homepage
|
||||||
className="mt-4 hidden !rounded-lg"
|
</a>
|
||||||
id="install-theme-uninstall"
|
)}
|
||||||
zen-theme-id={theme.id}
|
<hr className="mt-4" />
|
||||||
>Uninstall Theme</Button>
|
<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>
|
className="mt-4 hidden !rounded-lg"
|
||||||
</div>
|
id="install-theme"
|
||||||
<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">
|
zen-theme-id={theme.id}
|
||||||
<div id="policy" className="w-full">
|
>
|
||||||
{readme === null ? (
|
Install Theme 🎉
|
||||||
<LoaderCircleIcon className="animate-spin w-12 h-12 mx-auto" />
|
</Button>
|
||||||
) : (
|
<Button
|
||||||
<Markdown>{`${readme}`}</Markdown>
|
className="mt-4 hidden !rounded-lg"
|
||||||
)}
|
id="install-theme-uninstall"
|
||||||
</div>
|
zen-theme-id={theme.id}
|
||||||
<hr className="my-5" />
|
>
|
||||||
<p className="text-muted-foreground text-sm">
|
Uninstall Theme
|
||||||
Theme by{" "}
|
</Button>
|
||||||
<a href={getThemeAuthorLink(theme)} className="text-blue-500 text-md mt-4" target="_blank" rel="noopener noreferrer">
|
<p
|
||||||
{theme.author}
|
id="install-theme-error"
|
||||||
</a>
|
className="mt-2 text-sm text-muted-foreground"
|
||||||
{` • v${theme.version}`}
|
>
|
||||||
</p>
|
You need to have Zen Browser installed to install this theme.{" "}
|
||||||
</div>
|
<a href="/download" className="text-blue-500">
|
||||||
</div>
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
import { type ThemeProviderProps } from "next-themes/dist/types"
|
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,50 +2,62 @@ import { SearchIcon } from "lucide-react";
|
|||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { ny } from "@/lib/utils";
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
const TAGS = [
|
const TAGS = ["all", "color-scheme", "utility"];
|
||||||
"all",
|
|
||||||
"color-scheme",
|
|
||||||
"utility",
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ThemesSearch({
|
export default function ThemesSearch({
|
||||||
input, setInput, tags, setTags
|
input,
|
||||||
|
setInput,
|
||||||
|
tags,
|
||||||
|
setTags,
|
||||||
}: {
|
}: {
|
||||||
input: string;
|
input: string;
|
||||||
setInput: (input: string) => void;
|
setInput: (input: string) => void;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
setTags: (tags: string[]) => void;
|
setTags: (tags: string[]) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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">
|
<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="size-6 mx-4 text-black dark:text-white" />
|
<SearchIcon className="mx-4 size-6 text-black dark:text-white" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder="Search themes"
|
placeholder="Search themes"
|
||||||
className="w-full bg-transparent border-none focus:outline-none focus:border-none focus:ring-0 dark:text-white text-black"
|
className="w-full border-none bg-transparent text-black focus:border-none focus:outline-none focus:ring-0 dark:text-white"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => window.open("https://docs.zen-browser.app/themes-store/themes-marketplace-submission-guidelines#themes-store-submission-guidelines", "_blank")}
|
onClick={() =>
|
||||||
className="text-muted rounded-full rounded-r-none hidden md:block"
|
window.open(
|
||||||
>Submit a theme</Button>
|
"https://docs.zen-browser.app/themes-store/themes-marketplace-submission-guidelines#themes-store-submission-guidelines",
|
||||||
<Button
|
"_blank",
|
||||||
onClick={() => window.open("/create-theme", "_self")}
|
)
|
||||||
className="text-muted rounded-full rounded-l-none border-l border-black dark:border-none hidden md:block"
|
}
|
||||||
>Create your theme</Button>
|
className="hidden rounded-full rounded-r-none text-muted md:block"
|
||||||
</div>
|
>
|
||||||
<div className="flex flex-wrap gap-2 mt-4">
|
Submit a theme
|
||||||
{TAGS.map((tag) => (
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
onClick={() => window.open("/create-theme", "_self")}
|
||||||
key={tag}
|
className="hidden rounded-full rounded-l-none border-l border-black text-muted dark:border-none md:block"
|
||||||
onClick={() => setTags([tag])}
|
>
|
||||||
className={ny(`!rounded-full px-5 ${tags.includes(tag) ? "bg-black dark:bg-white text-white dark:text-black" : ""}`)}
|
Create your theme
|
||||||
>{tag}</Button>
|
</Button>
|
||||||
))}
|
</div>
|
||||||
</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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
import { ChevronDownIcon } from '@radix-ui/react-icons'
|
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<
|
const AccordionItem = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<AccordionPrimitive.Item
|
<AccordionPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('border-b', className)}
|
className={ny("border-b", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AccordionItem.displayName = 'AccordionItem'
|
AccordionItem.displayName = "AccordionItem";
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
const AccordionTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<AccordionPrimitive.Header className="flex">
|
<AccordionPrimitive.Header className="flex">
|
||||||
<AccordionPrimitive.Trigger
|
<AccordionPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronDownIcon className="text-muted-foreground size-4 shrink-0 transition-transform duration-200" />
|
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
</AccordionPrimitive.Header>
|
</AccordionPrimitive.Header>
|
||||||
))
|
));
|
||||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const AccordionContent = React.forwardRef<
|
const AccordionContent = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<AccordionPrimitive.Content
|
<AccordionPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={ny('pb-4 pt-0', className)}>{children}</div>
|
<div className={ny("pb-4 pt-0", className)}>{children}</div>
|
||||||
</AccordionPrimitive.Content>
|
</AccordionPrimitive.Content>
|
||||||
))
|
));
|
||||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from "react";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
export default function AnimatedGradientText({
|
export default function AnimatedGradientText({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
className?: string
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ny(
|
className={ny(
|
||||||
'group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-white/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] dark:bg-black/40',
|
"group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-white/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] dark:bg-black/40",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] ![mask-composite:subtract] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]" />
|
<div className="animate-gradient absolute inset-0 block h-full w-full bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]" />
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,146 +1,144 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from "framer-motion";
|
||||||
import { useEffect, useId, useRef, useState } from 'react'
|
import { useEffect, useId, useRef, useState } from "react";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface GridPatternProps {
|
interface GridPatternProps {
|
||||||
width?: number
|
width?: number;
|
||||||
height?: number
|
height?: number;
|
||||||
x?: number
|
x?: number;
|
||||||
y?: number
|
y?: number;
|
||||||
strokeDasharray?: any
|
strokeDasharray?: any;
|
||||||
numSquares?: number
|
numSquares?: number;
|
||||||
className?: string
|
className?: string;
|
||||||
maxOpacity?: number
|
maxOpacity?: number;
|
||||||
duration?: number
|
duration?: number;
|
||||||
repeatDelay?: number
|
repeatDelay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GridPattern({
|
export function GridPattern({
|
||||||
width = 40,
|
width = 40,
|
||||||
height = 40,
|
height = 40,
|
||||||
x = -1,
|
x = -1,
|
||||||
y = -1,
|
y = -1,
|
||||||
strokeDasharray = 0,
|
strokeDasharray = 0,
|
||||||
numSquares = 50,
|
numSquares = 50,
|
||||||
className,
|
className,
|
||||||
maxOpacity = 0.5,
|
maxOpacity = 0.5,
|
||||||
duration = 4,
|
duration = 4,
|
||||||
repeatDelay = 0.5,
|
repeatDelay = 0.5,
|
||||||
...props
|
...props
|
||||||
}: GridPatternProps) {
|
}: GridPatternProps) {
|
||||||
const id = useId()
|
const id = useId();
|
||||||
const containerRef = useRef(null)
|
const containerRef = useRef(null);
|
||||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||||
const [squares, setSquares] = useState(() => generateSquares(numSquares))
|
const [squares, setSquares] = useState(() => generateSquares(numSquares));
|
||||||
|
|
||||||
function getPos() {
|
function getPos() {
|
||||||
return [
|
return [
|
||||||
Math.floor((Math.random() * dimensions.width) / width),
|
Math.floor((Math.random() * dimensions.width) / width),
|
||||||
Math.floor((Math.random() * dimensions.height) / height),
|
Math.floor((Math.random() * dimensions.height) / height),
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust the generateSquares function to return objects with an id, x, and y
|
// Adjust the generateSquares function to return objects with an id, x, and y
|
||||||
function generateSquares(count: number) {
|
function generateSquares(count: number) {
|
||||||
return Array.from({ length: count }, (_, i) => ({
|
return Array.from({ length: count }, (_, i) => ({
|
||||||
id: i,
|
id: i,
|
||||||
pos: getPos(),
|
pos: getPos(),
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to update a single square's position
|
// Function to update a single square's position
|
||||||
const updateSquarePosition = (id: number) => {
|
const updateSquarePosition = (id: number) => {
|
||||||
setSquares(currentSquares =>
|
setSquares((currentSquares) =>
|
||||||
currentSquares.map(sq =>
|
currentSquares.map((sq) =>
|
||||||
sq.id === id
|
sq.id === id
|
||||||
? {
|
? {
|
||||||
...sq,
|
...sq,
|
||||||
pos: getPos(),
|
pos: getPos(),
|
||||||
}
|
}
|
||||||
: sq,
|
: sq,
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Update squares to animate in
|
// Update squares to animate in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dimensions.width && dimensions.height)
|
if (dimensions.width && dimensions.height)
|
||||||
setSquares(generateSquares(numSquares))
|
setSquares(generateSquares(numSquares));
|
||||||
}, [dimensions, numSquares])
|
}, [dimensions, numSquares]);
|
||||||
|
|
||||||
// Resize observer to update container dimensions
|
// Resize observer to update container dimensions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
setDimensions({
|
setDimensions({
|
||||||
width: entry.contentRect.width,
|
width: entry.contentRect.width,
|
||||||
height: entry.contentRect.height,
|
height: entry.contentRect.height,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
if (containerRef.current)
|
if (containerRef.current) resizeObserver.observe(containerRef.current);
|
||||||
resizeObserver.observe(containerRef.current)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (containerRef.current)
|
if (containerRef.current) resizeObserver.unobserve(containerRef.current);
|
||||||
resizeObserver.unobserve(containerRef.current)
|
};
|
||||||
}
|
}, [containerRef]);
|
||||||
}, [containerRef])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={ny(
|
className={ny(
|
||||||
'pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30',
|
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<pattern
|
<pattern
|
||||||
id={id}
|
id={id}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
patternUnits="userSpaceOnUse"
|
patternUnits="userSpaceOnUse"
|
||||||
x={x}
|
x={x}
|
||||||
y={y}
|
y={y}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d={`M.5 ${height}V.5H${width}`}
|
d={`M.5 ${height}V.5H${width}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
/>
|
/>
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill={`url(#${id})`} />
|
<rect width="100%" height="100%" fill={`url(#${id})`} />
|
||||||
<svg x={x} y={y} className="overflow-visible">
|
<svg x={x} y={y} className="overflow-visible">
|
||||||
{squares.map(({ pos: [x, y], id }, index) => (
|
{squares.map(({ pos: [x, y], id }, index) => (
|
||||||
<motion.rect
|
<motion.rect
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: maxOpacity }}
|
animate={{ opacity: maxOpacity }}
|
||||||
transition={{
|
transition={{
|
||||||
duration,
|
duration,
|
||||||
repeat: 1,
|
repeat: 1,
|
||||||
delay: index * 0.1,
|
delay: index * 0.1,
|
||||||
repeatType: 'reverse',
|
repeatType: "reverse",
|
||||||
}}
|
}}
|
||||||
onAnimationComplete={() => updateSquarePosition(id)}
|
onAnimationComplete={() => updateSquarePosition(id)}
|
||||||
key={`${x}-${y}-${index}`}
|
key={`${x}-${y}-${index}`}
|
||||||
width={width - 1}
|
width={width - 1}
|
||||||
height={height - 1}
|
height={height - 1}
|
||||||
x={x * width + 1}
|
x={x * width + 1}
|
||||||
y={y * height + 1}
|
y={y * height + 1}
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
strokeWidth="0"
|
strokeWidth="0"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</svg>
|
</svg>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GridPattern
|
export default GridPattern;
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
import type { CSSProperties, FC, ReactNode } from 'react'
|
import type { CSSProperties, FC, ReactNode } from "react";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface AnimatedShinyTextProps {
|
interface AnimatedShinyTextProps {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
className?: string
|
className?: string;
|
||||||
shimmerWidth?: number
|
shimmerWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
shimmerWidth = 100,
|
shimmerWidth = 100,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--shimmer-width': `${shimmerWidth}px`,
|
"--shimmer-width": `${shimmerWidth}px`,
|
||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}
|
||||||
className={ny(
|
className={ny(
|
||||||
'mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50 ',
|
"mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50",
|
||||||
|
|
||||||
// Shimmer effect
|
// Shimmer effect
|
||||||
'animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
|
"animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
|
||||||
|
|
||||||
// Shimmer gradient
|
// Shimmer gradient
|
||||||
'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',
|
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
|
||||||
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AnimatedShinyText
|
export default AnimatedShinyText;
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { useRef } from 'react'
|
import { useRef } from "react";
|
||||||
import type { Variants } from 'framer-motion'
|
import type { Variants } from "framer-motion";
|
||||||
import { AnimatePresence, motion, useInView } from 'framer-motion'
|
import { AnimatePresence, motion, useInView } from "framer-motion";
|
||||||
|
|
||||||
interface BlurFadeProps {
|
interface BlurFadeProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
className?: string
|
className?: string;
|
||||||
variant?: {
|
variant?: {
|
||||||
hidden: { y: number }
|
hidden: { y: number };
|
||||||
visible: { y: number }
|
visible: { y: number };
|
||||||
}
|
};
|
||||||
duration?: number
|
duration?: number;
|
||||||
delay?: number
|
delay?: number;
|
||||||
yOffset?: number
|
yOffset?: number;
|
||||||
inView?: boolean
|
inView?: boolean;
|
||||||
inViewMargin?: any
|
inViewMargin?: any;
|
||||||
blur?: string
|
blur?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BlurFade({
|
export default function BlurFade({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
duration = 0.4,
|
duration = 0.4,
|
||||||
delay = 0,
|
delay = 0,
|
||||||
yOffset = 6,
|
yOffset = 6,
|
||||||
inView = false,
|
inView = false,
|
||||||
inViewMargin = '-50px',
|
inViewMargin = "-50px",
|
||||||
blur = '6px',
|
blur = "6px",
|
||||||
}: BlurFadeProps) {
|
}: BlurFadeProps) {
|
||||||
const ref = useRef(null)
|
const ref = useRef(null);
|
||||||
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
|
const inViewResult = useInView(ref, { once: true, margin: inViewMargin });
|
||||||
const isInView = !inView || inViewResult
|
const isInView = !inView || inViewResult;
|
||||||
const defaultVariants: Variants = {
|
const defaultVariants: Variants = {
|
||||||
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
|
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
|
||||||
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
|
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
|
||||||
}
|
};
|
||||||
const combinedVariants = variant || defaultVariants
|
const combinedVariants = variant || defaultVariants;
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<motion.div
|
<motion.div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate={isInView ? 'visible' : 'hidden'}
|
animate={isInView ? "visible" : "hidden"}
|
||||||
exit="hidden"
|
exit="hidden"
|
||||||
variants={combinedVariants}
|
variants={combinedVariants}
|
||||||
transition={{
|
transition={{
|
||||||
delay: 0.04 + delay,
|
delay: 0.04 + delay,
|
||||||
duration,
|
duration,
|
||||||
ease: 'easeOut',
|
ease: "easeOut",
|
||||||
}}
|
}}
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
'use client'
|
"use client";
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from "framer-motion";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface BlurIntProps {
|
interface BlurIntProps {
|
||||||
word: string
|
word: string;
|
||||||
className?: string
|
className?: string;
|
||||||
variant?: {
|
variant?: {
|
||||||
hidden: { filter: string, opacity: number }
|
hidden: { filter: string; opacity: number };
|
||||||
visible: { filter: string, opacity: number }
|
visible: { filter: string; opacity: number };
|
||||||
}
|
};
|
||||||
duration?: number
|
duration?: number;
|
||||||
}
|
}
|
||||||
function BlurIn({ word, className, variant, duration = 1 }: BlurIntProps) {
|
function BlurIn({ word, className, variant, duration = 1 }: BlurIntProps) {
|
||||||
const defaultVariants = {
|
const defaultVariants = {
|
||||||
hidden: { filter: 'blur(10px)', opacity: 0 },
|
hidden: { filter: "blur(10px)", opacity: 0 },
|
||||||
visible: { filter: 'blur(0px)', opacity: 1 },
|
visible: { filter: "blur(0px)", opacity: 1 },
|
||||||
}
|
};
|
||||||
const combinedVariants = variant || defaultVariants
|
const combinedVariants = variant || defaultVariants;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.h1
|
<motion.h1
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
transition={{ duration }}
|
transition={{ duration }}
|
||||||
variants={combinedVariants}
|
variants={combinedVariants}
|
||||||
className={ny(
|
className={ny(
|
||||||
className,
|
className,
|
||||||
'font-display text-center text-4xl font-bold tracking-[-0.02em] drop-shadow-sm md:text-7xl md:leading-[5rem]',
|
"font-display text-center text-4xl font-bold tracking-[-0.02em] drop-shadow-sm md:text-7xl md:leading-[5rem]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{word}
|
{word}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BlurIn
|
export default BlurIn;
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface BorderBeamProps {
|
interface BorderBeamProps {
|
||||||
className?: string
|
className?: string;
|
||||||
size?: number
|
size?: number;
|
||||||
duration?: number
|
duration?: number;
|
||||||
borderWidth?: number
|
borderWidth?: number;
|
||||||
anchor?: number
|
anchor?: number;
|
||||||
colorFrom?: string
|
colorFrom?: string;
|
||||||
colorTo?: string
|
colorTo?: string;
|
||||||
delay?: number
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BorderBeam({
|
export function BorderBeam({
|
||||||
className,
|
className,
|
||||||
size = 200,
|
size = 200,
|
||||||
duration = 15,
|
duration = 15,
|
||||||
anchor = 90,
|
anchor = 90,
|
||||||
borderWidth = 1.5,
|
borderWidth = 1.5,
|
||||||
colorFrom = '#ffaa40',
|
colorFrom = "#ffaa40",
|
||||||
colorTo = '#9c40ff',
|
colorTo = "#9c40ff",
|
||||||
delay = 0,
|
delay = 0,
|
||||||
}: BorderBeamProps) {
|
}: BorderBeamProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--size': size,
|
"--size": size,
|
||||||
'--duration': duration,
|
"--duration": duration,
|
||||||
'--anchor': anchor,
|
"--anchor": anchor,
|
||||||
'--border-width': borderWidth,
|
"--border-width": borderWidth,
|
||||||
'--color-from': colorFrom,
|
"--color-from": colorFrom,
|
||||||
'--color-to': colorTo,
|
"--color-to": colorTo,
|
||||||
'--delay': `-${delay}s`,
|
"--delay": `-${delay}s`,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
className={ny(
|
className={ny(
|
||||||
'pointer-events-none absolute inset-0 rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]',
|
"pointer-events-none absolute inset-0 rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]",
|
||||||
|
|
||||||
// mask styles
|
// mask styles
|
||||||
'![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]',
|
"![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]",
|
||||||
|
|
||||||
// pseudo styles
|
// pseudo styles
|
||||||
'after:animate-border-beam after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]',
|
"after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:animate-border-beam after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,52 +5,52 @@ import { type VariantProps, cva } from "class-variance-authority";
|
|||||||
import { ny } from "@/lib/utils";
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground ",
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground ",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-12 px-8 py-4",
|
default: "h-12 px-8 py-4",
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: "h-10 rounded-md px-8",
|
||||||
icon: "h-9 w-9",
|
icon: "h-9 w-9",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
const Comp = asChild ? Slot : "button";
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={ny(buttonVariants({ variant, size, className }))}
|
className={ny(buttonVariants({ variant, size, className }))}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Button.displayName = "Button";
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
import { CheckIcon } from '@radix-ui/react-icons'
|
import { CheckIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
const Checkbox = React.forwardRef<
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
className={ny('flex items-center justify-center text-current')}
|
className={ny("flex items-center justify-center text-current")}
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
))
|
));
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Checkbox }
|
export { Checkbox };
|
||||||
|
|||||||
@@ -1,119 +1,124 @@
|
|||||||
'use client'
|
"use client";
|
||||||
import confetti from 'canvas-confetti'
|
import confetti from "canvas-confetti";
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from "react";
|
||||||
import React, { createContext, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
import React, {
|
||||||
|
createContext,
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GlobalOptions as ConfettiGlobalOptions,
|
GlobalOptions as ConfettiGlobalOptions,
|
||||||
CreateTypes as ConfettiInstance,
|
CreateTypes as ConfettiInstance,
|
||||||
Options as ConfettiOptions,
|
Options as ConfettiOptions,
|
||||||
} from 'canvas-confetti'
|
} from "canvas-confetti";
|
||||||
import { Button, ButtonProps } from './button'
|
import { Button, ButtonProps } from "./button";
|
||||||
|
|
||||||
interface Api {
|
interface Api {
|
||||||
fire: (options?: ConfettiOptions) => void
|
fire: (options?: ConfettiOptions) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = React.ComponentPropsWithRef<'canvas'> & {
|
type Props = React.ComponentPropsWithRef<"canvas"> & {
|
||||||
options?: ConfettiOptions
|
options?: ConfettiOptions;
|
||||||
globalOptions?: ConfettiGlobalOptions
|
globalOptions?: ConfettiGlobalOptions;
|
||||||
manualstart?: boolean
|
manualstart?: boolean;
|
||||||
children?: ReactNode
|
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 Confetti = forwardRef<ConfettiRef, Props>((props, ref) => {
|
||||||
const {
|
const {
|
||||||
options,
|
options,
|
||||||
globalOptions = { resize: true, useWorker: true },
|
globalOptions = { resize: true, useWorker: true },
|
||||||
manualstart = false,
|
manualstart = false,
|
||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props;
|
||||||
const instanceRef = useRef<ConfettiInstance | null>(null) // confetti instance
|
const instanceRef = useRef<ConfettiInstance | null>(null); // confetti instance
|
||||||
|
|
||||||
const canvasRef = useCallback(
|
const canvasRef = useCallback(
|
||||||
// https://react.dev/reference/react-dom/components/common#ref-callback
|
// https://react.dev/reference/react-dom/components/common#ref-callback
|
||||||
// https://reactjs.org/docs/refs-and-the-dom.html#callback-refs
|
// https://reactjs.org/docs/refs-and-the-dom.html#callback-refs
|
||||||
(node: HTMLCanvasElement) => {
|
(node: HTMLCanvasElement) => {
|
||||||
if (node !== null) {
|
if (node !== null) {
|
||||||
// <canvas> is mounted => create the confetti instance
|
// <canvas> is mounted => create the confetti instance
|
||||||
if (instanceRef.current)
|
if (instanceRef.current) return; // if not already created
|
||||||
return // if not already created
|
instanceRef.current = confetti.create(node, {
|
||||||
instanceRef.current = confetti.create(node, {
|
...globalOptions,
|
||||||
...globalOptions,
|
resize: true,
|
||||||
resize: true,
|
});
|
||||||
})
|
} else {
|
||||||
}
|
// <canvas> is unmounted => reset and destroy instanceRef
|
||||||
else {
|
if (instanceRef.current) {
|
||||||
// <canvas> is unmounted => reset and destroy instanceRef
|
instanceRef.current.reset();
|
||||||
if (instanceRef.current) {
|
instanceRef.current = null;
|
||||||
instanceRef.current.reset()
|
}
|
||||||
instanceRef.current = null
|
}
|
||||||
}
|
},
|
||||||
}
|
[globalOptions],
|
||||||
},
|
);
|
||||||
[globalOptions],
|
|
||||||
)
|
|
||||||
|
|
||||||
// `fire` is a function that calls the instance() with `opts` merged with `options`
|
// `fire` is a function that calls the instance() with `opts` merged with `options`
|
||||||
const fire = useCallback(
|
const fire = useCallback(
|
||||||
(opts = {}) => instanceRef.current?.({ ...options, ...opts }),
|
(opts = {}) => instanceRef.current?.({ ...options, ...opts }),
|
||||||
[options],
|
[options],
|
||||||
)
|
);
|
||||||
|
|
||||||
const api = useMemo(
|
const api = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
fire,
|
fire,
|
||||||
}),
|
}),
|
||||||
[fire],
|
[fire],
|
||||||
)
|
);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => api, [api])
|
useImperativeHandle(ref, () => api, [api]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!manualstart)
|
if (!manualstart) fire();
|
||||||
fire()
|
}, [manualstart, fire]);
|
||||||
}, [manualstart, fire])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfettiContext.Provider value={api}>
|
<ConfettiContext.Provider value={api}>
|
||||||
<canvas ref={canvasRef} {...rest} />
|
<canvas ref={canvasRef} {...rest} />
|
||||||
{children}
|
{children}
|
||||||
</ConfettiContext.Provider>
|
</ConfettiContext.Provider>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
interface ConfettiButtonProps extends ButtonProps {
|
interface ConfettiButtonProps extends ButtonProps {
|
||||||
options?: ConfettiOptions &
|
options?: ConfettiOptions &
|
||||||
ConfettiGlobalOptions & { canvas?: HTMLCanvasElement }
|
ConfettiGlobalOptions & { canvas?: HTMLCanvasElement };
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConfettiButton({ options, children, ...props }: ConfettiButtonProps) {
|
function ConfettiButton({ options, children, ...props }: ConfettiButtonProps) {
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
const rect = event.currentTarget.getBoundingClientRect()
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
const x = rect.left + rect.width / 2
|
const x = rect.left + rect.width / 2;
|
||||||
const y = rect.top + rect.height / 2
|
const y = rect.top + rect.height / 2;
|
||||||
confetti({
|
confetti({
|
||||||
...options,
|
...options,
|
||||||
origin: {
|
origin: {
|
||||||
x: x / window.innerWidth,
|
x: x / window.innerWidth,
|
||||||
y: y / window.innerHeight,
|
y: y / window.innerHeight,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleClick} {...props}>
|
<Button onClick={handleClick} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Confetti, ConfettiButton }
|
export { Confetti, ConfettiButton };
|
||||||
|
|
||||||
export default Confetti
|
export default Confetti;
|
||||||
|
|||||||
@@ -1,239 +1,234 @@
|
|||||||
'use client'
|
"use client";
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from "react";
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
export interface BaseParticle {
|
export interface BaseParticle {
|
||||||
element: HTMLElement | SVGSVGElement
|
element: HTMLElement | SVGSVGElement;
|
||||||
left: number
|
left: number;
|
||||||
size: number
|
size: number;
|
||||||
top: number
|
top: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseParticleOptions {
|
export interface BaseParticleOptions {
|
||||||
particle?: string
|
particle?: string;
|
||||||
size?: number
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CoolParticle extends BaseParticle {
|
export interface CoolParticle extends BaseParticle {
|
||||||
direction: number
|
direction: number;
|
||||||
speedHorz: number
|
speedHorz: number;
|
||||||
speedUp: number
|
speedUp: number;
|
||||||
spinSpeed: number
|
spinSpeed: number;
|
||||||
spinVal: number
|
spinVal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CoolParticleOptions extends BaseParticleOptions {
|
export interface CoolParticleOptions extends BaseParticleOptions {
|
||||||
particleCount?: number
|
particleCount?: number;
|
||||||
speedHorz?: number
|
speedHorz?: number;
|
||||||
speedUp?: number
|
speedUp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContainer() {
|
function getContainer() {
|
||||||
const id = '_coolMode_effect'
|
const id = "_coolMode_effect";
|
||||||
const existingContainer = document.getElementById(id)
|
const existingContainer = document.getElementById(id);
|
||||||
|
|
||||||
if (existingContainer)
|
if (existingContainer) return existingContainer;
|
||||||
return existingContainer
|
|
||||||
|
|
||||||
const container = document.createElement('div')
|
const container = document.createElement("div");
|
||||||
container.setAttribute('id', id)
|
container.setAttribute("id", id);
|
||||||
container.setAttribute(
|
container.setAttribute(
|
||||||
'style',
|
"style",
|
||||||
'overflow:hidden; position:fixed; height:100%; top:0; left:0; right:0; bottom:0; pointer-events:none; z-index:2147483647',
|
"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(
|
function applyParticleEffect(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
options?: CoolParticleOptions,
|
options?: CoolParticleOptions,
|
||||||
): () => void {
|
): () => void {
|
||||||
instanceCounter++
|
instanceCounter++;
|
||||||
|
|
||||||
const defaultParticle = 'circle'
|
const defaultParticle = "circle";
|
||||||
const particleType = options?.particle || defaultParticle
|
const particleType = options?.particle || defaultParticle;
|
||||||
const sizes = [15, 20, 25, 35, 45]
|
const sizes = [15, 20, 25, 35, 45];
|
||||||
const limit = 45
|
const limit = 45;
|
||||||
|
|
||||||
let particles: CoolParticle[] = []
|
let particles: CoolParticle[] = [];
|
||||||
let autoAddParticle = false
|
let autoAddParticle = false;
|
||||||
let mouseX = 0
|
let mouseX = 0;
|
||||||
let mouseY = 0
|
let mouseY = 0;
|
||||||
|
|
||||||
const container = getContainer()
|
const container = getContainer();
|
||||||
|
|
||||||
function generateParticle() {
|
function generateParticle() {
|
||||||
const size
|
const size =
|
||||||
= options?.size || sizes[Math.floor(Math.random() * sizes.length)]
|
options?.size || sizes[Math.floor(Math.random() * sizes.length)];
|
||||||
const speedHorz = options?.speedHorz || Math.random() * 10
|
const speedHorz = options?.speedHorz || Math.random() * 10;
|
||||||
const speedUp = options?.speedUp || Math.random() * 25
|
const speedUp = options?.speedUp || Math.random() * 25;
|
||||||
const spinVal = Math.random() * 360
|
const spinVal = Math.random() * 360;
|
||||||
const spinSpeed = Math.random() * 35 * (Math.random() <= 0.5 ? -1 : 1)
|
const spinSpeed = Math.random() * 35 * (Math.random() <= 0.5 ? -1 : 1);
|
||||||
const top = mouseY - size / 2
|
const top = mouseY - size / 2;
|
||||||
const left = mouseX - size / 2
|
const left = mouseX - size / 2;
|
||||||
const direction = Math.random() <= 0.5 ? -1 : 1
|
const direction = Math.random() <= 0.5 ? -1 : 1;
|
||||||
|
|
||||||
const particle = document.createElement('div')
|
const particle = document.createElement("div");
|
||||||
|
|
||||||
if (particleType === 'circle') {
|
if (particleType === "circle") {
|
||||||
const svgNS = 'http://www.w3.org/2000/svg'
|
const svgNS = "http://www.w3.org/2000/svg";
|
||||||
const circleSVG = document.createElementNS(svgNS, 'svg')
|
const circleSVG = document.createElementNS(svgNS, "svg");
|
||||||
const circle = document.createElementNS(svgNS, 'circle')
|
const circle = document.createElementNS(svgNS, "circle");
|
||||||
circle.setAttributeNS(null, 'cx', (size / 2).toString())
|
circle.setAttributeNS(null, "cx", (size / 2).toString());
|
||||||
circle.setAttributeNS(null, 'cy', (size / 2).toString())
|
circle.setAttributeNS(null, "cy", (size / 2).toString());
|
||||||
circle.setAttributeNS(null, 'r', (size / 2).toString())
|
circle.setAttributeNS(null, "r", (size / 2).toString());
|
||||||
circle.setAttributeNS(
|
circle.setAttributeNS(
|
||||||
null,
|
null,
|
||||||
'fill',
|
"fill",
|
||||||
`hsl(${Math.random() * 360}, 70%, 50%)`,
|
`hsl(${Math.random() * 360}, 70%, 50%)`,
|
||||||
)
|
);
|
||||||
|
|
||||||
circleSVG.appendChild(circle)
|
circleSVG.appendChild(circle);
|
||||||
circleSVG.setAttribute('width', size.toString())
|
circleSVG.setAttribute("width", size.toString());
|
||||||
circleSVG.setAttribute('height', size.toString())
|
circleSVG.setAttribute("height", size.toString());
|
||||||
|
|
||||||
particle.appendChild(circleSVG)
|
particle.appendChild(circleSVG);
|
||||||
}
|
} else {
|
||||||
else {
|
particle.innerHTML = `<img src="${particleType}" width="${size}" height="${size}" style="border-radius: 50%">`;
|
||||||
particle.innerHTML = `<img src="${particleType}" width="${size}" height="${size}" style="border-radius: 50%">`
|
}
|
||||||
}
|
|
||||||
|
|
||||||
particle.style.position = 'absolute'
|
particle.style.position = "absolute";
|
||||||
particle.style.transform = `translate3d(${left}px, ${top}px, 0px) rotate(${spinVal}deg)`
|
particle.style.transform = `translate3d(${left}px, ${top}px, 0px) rotate(${spinVal}deg)`;
|
||||||
|
|
||||||
container.appendChild(particle)
|
container.appendChild(particle);
|
||||||
|
|
||||||
particles.push({
|
particles.push({
|
||||||
direction,
|
direction,
|
||||||
element: particle,
|
element: particle,
|
||||||
left,
|
left,
|
||||||
size,
|
size,
|
||||||
speedHorz,
|
speedHorz,
|
||||||
speedUp,
|
speedUp,
|
||||||
spinSpeed,
|
spinSpeed,
|
||||||
spinVal,
|
spinVal,
|
||||||
top,
|
top,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshParticles() {
|
function refreshParticles() {
|
||||||
particles.forEach((p) => {
|
particles.forEach((p) => {
|
||||||
p.left = p.left - p.speedHorz * p.direction
|
p.left = p.left - p.speedHorz * p.direction;
|
||||||
p.top = p.top - p.speedUp
|
p.top = p.top - p.speedUp;
|
||||||
p.speedUp = Math.min(p.size, p.speedUp - 1)
|
p.speedUp = Math.min(p.size, p.speedUp - 1);
|
||||||
p.spinVal = p.spinVal + p.spinSpeed
|
p.spinVal = p.spinVal + p.spinSpeed;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
p.top
|
p.top >=
|
||||||
>= Math.max(window.innerHeight, document.body.clientHeight) + p.size
|
Math.max(window.innerHeight, document.body.clientHeight) + p.size
|
||||||
) {
|
) {
|
||||||
particles = particles.filter(o => o !== p)
|
particles = particles.filter((o) => o !== p);
|
||||||
p.element.remove()
|
p.element.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
p.element.setAttribute(
|
p.element.setAttribute(
|
||||||
'style',
|
"style",
|
||||||
[
|
[
|
||||||
'position:absolute',
|
"position:absolute",
|
||||||
'will-change:transform',
|
"will-change:transform",
|
||||||
`top:${p.top}px`,
|
`top:${p.top}px`,
|
||||||
`left:${p.left}px`,
|
`left:${p.left}px`,
|
||||||
`transform:rotate(${p.spinVal}deg)`,
|
`transform:rotate(${p.spinVal}deg)`,
|
||||||
].join(';'),
|
].join(";"),
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let animationFrame: number | undefined
|
let animationFrame: number | undefined;
|
||||||
|
|
||||||
let lastParticleTimestamp = 0
|
let lastParticleTimestamp = 0;
|
||||||
const particleGenerationDelay = 30
|
const particleGenerationDelay = 30;
|
||||||
|
|
||||||
function loop() {
|
function loop() {
|
||||||
const currentTime = performance.now()
|
const currentTime = performance.now();
|
||||||
if (
|
if (
|
||||||
autoAddParticle
|
autoAddParticle &&
|
||||||
&& particles.length < limit
|
particles.length < limit &&
|
||||||
&& currentTime - lastParticleTimestamp > particleGenerationDelay
|
currentTime - lastParticleTimestamp > particleGenerationDelay
|
||||||
) {
|
) {
|
||||||
generateParticle()
|
generateParticle();
|
||||||
lastParticleTimestamp = currentTime
|
lastParticleTimestamp = currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshParticles()
|
refreshParticles();
|
||||||
animationFrame = requestAnimationFrame(loop)
|
animationFrame = requestAnimationFrame(loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
loop()
|
loop();
|
||||||
|
|
||||||
const isTouchInteraction = 'ontouchstart' in window
|
const isTouchInteraction = "ontouchstart" in window;
|
||||||
|
|
||||||
const tap = isTouchInteraction ? 'touchstart' : 'mousedown'
|
const tap = isTouchInteraction ? "touchstart" : "mousedown";
|
||||||
const tapEnd = isTouchInteraction ? 'touchend' : 'mouseup'
|
const tapEnd = isTouchInteraction ? "touchend" : "mouseup";
|
||||||
const move = isTouchInteraction ? 'touchmove' : 'mousemove'
|
const move = isTouchInteraction ? "touchmove" : "mousemove";
|
||||||
|
|
||||||
const updateMousePosition = (e: MouseEvent | TouchEvent) => {
|
const updateMousePosition = (e: MouseEvent | TouchEvent) => {
|
||||||
if ('touches' in e) {
|
if ("touches" in e) {
|
||||||
mouseX = e.touches?.[0].clientX
|
mouseX = e.touches?.[0].clientX;
|
||||||
mouseY = e.touches?.[0].clientY
|
mouseY = e.touches?.[0].clientY;
|
||||||
}
|
} else {
|
||||||
else {
|
mouseX = e.clientX;
|
||||||
mouseX = e.clientX
|
mouseY = e.clientY;
|
||||||
mouseY = e.clientY
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const tapHandler = (e: MouseEvent | TouchEvent) => {
|
const tapHandler = (e: MouseEvent | TouchEvent) => {
|
||||||
updateMousePosition(e)
|
updateMousePosition(e);
|
||||||
autoAddParticle = true
|
autoAddParticle = true;
|
||||||
}
|
};
|
||||||
|
|
||||||
const disableAutoAddParticle = () => {
|
const disableAutoAddParticle = () => {
|
||||||
autoAddParticle = false
|
autoAddParticle = false;
|
||||||
}
|
};
|
||||||
|
|
||||||
element.addEventListener(move, updateMousePosition, { passive: true })
|
element.addEventListener(move, updateMousePosition, { passive: true });
|
||||||
element.addEventListener(tap, tapHandler, { passive: true })
|
element.addEventListener(tap, tapHandler, { passive: true });
|
||||||
element.addEventListener(tapEnd, disableAutoAddParticle, { passive: true })
|
element.addEventListener(tapEnd, disableAutoAddParticle, { passive: true });
|
||||||
element.addEventListener('mouseleave', disableAutoAddParticle, {
|
element.addEventListener("mouseleave", disableAutoAddParticle, {
|
||||||
passive: true,
|
passive: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
element.removeEventListener(move, updateMousePosition)
|
element.removeEventListener(move, updateMousePosition);
|
||||||
element.removeEventListener(tap, tapHandler)
|
element.removeEventListener(tap, tapHandler);
|
||||||
element.removeEventListener(tapEnd, disableAutoAddParticle)
|
element.removeEventListener(tapEnd, disableAutoAddParticle);
|
||||||
element.removeEventListener('mouseleave', disableAutoAddParticle)
|
element.removeEventListener("mouseleave", disableAutoAddParticle);
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (animationFrame && particles.length === 0) {
|
if (animationFrame && particles.length === 0) {
|
||||||
cancelAnimationFrame(animationFrame)
|
cancelAnimationFrame(animationFrame);
|
||||||
clearInterval(interval)
|
clearInterval(interval);
|
||||||
|
|
||||||
if (--instanceCounter === 0)
|
if (--instanceCounter === 0) container.remove();
|
||||||
container.remove()
|
}
|
||||||
}
|
}, 500);
|
||||||
}, 500)
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CoolModeProps {
|
interface CoolModeProps {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
options?: CoolParticleOptions
|
options?: CoolParticleOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CoolMode: React.FC<CoolModeProps> = ({ children, options }) => {
|
export const CoolMode: React.FC<CoolModeProps> = ({ children, options }) => {
|
||||||
const ref = useRef<HTMLElement>(null)
|
const ref = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current)
|
if (ref.current) return applyParticleEffect(ref.current, options);
|
||||||
return applyParticleEffect(ref.current, options)
|
}, [options]);
|
||||||
}, [options])
|
|
||||||
|
|
||||||
return React.cloneElement(children as React.ReactElement, { ref })
|
return React.cloneElement(children as React.ReactElement, { ref });
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,126 +1,126 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { Cross2Icon } from '@radix-ui/react-icons'
|
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<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
|
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
<Cross2Icon className="size-4" />
|
<Cross2Icon className="size-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
))
|
));
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
function DialogHeader({
|
function DialogHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ny(
|
className={ny(
|
||||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
DialogHeader.displayName = 'DialogHeader'
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
function DialogFooter({
|
function DialogFooter({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ny(
|
className={ny(
|
||||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
DialogFooter.displayName = 'DialogFooter'
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'text-lg font-semibold leading-none tracking-tight',
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
const DialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('text-muted-foreground text-sm', className)}
|
className={ny("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,55 +1,55 @@
|
|||||||
import { useId } from 'react'
|
import { useId } from "react";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface DotPatternProps {
|
interface DotPatternProps {
|
||||||
width?: any
|
width?: any;
|
||||||
height?: any
|
height?: any;
|
||||||
x?: any
|
x?: any;
|
||||||
y?: any
|
y?: any;
|
||||||
cx?: any
|
cx?: any;
|
||||||
cy?: any
|
cy?: any;
|
||||||
cr?: any
|
cr?: any;
|
||||||
className?: string
|
className?: string;
|
||||||
[key: string]: any
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
export function DotPattern({
|
export function DotPattern({
|
||||||
width = 16,
|
width = 16,
|
||||||
height = 16,
|
height = 16,
|
||||||
x = 0,
|
x = 0,
|
||||||
y = 0,
|
y = 0,
|
||||||
cx = 1,
|
cx = 1,
|
||||||
cy = 1,
|
cy = 1,
|
||||||
cr = 1,
|
cr = 1,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: DotPatternProps) {
|
}: DotPatternProps) {
|
||||||
const id = useId()
|
const id = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={ny(
|
className={ny(
|
||||||
'pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80',
|
"pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<pattern
|
<pattern
|
||||||
id={id}
|
id={id}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
patternUnits="userSpaceOnUse"
|
patternUnits="userSpaceOnUse"
|
||||||
patternContentUnits="userSpaceOnUse"
|
patternContentUnits="userSpaceOnUse"
|
||||||
x={x}
|
x={x}
|
||||||
y={y}
|
y={y}
|
||||||
>
|
>
|
||||||
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
|
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DotPattern
|
export default DotPattern;
|
||||||
|
|||||||
@@ -1,205 +1,205 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
DotFilledIcon,
|
DotFilledIcon,
|
||||||
} from '@radix-ui/react-icons'
|
} from "@radix-ui/react-icons";
|
||||||
|
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
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',
|
"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',
|
inset && "pl-8",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
))
|
));
|
||||||
DropdownMenuSubTrigger.displayName
|
DropdownMenuSubTrigger.displayName =
|
||||||
= DropdownMenuPrimitive.SubTrigger.displayName
|
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-lg',
|
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSubContent.displayName
|
DropdownMenuSubContent.displayName =
|
||||||
= DropdownMenuPrimitive.SubContent.displayName
|
DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={ny(
|
className={ny(
|
||||||
'bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-md',
|
"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',
|
"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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
))
|
));
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
const DropdownMenuItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
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',
|
"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',
|
inset && "pl-8",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<CheckIcon className="size-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
))
|
));
|
||||||
DropdownMenuCheckboxItem.displayName
|
DropdownMenuCheckboxItem.displayName =
|
||||||
= DropdownMenuPrimitive.CheckboxItem.displayName
|
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<DotFilledIcon className="size-4 fill-current" />
|
<DotFilledIcon className="size-4 fill-current" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
))
|
));
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'px-2 py-1.5 text-sm font-semibold',
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
inset && 'pl-8',
|
inset && "pl-8",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('bg-muted -mx-1 my-1 h-px', className)}
|
className={ny("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
function DropdownMenuShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) {
|
}: React.HTMLAttributes<HTMLSpanElement>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={ny('ml-auto text-xs tracking-widest opacity-60', className)}
|
className={ny("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuPortal,
|
DropdownMenuPortal,
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuRadioGroup,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import type { Variants } from 'framer-motion'
|
import type { Variants } from "framer-motion";
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from "framer-motion";
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from "react";
|
||||||
|
|
||||||
interface FadeTextProps {
|
interface FadeTextProps {
|
||||||
className?: string
|
className?: string;
|
||||||
direction?: 'up' | 'down' | 'left' | 'right'
|
direction?: "up" | "down" | "left" | "right";
|
||||||
framerProps?: Variants
|
framerProps?: Variants;
|
||||||
text: string
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FadeText({
|
export function FadeText({
|
||||||
direction = 'up',
|
direction = "up",
|
||||||
className,
|
className,
|
||||||
framerProps = {
|
framerProps = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
show: { opacity: 1, transition: { type: 'spring' } },
|
show: { opacity: 1, transition: { type: "spring" } },
|
||||||
},
|
},
|
||||||
text,
|
text,
|
||||||
}: FadeTextProps) {
|
}: FadeTextProps) {
|
||||||
const directionOffset = useMemo(() => {
|
const directionOffset = useMemo(() => {
|
||||||
const map = { up: 10, down: -10, left: -10, right: 10 }
|
const map = { up: 10, down: -10, left: -10, right: 10 };
|
||||||
return map[direction]
|
return map[direction];
|
||||||
}, [direction])
|
}, [direction]);
|
||||||
|
|
||||||
const axis = direction === 'up' || direction === 'down' ? 'y' : 'x'
|
const axis = direction === "up" || direction === "down" ? "y" : "x";
|
||||||
|
|
||||||
const FADE_ANIMATION_VARIANTS = useMemo(() => {
|
const FADE_ANIMATION_VARIANTS = useMemo(() => {
|
||||||
const { hidden, show, ...rest } = framerProps as {
|
const { hidden, show, ...rest } = framerProps as {
|
||||||
[name: string]: { [name: string]: number, opacity: number }
|
[name: string]: { [name: string]: number; opacity: number };
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
hidden: {
|
hidden: {
|
||||||
...(hidden ?? {}),
|
...(hidden ?? {}),
|
||||||
opacity: hidden?.opacity ?? 0,
|
opacity: hidden?.opacity ?? 0,
|
||||||
[axis]: hidden?.[axis] ?? directionOffset,
|
[axis]: hidden?.[axis] ?? directionOffset,
|
||||||
},
|
},
|
||||||
show: {
|
show: {
|
||||||
...(show ?? {}),
|
...(show ?? {}),
|
||||||
opacity: show?.opacity ?? 1,
|
opacity: show?.opacity ?? 1,
|
||||||
[axis]: show?.[axis] ?? 0,
|
[axis]: show?.[axis] ?? 0,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}, [directionOffset, axis, framerProps])
|
}, [directionOffset, axis, framerProps]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="show"
|
animate="show"
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
variants={FADE_ANIMATION_VARIANTS}
|
variants={FADE_ANIMATION_VARIANTS}
|
||||||
>
|
>
|
||||||
<motion.span className={className}>{text}</motion.span>
|
<motion.span className={className}>{text}</motion.span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,189 +1,192 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import type * as LabelPrimitive from '@radix-ui/react-label'
|
import type * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import { Slot } from '@radix-ui/react-slot'
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'
|
import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form";
|
||||||
import { Controller, FormProvider, useFormContext, useFormState } from 'react-hook-form'
|
import {
|
||||||
|
Controller,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
useFormState,
|
||||||
|
} from "react-hook-form";
|
||||||
|
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
const Form = FormProvider
|
const Form = FormProvider;
|
||||||
|
|
||||||
interface FormFieldContextValue<
|
interface FormFieldContextValue<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
> {
|
> {
|
||||||
name: TName
|
name: TName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
{} as FormFieldContextValue,
|
{} as FormFieldContextValue,
|
||||||
)
|
);
|
||||||
|
|
||||||
function FormField<
|
function FormField<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
>({ ...props }: ControllerProps<TFieldValues, TName>) {
|
>({ ...props }: ControllerProps<TFieldValues, TName>) {
|
||||||
return (
|
return (
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
<Controller {...props} />
|
<Controller {...props} />
|
||||||
</FormFieldContext.Provider>
|
</FormFieldContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useFormField() {
|
function useFormField() {
|
||||||
const fieldContext = React.useContext(FormFieldContext)
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
const itemContext = React.useContext(FormItemContext)
|
const itemContext = React.useContext(FormItemContext);
|
||||||
const { getFieldState, formState } = useFormContext()
|
const { getFieldState, formState } = useFormContext();
|
||||||
|
|
||||||
const fieldState = getFieldState(fieldContext.name, formState)
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
if (!fieldContext)
|
if (!fieldContext)
|
||||||
throw new Error('useFormField should be used within <FormField>')
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
|
|
||||||
const { id } = itemContext
|
const { id } = itemContext;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: fieldContext.name,
|
name: fieldContext.name,
|
||||||
formItemId: `${id}-form-item`,
|
formItemId: `${id}-form-item`,
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
formMessageId: `${id}-form-item-message`,
|
formMessageId: `${id}-form-item-message`,
|
||||||
...fieldState,
|
...fieldState,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormItemContextValue {
|
interface FormItemContextValue {
|
||||||
id: string
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
{} as FormItemContextValue,
|
{} as FormItemContextValue,
|
||||||
)
|
);
|
||||||
|
|
||||||
const FormItem = React.forwardRef<
|
const FormItem = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const id = React.useId()
|
const id = React.useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItemContext.Provider value={{ id }}>
|
<FormItemContext.Provider value={{ id }}>
|
||||||
<div ref={ref} className={ny('space-y-2', className)} {...props} />
|
<div ref={ref} className={ny("space-y-2", className)} {...props} />
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormItem.displayName = 'FormItem'
|
FormItem.displayName = "FormItem";
|
||||||
|
|
||||||
const FormLabel = React.forwardRef<
|
const FormLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { error, formItemId } = useFormField()
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(error && 'text-destructive', className)}
|
className={ny(error && "text-destructive", className)}
|
||||||
htmlFor={formItemId}
|
htmlFor={formItemId}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormLabel.displayName = 'FormLabel'
|
FormLabel.displayName = "FormLabel";
|
||||||
|
|
||||||
const FormControl = React.forwardRef<
|
const FormControl = React.forwardRef<
|
||||||
React.ElementRef<typeof Slot>,
|
React.ElementRef<typeof Slot>,
|
||||||
React.ComponentPropsWithoutRef<typeof Slot>
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
>(({ ...props }, ref) => {
|
>(({ ...props }, ref) => {
|
||||||
const { error, formItemId, formDescriptionId, formMessageId }
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
= useFormField()
|
useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<Slot
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={formItemId}
|
id={formItemId}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
!error
|
!error
|
||||||
? `${formDescriptionId}`
|
? `${formDescriptionId}`
|
||||||
: `${formDescriptionId} ${formMessageId}`
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
}
|
}
|
||||||
aria-invalid={!!error}
|
aria-invalid={!!error}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormControl.displayName = 'FormControl'
|
FormControl.displayName = "FormControl";
|
||||||
|
|
||||||
const FormDescription = React.forwardRef<
|
const FormDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { formDescriptionId } = useFormField()
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={formDescriptionId}
|
id={formDescriptionId}
|
||||||
className={ny('text-[0.8rem] text-muted-foreground', className)}
|
className={ny("text-[0.8rem] text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormDescription.displayName = 'FormDescription'
|
FormDescription.displayName = "FormDescription";
|
||||||
|
|
||||||
const FormMessage = React.forwardRef<
|
const FormMessage = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, children, ...props }, ref) => {
|
>(({ className, children, ...props }, ref) => {
|
||||||
const { error, formMessageId } = useFormField()
|
const { error, formMessageId } = useFormField();
|
||||||
const body = error ? String(error?.message) : children
|
const body = error ? String(error?.message) : children;
|
||||||
|
|
||||||
if (!body)
|
if (!body) return null;
|
||||||
return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={formMessageId}
|
id={formMessageId}
|
||||||
className={ny('text-[0.8rem] font-medium text-destructive', className)}
|
className={ny("text-[0.8rem] font-medium text-destructive", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormMessage.displayName = 'FormMessage'
|
FormMessage.displayName = "FormMessage";
|
||||||
|
|
||||||
const FormGlobalError = React.forwardRef<
|
const FormGlobalError = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { errors } = useFormState()
|
const { errors } = useFormState();
|
||||||
const rootError = errors.root
|
const rootError = errors.root;
|
||||||
if (!rootError)
|
if (!rootError) return null;
|
||||||
return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('text-sm font-medium text-destructive', className)}
|
className={ny("text-sm font-medium text-destructive", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{rootError.message}
|
{rootError.message}
|
||||||
</p>
|
</p>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
FormGlobalError.displayName = 'FormGlobalError'
|
FormGlobalError.displayName = "FormGlobalError";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
useFormField,
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
FormGlobalError,
|
FormGlobalError,
|
||||||
FormField,
|
FormField,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import type { Variants } from 'framer-motion'
|
import type { Variants } from "framer-motion";
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface GradualSpacingProps {
|
interface GradualSpacingProps {
|
||||||
text: string
|
text: string;
|
||||||
duration?: number
|
duration?: number;
|
||||||
delayMultiple?: number
|
delayMultiple?: number;
|
||||||
framerProps?: Variants
|
framerProps?: Variants;
|
||||||
className?: string
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GradualSpacing({
|
export default function GradualSpacing({
|
||||||
text,
|
text,
|
||||||
duration = 0.5,
|
duration = 0.5,
|
||||||
delayMultiple = 0.04,
|
delayMultiple = 0.04,
|
||||||
framerProps = {
|
framerProps = {
|
||||||
hidden: { opacity: 0, x: -20 },
|
hidden: { opacity: 0, x: -20 },
|
||||||
visible: { opacity: 1, x: 0 },
|
visible: { opacity: 1, x: 0 },
|
||||||
},
|
},
|
||||||
className,
|
className,
|
||||||
}: GradualSpacingProps) {
|
}: GradualSpacingProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center space-x-1">
|
<div className="flex justify-center space-x-1">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{text.split('').map((char, i) => (
|
{text.split("").map((char, i) => (
|
||||||
<motion.h1
|
<motion.h1
|
||||||
key={i}
|
key={i}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
exit="hidden"
|
exit="hidden"
|
||||||
variants={framerProps}
|
variants={framerProps}
|
||||||
transition={{ duration, delay: i * delayMultiple }}
|
transition={{ duration, delay: i * delayMultiple }}
|
||||||
className={ny('drop-shadow-sm ', className)}
|
className={ny("drop-shadow-sm", className)}
|
||||||
>
|
>
|
||||||
{char === ' ' ? <span> </span> : char}
|
{char === " " ? <span> </span> : char}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,71 @@
|
|||||||
import { useId } from 'react'
|
import { useId } from "react";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface GridPatternProps {
|
interface GridPatternProps {
|
||||||
width?: any
|
width?: any;
|
||||||
height?: any
|
height?: any;
|
||||||
x?: any
|
x?: any;
|
||||||
y?: any
|
y?: any;
|
||||||
squares?: Array<[x: number, y: number]>
|
squares?: Array<[x: number, y: number]>;
|
||||||
strokeDasharray?: any
|
strokeDasharray?: any;
|
||||||
className?: string
|
className?: string;
|
||||||
[key: string]: any
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GridPattern({
|
export function GridPattern({
|
||||||
width = 40,
|
width = 40,
|
||||||
height = 40,
|
height = 40,
|
||||||
x = -1,
|
x = -1,
|
||||||
y = -1,
|
y = -1,
|
||||||
strokeDasharray = 0,
|
strokeDasharray = 0,
|
||||||
squares,
|
squares,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: GridPatternProps) {
|
}: GridPatternProps) {
|
||||||
const id = useId()
|
const id = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={ny(
|
className={ny(
|
||||||
'pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30',
|
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<pattern
|
<pattern
|
||||||
id={id}
|
id={id}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
patternUnits="userSpaceOnUse"
|
patternUnits="userSpaceOnUse"
|
||||||
x={x}
|
x={x}
|
||||||
y={y}
|
y={y}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d={`M.5 ${height}V.5H${width}`}
|
d={`M.5 ${height}V.5H${width}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
/>
|
/>
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
|
||||||
{squares && (
|
{squares && (
|
||||||
<svg x={x} y={y} className="overflow-visible">
|
<svg x={x} y={y} className="overflow-visible">
|
||||||
{squares.map(([x, y]) => (
|
{squares.map(([x, y]) => (
|
||||||
<rect
|
<rect
|
||||||
strokeWidth="0"
|
strokeWidth="0"
|
||||||
key={`${x}-${y}`}
|
key={`${x}-${y}`}
|
||||||
width={width - 1}
|
width={width - 1}
|
||||||
height={height - 1}
|
height={height - 1}
|
||||||
x={x * width + 1}
|
x={x * width + 1}
|
||||||
y={y * height + 1}
|
y={y * height + 1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GridPattern
|
export default GridPattern;
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import { type VariantProps, cva } from 'class-variance-authority'
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
|
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
const labelVariants = cva(
|
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<
|
const Label = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
VariantProps<typeof labelVariants>
|
VariantProps<typeof labelVariants>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(labelVariants(), className)}
|
className={ny(labelVariants(), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Label }
|
export { Label };
|
||||||
|
|||||||
@@ -1,128 +1,128 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||||
import { cva } from "class-variance-authority"
|
import { cva } from "class-variance-authority";
|
||||||
import { ChevronDown } from "lucide-react"
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
import { ny } from "@/lib/utils"
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
const NavigationMenu = React.forwardRef<
|
const NavigationMenu = React.forwardRef<
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<NavigationMenuPrimitive.Root
|
<NavigationMenuPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<NavigationMenuViewport />
|
<NavigationMenuViewport />
|
||||||
</NavigationMenuPrimitive.Root>
|
</NavigationMenuPrimitive.Root>
|
||||||
))
|
));
|
||||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
||||||
|
|
||||||
const NavigationMenuList = React.forwardRef<
|
const NavigationMenuList = React.forwardRef<
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<NavigationMenuPrimitive.List
|
<NavigationMenuPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||||
|
|
||||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||||
|
|
||||||
const navigationMenuTriggerStyle = cva(
|
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<
|
const NavigationMenuTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<NavigationMenuPrimitive.Trigger
|
<NavigationMenuPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(navigationMenuTriggerStyle(), "group", className)}
|
className={ny(navigationMenuTriggerStyle(), "group", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}{" "}
|
{children}{" "}
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</NavigationMenuPrimitive.Trigger>
|
</NavigationMenuPrimitive.Trigger>
|
||||||
))
|
));
|
||||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const NavigationMenuContent = React.forwardRef<
|
const NavigationMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<NavigationMenuPrimitive.Content
|
<NavigationMenuPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
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 ",
|
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
||||||
|
|
||||||
const NavigationMenuViewport = React.forwardRef<
|
const NavigationMenuViewport = React.forwardRef<
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div className={ny("absolute left-0 right-0 top-full flex justify-center")}>
|
<div className={ny("absolute left-0 right-0 top-full flex justify-center")}>
|
||||||
<NavigationMenuPrimitive.Viewport
|
<NavigationMenuPrimitive.Viewport
|
||||||
className={ny(
|
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)]",
|
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-fit overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
));
|
||||||
NavigationMenuViewport.displayName =
|
NavigationMenuViewport.displayName =
|
||||||
NavigationMenuPrimitive.Viewport.displayName
|
NavigationMenuPrimitive.Viewport.displayName;
|
||||||
|
|
||||||
const NavigationMenuIndicator = React.forwardRef<
|
const NavigationMenuIndicator = React.forwardRef<
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<NavigationMenuPrimitive.Indicator
|
<NavigationMenuPrimitive.Indicator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
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",
|
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||||
</NavigationMenuPrimitive.Indicator>
|
</NavigationMenuPrimitive.Indicator>
|
||||||
))
|
));
|
||||||
NavigationMenuIndicator.displayName =
|
NavigationMenuIndicator.displayName =
|
||||||
NavigationMenuPrimitive.Indicator.displayName
|
NavigationMenuPrimitive.Indicator.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
navigationMenuTriggerStyle,
|
navigationMenuTriggerStyle,
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
NavigationMenuList,
|
NavigationMenuList,
|
||||||
NavigationMenuItem,
|
NavigationMenuItem,
|
||||||
NavigationMenuContent,
|
NavigationMenuContent,
|
||||||
NavigationMenuTrigger,
|
NavigationMenuTrigger,
|
||||||
NavigationMenuLink,
|
NavigationMenuLink,
|
||||||
NavigationMenuIndicator,
|
NavigationMenuIndicator,
|
||||||
NavigationMenuViewport,
|
NavigationMenuViewport,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,144 +1,144 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
import type { CSSProperties, ReactElement, ReactNode } from "react";
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface NeonColorsProps {
|
interface NeonColorsProps {
|
||||||
firstColor: string
|
firstColor: string;
|
||||||
secondColor: string
|
secondColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NeonGradientCardProps {
|
interface NeonGradientCardProps {
|
||||||
/**
|
/**
|
||||||
* @default <div />
|
* @default <div />
|
||||||
* @type ReactElement
|
* @type ReactElement
|
||||||
* @description
|
* @description
|
||||||
* The component to be rendered as the card
|
* The component to be rendered as the card
|
||||||
*/
|
*/
|
||||||
as?: ReactElement
|
as?: ReactElement;
|
||||||
/**
|
/**
|
||||||
* @default ""
|
* @default ""
|
||||||
* @type string
|
* @type string
|
||||||
* @description
|
* @description
|
||||||
* The className of the card
|
* The className of the card
|
||||||
*/
|
*/
|
||||||
className?: string
|
className?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default ""
|
* @default ""
|
||||||
* @type ReactNode
|
* @type ReactNode
|
||||||
* @description
|
* @description
|
||||||
* The children of the card
|
* The children of the card
|
||||||
*/
|
*/
|
||||||
children?: ReactNode
|
children?: ReactNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default 5
|
* @default 5
|
||||||
* @type number
|
* @type number
|
||||||
* @description
|
* @description
|
||||||
* The size of the border in pixels
|
* The size of the border in pixels
|
||||||
*/
|
*/
|
||||||
borderSize?: number
|
borderSize?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default 20
|
* @default 20
|
||||||
* @type number
|
* @type number
|
||||||
* @description
|
* @description
|
||||||
* The size of the radius in pixels
|
* The size of the radius in pixels
|
||||||
*/
|
*/
|
||||||
borderRadius?: number
|
borderRadius?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default "{ firstColor: '#ff00aa', secondColor: '#00FFF1' }"
|
* @default "{ firstColor: '#ff00aa', secondColor: '#00FFF1' }"
|
||||||
* @type string
|
* @type string
|
||||||
* @description
|
* @description
|
||||||
* The colors of the neon gradient
|
* The colors of the neon gradient
|
||||||
*/
|
*/
|
||||||
neonColors?: NeonColorsProps
|
neonColors?: NeonColorsProps;
|
||||||
|
|
||||||
[key: string]: any
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NeonGradientCard: React.FC<NeonGradientCardProps> = ({
|
const NeonGradientCard: React.FC<NeonGradientCardProps> = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
borderSize = 2,
|
borderSize = 2,
|
||||||
borderRadius = 20,
|
borderRadius = 20,
|
||||||
neonColors = {
|
neonColors = {
|
||||||
firstColor: '#ff00aa',
|
firstColor: "#ff00aa",
|
||||||
secondColor: '#00FFF1',
|
secondColor: "#00FFF1",
|
||||||
},
|
},
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateDimensions = () => {
|
const updateDimensions = () => {
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
const { offsetWidth, offsetHeight } = containerRef.current
|
const { offsetWidth, offsetHeight } = containerRef.current;
|
||||||
setDimensions({ width: offsetWidth, height: offsetHeight })
|
setDimensions({ width: offsetWidth, height: offsetHeight });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
updateDimensions()
|
updateDimensions();
|
||||||
window.addEventListener('resize', updateDimensions)
|
window.addEventListener("resize", updateDimensions);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', updateDimensions)
|
window.removeEventListener("resize", updateDimensions);
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
const { offsetWidth, offsetHeight } = containerRef.current
|
const { offsetWidth, offsetHeight } = containerRef.current;
|
||||||
setDimensions({ width: offsetWidth, height: offsetHeight })
|
setDimensions({ width: offsetWidth, height: offsetHeight });
|
||||||
}
|
}
|
||||||
}, [children])
|
}, [children]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--border-size': `${borderSize}px`,
|
"--border-size": `${borderSize}px`,
|
||||||
'--border-radius': `${borderRadius}px`,
|
"--border-radius": `${borderRadius}px`,
|
||||||
'--neon-first-color': neonColors.firstColor,
|
"--neon-first-color": neonColors.firstColor,
|
||||||
'--neon-second-color': neonColors.secondColor,
|
"--neon-second-color": neonColors.secondColor,
|
||||||
'--card-width': `${dimensions.width}px`,
|
"--card-width": `${dimensions.width}px`,
|
||||||
'--card-height': `${dimensions.height}px`,
|
"--card-height": `${dimensions.height}px`,
|
||||||
'--card-content-radius': `${borderRadius - borderSize}px`,
|
"--card-content-radius": `${borderRadius - borderSize}px`,
|
||||||
'--pseudo-element-background-image': `linear-gradient(0deg, ${neonColors.firstColor}, ${neonColors.secondColor})`,
|
"--pseudo-element-background-image": `linear-gradient(0deg, ${neonColors.firstColor}, ${neonColors.secondColor})`,
|
||||||
'--pseudo-element-width': `${dimensions.width + borderSize * 2}px`,
|
"--pseudo-element-width": `${dimensions.width + borderSize * 2}px`,
|
||||||
'--pseudo-element-height': `${dimensions.height + borderSize * 2}px`,
|
"--pseudo-element-height": `${dimensions.height + borderSize * 2}px`,
|
||||||
'--after-blur': `${dimensions.width / 3}px`,
|
"--after-blur": `${dimensions.width / 3}px`,
|
||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}
|
||||||
className={ny(
|
className={ny(
|
||||||
'relative z-10 size-full rounded-[var(--border-radius)]',
|
"relative z-10 size-full rounded-[var(--border-radius)]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={ny(
|
className={ny(
|
||||||
'relative size-full min-h-[inherit] rounded-[var(--card-content-radius)] bg-gray-100 p-6',
|
"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: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: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:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] before:bg-[length:100%_200%]",
|
||||||
'before:animate-backgroundPositionSpin',
|
"before:animate-backgroundPositionSpin",
|
||||||
'after:absolute after:-left-[var(--border-size)] after:-top-[var(--border-size)] after:-z-10 after:block',
|
"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: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:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] after:bg-[length:100%_200%] after:opacity-80",
|
||||||
'after:animate-backgroundPositionSpin',
|
"after:animate-backgroundPositionSpin",
|
||||||
'dark:bg-neutral-900',
|
"dark:bg-neutral-900",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { NeonGradientCard }
|
export { NeonGradientCard };
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
export default function OrbitingCircles({
|
export default function OrbitingCircles({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
reverse,
|
reverse,
|
||||||
duration = 20,
|
duration = 20,
|
||||||
delay = 10,
|
delay = 10,
|
||||||
radius = 50,
|
radius = 50,
|
||||||
path = true,
|
path = true,
|
||||||
}: {
|
}: {
|
||||||
className?: string
|
className?: string;
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
reverse?: boolean
|
reverse?: boolean;
|
||||||
duration?: number
|
duration?: number;
|
||||||
delay?: number
|
delay?: number;
|
||||||
radius?: number
|
radius?: number;
|
||||||
path?: boolean
|
path?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{path && (
|
{path && (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
className="pointer-events-none absolute inset-0 size-full"
|
className="pointer-events-none absolute inset-0 size-full"
|
||||||
>
|
>
|
||||||
<circle
|
<circle
|
||||||
className="stroke-black/10 stroke-1 dark:stroke-white/10"
|
className="stroke-black/10 stroke-1 dark:stroke-white/10"
|
||||||
cx="50%"
|
cx="50%"
|
||||||
cy="50%"
|
cy="50%"
|
||||||
r={radius}
|
r={radius}
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeDasharray="4 4"
|
strokeDasharray="4 4"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--duration': duration,
|
"--duration": duration,
|
||||||
'--radius': radius,
|
"--radius": radius,
|
||||||
'--delay': -delay,
|
"--delay": -delay,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
className={ny(
|
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',
|
"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 },
|
{ "[animation-direction:reverse]": reverse },
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,271 +1,272 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
interface MousePosition {
|
interface MousePosition {
|
||||||
x: number
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MousePosition(): MousePosition {
|
function MousePosition(): MousePosition {
|
||||||
const [mousePosition, setMousePosition] = useState<MousePosition>({
|
const [mousePosition, setMousePosition] = useState<MousePosition>({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
})
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
setMousePosition({ x: event.clientX, y: event.clientY })
|
setMousePosition({ x: event.clientX, y: event.clientY });
|
||||||
}
|
};
|
||||||
|
|
||||||
window.addEventListener('mousemove', handleMouseMove)
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('mousemove', handleMouseMove)
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return mousePosition
|
return mousePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParticlesProps {
|
interface ParticlesProps {
|
||||||
className?: string
|
className?: string;
|
||||||
quantity?: number
|
quantity?: number;
|
||||||
staticity?: number
|
staticity?: number;
|
||||||
ease?: number
|
ease?: number;
|
||||||
size?: number
|
size?: number;
|
||||||
refresh?: boolean
|
refresh?: boolean;
|
||||||
color?: string
|
color?: string;
|
||||||
vx?: number
|
vx?: number;
|
||||||
vy?: number
|
vy?: number;
|
||||||
}
|
}
|
||||||
function hexToRgb(hex: string): number[] {
|
function hexToRgb(hex: string): number[] {
|
||||||
hex = hex.replace('#', '')
|
hex = hex.replace("#", "");
|
||||||
const hexInt = Number.parseInt(hex, 16)
|
const hexInt = Number.parseInt(hex, 16);
|
||||||
const red = (hexInt >> 16) & 255
|
const red = (hexInt >> 16) & 255;
|
||||||
const green = (hexInt >> 8) & 255
|
const green = (hexInt >> 8) & 255;
|
||||||
const blue = hexInt & 255
|
const blue = hexInt & 255;
|
||||||
return [red, green, blue]
|
return [red, green, blue];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Particles: React.FC<ParticlesProps> = ({
|
const Particles: React.FC<ParticlesProps> = ({
|
||||||
className = '',
|
className = "",
|
||||||
quantity = 100,
|
quantity = 100,
|
||||||
staticity = 50,
|
staticity = 50,
|
||||||
ease = 50,
|
ease = 50,
|
||||||
size = 0.4,
|
size = 0.4,
|
||||||
refresh = false,
|
refresh = false,
|
||||||
color = '#ffffff',
|
color = "#ffffff",
|
||||||
vx = 0,
|
vx = 0,
|
||||||
vy = 0,
|
vy = 0,
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const canvasContainerRef = useRef<HTMLDivElement>(null)
|
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const context = useRef<CanvasRenderingContext2D | null>(null)
|
const context = useRef<CanvasRenderingContext2D | null>(null);
|
||||||
const circles = useRef<any[]>([])
|
const circles = useRef<any[]>([]);
|
||||||
const mousePosition = MousePosition()
|
const mousePosition = MousePosition();
|
||||||
const mouse = useRef<{ x: number, y: number }>({ x: 0, y: 0 })
|
const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
const canvasSize = useRef<{ w: number, h: number }>({ w: 0, h: 0 })
|
const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
|
||||||
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1
|
const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canvasRef.current) {
|
if (canvasRef.current) {
|
||||||
context.current = canvasRef.current.getContext('2d')
|
context.current = canvasRef.current.getContext("2d");
|
||||||
}
|
}
|
||||||
initCanvas()
|
initCanvas();
|
||||||
animate()
|
animate();
|
||||||
window.addEventListener('resize', initCanvas)
|
window.addEventListener("resize", initCanvas);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', initCanvas)
|
window.removeEventListener("resize", initCanvas);
|
||||||
}
|
};
|
||||||
}, [color])
|
}, [color]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onMouseMove()
|
onMouseMove();
|
||||||
}, [mousePosition.x, mousePosition.y])
|
}, [mousePosition.x, mousePosition.y]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initCanvas()
|
initCanvas();
|
||||||
}, [refresh])
|
}, [refresh]);
|
||||||
|
|
||||||
const initCanvas = () => {
|
const initCanvas = () => {
|
||||||
resizeCanvas()
|
resizeCanvas();
|
||||||
drawParticles()
|
drawParticles();
|
||||||
}
|
};
|
||||||
|
|
||||||
const onMouseMove = () => {
|
const onMouseMove = () => {
|
||||||
if (canvasRef.current) {
|
if (canvasRef.current) {
|
||||||
const rect = canvasRef.current.getBoundingClientRect()
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
const { w, h } = canvasSize.current
|
const { w, h } = canvasSize.current;
|
||||||
const x = mousePosition.x - rect.left - w / 2
|
const x = mousePosition.x - rect.left - w / 2;
|
||||||
const y = mousePosition.y - rect.top - h / 2
|
const y = mousePosition.y - rect.top - h / 2;
|
||||||
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2
|
const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
|
||||||
if (inside) {
|
if (inside) {
|
||||||
mouse.current.x = x
|
mouse.current.x = x;
|
||||||
mouse.current.y = y
|
mouse.current.y = y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
interface Circle {
|
interface Circle {
|
||||||
x: number
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
translateX: number
|
translateX: number;
|
||||||
translateY: number
|
translateY: number;
|
||||||
size: number
|
size: number;
|
||||||
alpha: number
|
alpha: number;
|
||||||
targetAlpha: number
|
targetAlpha: number;
|
||||||
dx: number
|
dx: number;
|
||||||
dy: number
|
dy: number;
|
||||||
magnetism: number
|
magnetism: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resizeCanvas = () => {
|
const resizeCanvas = () => {
|
||||||
if (canvasContainerRef.current && canvasRef.current && context.current) {
|
if (canvasContainerRef.current && canvasRef.current && context.current) {
|
||||||
circles.current.length = 0
|
circles.current.length = 0;
|
||||||
canvasSize.current.w = canvasContainerRef.current.offsetWidth
|
canvasSize.current.w = canvasContainerRef.current.offsetWidth;
|
||||||
canvasSize.current.h = canvasContainerRef.current.offsetHeight
|
canvasSize.current.h = canvasContainerRef.current.offsetHeight;
|
||||||
canvasRef.current.width = canvasSize.current.w * dpr
|
canvasRef.current.width = canvasSize.current.w * dpr;
|
||||||
canvasRef.current.height = canvasSize.current.h * dpr
|
canvasRef.current.height = canvasSize.current.h * dpr;
|
||||||
canvasRef.current.style.width = `${canvasSize.current.w}px`
|
canvasRef.current.style.width = `${canvasSize.current.w}px`;
|
||||||
canvasRef.current.style.height = `${canvasSize.current.h}px`
|
canvasRef.current.style.height = `${canvasSize.current.h}px`;
|
||||||
context.current.scale(dpr, dpr)
|
context.current.scale(dpr, dpr);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const circleParams = (): Circle => {
|
const circleParams = (): Circle => {
|
||||||
const x = Math.floor(Math.random() * canvasSize.current.w)
|
const x = Math.floor(Math.random() * canvasSize.current.w);
|
||||||
const y = Math.floor(Math.random() * canvasSize.current.h)
|
const y = Math.floor(Math.random() * canvasSize.current.h);
|
||||||
const translateX = 0
|
const translateX = 0;
|
||||||
const translateY = 0
|
const translateY = 0;
|
||||||
const pSize = Math.floor(Math.random() * 2) + size
|
const pSize = Math.floor(Math.random() * 2) + size;
|
||||||
const alpha = 0
|
const alpha = 0;
|
||||||
const targetAlpha = Number.parseFloat((Math.random() * 0.6 + 0.1).toFixed(1))
|
const targetAlpha = Number.parseFloat(
|
||||||
const dx = (Math.random() - 0.5) * 0.1
|
(Math.random() * 0.6 + 0.1).toFixed(1),
|
||||||
const dy = (Math.random() - 0.5) * 0.1
|
);
|
||||||
const magnetism = 0.1 + Math.random() * 4
|
const dx = (Math.random() - 0.5) * 0.1;
|
||||||
return {
|
const dy = (Math.random() - 0.5) * 0.1;
|
||||||
x,
|
const magnetism = 0.1 + Math.random() * 4;
|
||||||
y,
|
return {
|
||||||
translateX,
|
x,
|
||||||
translateY,
|
y,
|
||||||
size: pSize,
|
translateX,
|
||||||
alpha,
|
translateY,
|
||||||
targetAlpha,
|
size: pSize,
|
||||||
dx,
|
alpha,
|
||||||
dy,
|
targetAlpha,
|
||||||
magnetism,
|
dx,
|
||||||
}
|
dy,
|
||||||
}
|
magnetism,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const rgb = hexToRgb(color)
|
const rgb = hexToRgb(color);
|
||||||
|
|
||||||
const drawCircle = (circle: Circle, update = false) => {
|
const drawCircle = (circle: Circle, update = false) => {
|
||||||
if (context.current) {
|
if (context.current) {
|
||||||
const { x, y, translateX, translateY, size, alpha } = circle
|
const { x, y, translateX, translateY, size, alpha } = circle;
|
||||||
context.current.translate(translateX, translateY)
|
context.current.translate(translateX, translateY);
|
||||||
context.current.beginPath()
|
context.current.beginPath();
|
||||||
context.current.arc(x, y, size, 0, 2 * Math.PI)
|
context.current.arc(x, y, size, 0, 2 * Math.PI);
|
||||||
context.current.fillStyle = `rgba(${rgb.join(', ')}, ${alpha})`
|
context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`;
|
||||||
context.current.fill()
|
context.current.fill();
|
||||||
context.current.setTransform(dpr, 0, 0, dpr, 0, 0)
|
context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
if (!update) {
|
if (!update) {
|
||||||
circles.current.push(circle)
|
circles.current.push(circle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const clearContext = () => {
|
const clearContext = () => {
|
||||||
if (context.current) {
|
if (context.current) {
|
||||||
context.current.clearRect(
|
context.current.clearRect(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
canvasSize.current.w,
|
canvasSize.current.w,
|
||||||
canvasSize.current.h,
|
canvasSize.current.h,
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const drawParticles = () => {
|
const drawParticles = () => {
|
||||||
clearContext()
|
clearContext();
|
||||||
const particleCount = quantity
|
const particleCount = quantity;
|
||||||
for (let i = 0; i < particleCount; i++) {
|
for (let i = 0; i < particleCount; i++) {
|
||||||
const circle = circleParams()
|
const circle = circleParams();
|
||||||
drawCircle(circle)
|
drawCircle(circle);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const remapValue = (
|
const remapValue = (
|
||||||
value: number,
|
value: number,
|
||||||
start1: number,
|
start1: number,
|
||||||
end1: number,
|
end1: number,
|
||||||
start2: number,
|
start2: number,
|
||||||
end2: number,
|
end2: number,
|
||||||
): number => {
|
): number => {
|
||||||
const remapped
|
const remapped =
|
||||||
= ((value - start1) * (end2 - start2)) / (end1 - start1) + start2
|
((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||||
return remapped > 0 ? remapped : 0
|
return remapped > 0 ? remapped : 0;
|
||||||
}
|
};
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
clearContext()
|
clearContext();
|
||||||
circles.current.forEach((circle: Circle, i: number) => {
|
circles.current.forEach((circle: Circle, i: number) => {
|
||||||
// Handle the alpha value
|
// Handle the alpha value
|
||||||
const edge = [
|
const edge = [
|
||||||
circle.x + circle.translateX - circle.size, // distance from left edge
|
circle.x + circle.translateX - circle.size, // distance from left edge
|
||||||
canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
|
canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
|
||||||
circle.y + circle.translateY - circle.size, // distance from top edge
|
circle.y + circle.translateY - circle.size, // distance from top edge
|
||||||
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
||||||
]
|
];
|
||||||
const closestEdge = edge.reduce((a, b) => Math.min(a, b))
|
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
||||||
const remapClosestEdge = Number.parseFloat(
|
const remapClosestEdge = Number.parseFloat(
|
||||||
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
|
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
|
||||||
)
|
);
|
||||||
if (remapClosestEdge > 1) {
|
if (remapClosestEdge > 1) {
|
||||||
circle.alpha += 0.02
|
circle.alpha += 0.02;
|
||||||
if (circle.alpha > circle.targetAlpha) {
|
if (circle.alpha > circle.targetAlpha) {
|
||||||
circle.alpha = circle.targetAlpha
|
circle.alpha = circle.targetAlpha;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
||||||
circle.alpha = circle.targetAlpha * remapClosestEdge
|
}
|
||||||
}
|
circle.x += circle.dx + vx;
|
||||||
circle.x += circle.dx + vx
|
circle.y += circle.dy + vy;
|
||||||
circle.y += circle.dy + vy
|
circle.translateX +=
|
||||||
circle.translateX
|
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
|
||||||
+= (mouse.current.x / (staticity / circle.magnetism) - circle.translateX)
|
ease;
|
||||||
/ ease
|
circle.translateY +=
|
||||||
circle.translateY
|
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
|
||||||
+= (mouse.current.y / (staticity / circle.magnetism) - circle.translateY)
|
ease;
|
||||||
/ ease
|
|
||||||
|
|
||||||
drawCircle(circle, true)
|
drawCircle(circle, true);
|
||||||
|
|
||||||
// circle gets out of the canvas
|
// circle gets out of the canvas
|
||||||
if (
|
if (
|
||||||
circle.x < -circle.size
|
circle.x < -circle.size ||
|
||||||
|| circle.x > canvasSize.current.w + circle.size
|
circle.x > canvasSize.current.w + circle.size ||
|
||||||
|| circle.y < -circle.size
|
circle.y < -circle.size ||
|
||||||
|| circle.y > canvasSize.current.h + circle.size
|
circle.y > canvasSize.current.h + circle.size
|
||||||
) {
|
) {
|
||||||
// remove the circle from the array
|
// remove the circle from the array
|
||||||
circles.current.splice(i, 1)
|
circles.current.splice(i, 1);
|
||||||
// create a new circle
|
// create a new circle
|
||||||
const newCircle = circleParams()
|
const newCircle = circleParams();
|
||||||
drawCircle(newCircle)
|
drawCircle(newCircle);
|
||||||
// update the circle position
|
// update the circle position
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
window.requestAnimationFrame(animate)
|
window.requestAnimationFrame(animate);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} ref={canvasContainerRef} aria-hidden="true">
|
<div className={className} ref={canvasContainerRef} aria-hidden="true">
|
||||||
<canvas ref={canvasRef} className="size-full" />
|
<canvas ref={canvasRef} className="size-full" />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Particles
|
export default Particles;
|
||||||
|
|||||||
@@ -1,78 +1,76 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import React from 'react'
|
import React from "react";
|
||||||
|
|
||||||
interface PulsatingButtonProps {
|
interface PulsatingButtonProps {
|
||||||
text: string
|
text: string;
|
||||||
pulseColor: string
|
pulseColor: string;
|
||||||
backgroundColor: string
|
backgroundColor: string;
|
||||||
textColor: string
|
textColor: string;
|
||||||
animationDuration: string
|
animationDuration: string;
|
||||||
buttonWidth: string
|
buttonWidth: string;
|
||||||
buttonHeight: string
|
buttonHeight: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PulsatingButton: React.FC<PulsatingButtonProps> = ({
|
export const PulsatingButton: React.FC<PulsatingButtonProps> = ({
|
||||||
text,
|
text,
|
||||||
pulseColor,
|
pulseColor,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
textColor,
|
textColor,
|
||||||
animationDuration,
|
animationDuration,
|
||||||
buttonWidth,
|
buttonWidth,
|
||||||
buttonHeight,
|
buttonHeight,
|
||||||
}) => {
|
}) => {
|
||||||
const pulseKeyframes = {
|
const pulseKeyframes = {
|
||||||
'--tw-pulse-color': pulseColor,
|
"--tw-pulse-color": pulseColor,
|
||||||
'animation': `pulse ${animationDuration} linear infinite`,
|
animation: `pulse ${animationDuration} linear infinite`,
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex items-center justify-center">
|
||||||
className="flex justify-center items-center"
|
<button
|
||||||
>
|
className="relative block flex cursor-pointer items-center justify-center text-center"
|
||||||
<button
|
style={{
|
||||||
className="relative block text-center cursor-pointer flex justify-center items-center"
|
color: textColor,
|
||||||
style={{
|
backgroundColor,
|
||||||
color: textColor,
|
width: buttonWidth,
|
||||||
backgroundColor,
|
height: buttonHeight,
|
||||||
width: buttonWidth,
|
borderRadius: "12px",
|
||||||
height: buttonHeight,
|
...pulseKeyframes,
|
||||||
borderRadius: '12px',
|
}}
|
||||||
...pulseKeyframes,
|
>
|
||||||
}}
|
<div>{text}</div>
|
||||||
>
|
<style jsx>
|
||||||
<div>{text}</div>
|
{`
|
||||||
<style jsx>
|
@keyframes pulse {
|
||||||
{`
|
0% {
|
||||||
@keyframes pulse {
|
box-shadow: 0 0 0 0 rgba(var(--tw-pulse-color), 0);
|
||||||
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);
|
||||||
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);
|
||||||
100% {
|
}
|
||||||
box-shadow: 0 0 0 0 rgba(var(--tw-pulse-color), 0);
|
}
|
||||||
}
|
button::before {
|
||||||
}
|
content: "";
|
||||||
button::before {
|
position: absolute;
|
||||||
content: '';
|
top: 50%;
|
||||||
position: absolute;
|
left: 50%;
|
||||||
top: 50%;
|
width: 100%;
|
||||||
left: 50%;
|
height: 100%;
|
||||||
width: 100%;
|
border-radius: 20px;
|
||||||
height: 100%;
|
background: inherit;
|
||||||
border-radius: 20px;
|
animation: inherit;
|
||||||
background: inherit;
|
transform: translate(-50%, -50%);
|
||||||
animation: inherit;
|
z-index: -1;
|
||||||
transform: translate(-50%, -50%);
|
}
|
||||||
z-index: -1;
|
`}
|
||||||
}
|
</style>
|
||||||
`}
|
</button>
|
||||||
</style>
|
</div>
|
||||||
</button>
|
);
|
||||||
</div>
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PulsatingButton
|
export default PulsatingButton;
|
||||||
|
|||||||
@@ -1,77 +1,77 @@
|
|||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
type Type = 'circle' | 'ellipse'
|
type Type = "circle" | "ellipse";
|
||||||
|
|
||||||
type Origin =
|
type Origin =
|
||||||
| 'center'
|
| "center"
|
||||||
| 'top'
|
| "top"
|
||||||
| 'bottom'
|
| "bottom"
|
||||||
| 'left'
|
| "left"
|
||||||
| 'right'
|
| "right"
|
||||||
| 'top left'
|
| "top left"
|
||||||
| 'top right'
|
| "top right"
|
||||||
| 'bottom left'
|
| "bottom left"
|
||||||
| 'bottom right'
|
| "bottom right";
|
||||||
|
|
||||||
interface RadialProps {
|
interface RadialProps {
|
||||||
/**
|
/**
|
||||||
* The type of radial gradient
|
* The type of radial gradient
|
||||||
* @default circle
|
* @default circle
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
type?: Type
|
type?: Type;
|
||||||
/**
|
/**
|
||||||
* The color to transition from
|
* The color to transition from
|
||||||
* @default #00000000
|
* @default #00000000
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
from?: string
|
from?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color to transition to
|
* The color to transition to
|
||||||
* @default #290A5C
|
* @default #290A5C
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
to?: string
|
to?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The size of the gradient in pixels
|
* The size of the gradient in pixels
|
||||||
* @default 300
|
* @default 300
|
||||||
* @type number
|
* @type number
|
||||||
*/
|
*/
|
||||||
size?: number
|
size?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The origin of the gradient
|
* The origin of the gradient
|
||||||
* @default center
|
* @default center
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
origin?: Origin
|
origin?: Origin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class name to apply to the gradient
|
* The class name to apply to the gradient
|
||||||
* @default ""
|
* @default ""
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
className?: string
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RadialGradient({
|
function RadialGradient({
|
||||||
type = 'circle',
|
type = "circle",
|
||||||
from = 'rgba(120,119,198,0.3)',
|
from = "rgba(120,119,198,0.3)",
|
||||||
to = 'hsla(0, 0%, 0%, 0)',
|
to = "hsla(0, 0%, 0%, 0)",
|
||||||
size = 300,
|
size = 300,
|
||||||
origin = 'center',
|
origin = "center",
|
||||||
className,
|
className,
|
||||||
}: RadialProps) {
|
}: RadialProps) {
|
||||||
const styles: CSSProperties = {
|
const styles: CSSProperties = {
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
pointerEvents: 'none',
|
pointerEvents: "none",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
backgroundImage: `radial-gradient(${type} ${size}px at ${origin}, ${from}, ${to})`,
|
backgroundImage: `radial-gradient(${type} ${size}px at ${origin}, ${from}, ${to})`,
|
||||||
}
|
};
|
||||||
|
|
||||||
return <div className={className} style={styles} />
|
return <div className={className} style={styles} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RadialGradient
|
export default RadialGradient;
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
export default function RetroGrid({ className }: { className?: string }) {
|
export default function RetroGrid({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ny(
|
className={ny(
|
||||||
'pointer-events-none absolute h-full w-full overflow-hidden opacity-50 [perspective:200px]',
|
"pointer-events-none absolute h-full w-full overflow-hidden opacity-50 [perspective:200px]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Grid */}
|
{/* Grid */}
|
||||||
<div className="absolute inset-0 [transform:rotateX(35deg)]">
|
<div className="absolute inset-0 [transform:rotateX(35deg)]">
|
||||||
<div
|
<div
|
||||||
className={ny(
|
className={ny(
|
||||||
'animate-grid',
|
"animate-grid",
|
||||||
|
|
||||||
'[background-repeat:repeat] [background-size:60px_60px] [height:300vh] [inset:0%_0px] [margin-left:-50%] [transform-origin:100%_0_0] [width:600vw]',
|
"[background-repeat:repeat] [background-size:60px_60px] [height:300vh] [inset:0%_0px] [margin-left:-50%] [transform-origin:100%_0_0] [width:600vw]",
|
||||||
|
|
||||||
// Light Styles
|
// Light Styles
|
||||||
'[background-image:linear-gradient(to_right,rgba(0,0,0,0.3)_1px,transparent_0),linear-gradient(to_bottom,rgba(0,0,0,0.3)_1px,transparent_0)]',
|
"[background-image:linear-gradient(to_right,rgba(0,0,0,0.3)_1px,transparent_0),linear-gradient(to_bottom,rgba(0,0,0,0.3)_1px,transparent_0)]",
|
||||||
|
|
||||||
// Dark styles
|
// Dark styles
|
||||||
'dark:[background-image:linear-gradient(to_right,rgba(255,255,255,0.2)_1px,transparent_0),linear-gradient(to_bottom,rgba(255,255,255,0.2)_1px,transparent_0)]',
|
"dark:[background-image:linear-gradient(to_right,rgba(255,255,255,0.2)_1px,transparent_0),linear-gradient(to_bottom,rgba(255,255,255,0.2)_1px,transparent_0)]",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Background Gradient */}
|
{/* Background Gradient */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-white to-transparent to-90% dark:from-black" />
|
<div className="absolute inset-0 bg-gradient-to-t from-white to-transparent to-90% dark:from-black" />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||||
|
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef<
|
const ScrollArea = React.forwardRef<
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<ScrollAreaPrimitive.Root
|
<ScrollAreaPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('relative overflow-hidden', className)}
|
className={ny("relative overflow-hidden", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]">
|
<ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]">
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
<ScrollBar />
|
<ScrollBar />
|
||||||
<ScrollAreaPrimitive.Corner />
|
<ScrollAreaPrimitive.Corner />
|
||||||
</ScrollAreaPrimitive.Root>
|
</ScrollAreaPrimitive.Root>
|
||||||
))
|
));
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
const ScrollBar = React.forwardRef<
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
ref={ref}
|
ref={ref}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={ny(
|
className={ny(
|
||||||
'flex touch-none select-none transition-colors',
|
"flex touch-none select-none transition-colors",
|
||||||
orientation === 'vertical'
|
orientation === "vertical" &&
|
||||||
&& 'h-full w-2.5 border-l border-l-transparent p-px',
|
"h-full w-2.5 border-l border-l-transparent p-px",
|
||||||
orientation === 'horizontal'
|
orientation === "horizontal" &&
|
||||||
&& 'h-2.5 flex-col border-t border-t-transparent p-px',
|
"h-2.5 flex-col border-t border-t-transparent p-px",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="bg-border relative flex-1 rounded-full" />
|
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
))
|
));
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
export { ScrollArea, ScrollBar };
|
||||||
|
|||||||
@@ -1,168 +1,167 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
CaretSortIcon,
|
CaretSortIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronUpIcon,
|
ChevronUpIcon,
|
||||||
} from '@radix-ui/react-icons'
|
} from "@radix-ui/react-icons";
|
||||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
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<
|
const SelectTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-left text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 [&>span]:text-left',
|
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-left text-sm shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1 [&>span]:text-left",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
if (e.pointerType === 'touch')
|
if (e.pointerType === "touch") e.preventDefault();
|
||||||
e.preventDefault()
|
}}
|
||||||
}}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
<SelectPrimitive.Icon asChild>
|
||||||
<SelectPrimitive.Icon asChild>
|
<CaretSortIcon className="h-4 w-4 shrink-0 opacity-50" />
|
||||||
<CaretSortIcon className="h-4 w-4 shrink-0 opacity-50" />
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Trigger>
|
||||||
</SelectPrimitive.Trigger>
|
));
|
||||||
))
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.ScrollUpButton
|
<SelectPrimitive.ScrollUpButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'flex cursor-default items-center justify-center py-1',
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronUpIcon />
|
<ChevronUpIcon />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
))
|
));
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.ScrollDownButton
|
<SelectPrimitive.ScrollDownButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'flex cursor-default items-center justify-center py-1',
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon />
|
<ChevronDownIcon />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
))
|
));
|
||||||
SelectScrollDownButton.displayName
|
SelectScrollDownButton.displayName =
|
||||||
= SelectPrimitive.ScrollDownButton.displayName
|
SelectPrimitive.ScrollDownButton.displayName;
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
const SelectContent = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
>(({ className, children, position = 'popper', ...props }, ref) => (
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
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',
|
"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'
|
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',
|
"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,
|
className,
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
<SelectScrollUpButton />
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
className={ny(
|
className={ny(
|
||||||
'p-1',
|
"p-1",
|
||||||
position === 'popper'
|
position === "popper" &&
|
||||||
&& 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</SelectPrimitive.Viewport>
|
</SelectPrimitive.Viewport>
|
||||||
<SelectScrollDownButton />
|
<SelectScrollDownButton />
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Content>
|
||||||
</SelectPrimitive.Portal>
|
</SelectPrimitive.Portal>
|
||||||
))
|
));
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
const SelectLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.Label
|
<SelectPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('px-2 py-1.5 text-sm font-semibold', className)}
|
className={ny("px-2 py-1.5 text-sm font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
const SelectItem = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<SelectPrimitive.ItemIndicator>
|
<SelectPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
</SelectPrimitive.ItemIndicator>
|
</SelectPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
))
|
));
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
const SelectSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.Separator
|
<SelectPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('-mx-1 my-1 h-px bg-muted', className)}
|
className={ny("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
SelectGroup,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectLabel,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
SelectScrollUpButton,
|
SelectScrollUpButton,
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,144 +1,144 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
import { Cross2Icon } from '@radix-ui/react-icons'
|
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||||
import { type VariantProps, cva } from 'class-variance-authority'
|
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<
|
const SheetOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
className={ny(
|
className={ny(
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const sheetVariants = cva(
|
const sheetVariants = cva(
|
||||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
|
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
side: {
|
side: {
|
||||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||||
bottom:
|
bottom:
|
||||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-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',
|
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:
|
right:
|
||||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
side: 'right',
|
side: "right",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
interface SheetContentProps
|
interface SheetContentProps
|
||||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||||
VariantProps<typeof sheetVariants> {}
|
VariantProps<typeof sheetVariants> {}
|
||||||
|
|
||||||
const SheetContent = React.forwardRef<
|
const SheetContent = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
SheetContentProps
|
SheetContentProps
|
||||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
>(({ side = "right", className, children, ...props }, ref) => (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(sheetVariants({ side }), className)}
|
className={ny(sheetVariants({ side }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||||
<Cross2Icon className="size-4" />
|
<Cross2Icon className="size-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
{children}
|
{children}
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPortal>
|
||||||
))
|
));
|
||||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||||
|
|
||||||
function SheetHeader({
|
function SheetHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ny(
|
className={ny(
|
||||||
'flex flex-col space-y-2 text-center sm:text-left',
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
SheetHeader.displayName = 'SheetHeader'
|
SheetHeader.displayName = "SheetHeader";
|
||||||
|
|
||||||
function SheetFooter({
|
function SheetFooter({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ny(
|
className={ny(
|
||||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
SheetFooter.displayName = 'SheetFooter'
|
SheetFooter.displayName = "SheetFooter";
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef<
|
const SheetTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('text-foreground text-lg font-semibold', className)}
|
className={ny("text-lg font-semibold text-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||||
|
|
||||||
const SheetDescription = React.forwardRef<
|
const SheetDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Description
|
<SheetPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('text-muted-foreground text-sm', className)}
|
className={ny("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetPortal,
|
SheetPortal,
|
||||||
SheetOverlay,
|
SheetOverlay,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
SheetClose,
|
SheetClose,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetFooter,
|
SheetFooter,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetDescription,
|
SheetDescription,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
type TColorProp = `#${string}` | `#${string}`[]
|
type TColorProp = `#${string}` | `#${string}`[];
|
||||||
interface ShineBorderProps {
|
interface ShineBorderProps {
|
||||||
borderRadius?: number
|
borderRadius?: number;
|
||||||
borderWidth?: number
|
borderWidth?: number;
|
||||||
duration?: number
|
duration?: number;
|
||||||
color?: TColorProp
|
color?: TColorProp;
|
||||||
className?: string
|
className?: string;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,44 +23,43 @@ interface ShineBorderProps {
|
|||||||
* @param children contains react node elements.
|
* @param children contains react node elements.
|
||||||
*/
|
*/
|
||||||
export default function ShineBorder({
|
export default function ShineBorder({
|
||||||
borderRadius = 8,
|
borderRadius = 8,
|
||||||
borderWidth = 1,
|
borderWidth = 1,
|
||||||
duration = 14,
|
duration = 14,
|
||||||
color = '#fff',
|
color = "#fff",
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
}: ShineBorderProps) {
|
}: ShineBorderProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--border-radius': `${borderRadius}px`,
|
"--border-radius": `${borderRadius}px`,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
className={ny(
|
className={ny(
|
||||||
'relative grid min-h-[60px] w-fit min-w-[300px] place-items-center rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white',
|
"relative grid min-h-[60px] w-fit min-w-[300px] place-items-center rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--border-width': `${borderWidth}px`,
|
"--border-width": `${borderWidth}px`,
|
||||||
'--border-radius': `${borderRadius}px`,
|
"--border-radius": `${borderRadius}px`,
|
||||||
'--border-radius-child': `${borderRadius * 0.2}px`,
|
"--border-radius-child": `${borderRadius * 0.2}px`,
|
||||||
'--shine-pulse-duration': `${duration}s`,
|
"--shine-pulse-duration": `${duration}s`,
|
||||||
'--mask-linear-gradient': `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
"--mask-linear-gradient": `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
||||||
'--background-radial-gradient': `radial-gradient(transparent,transparent, ${
|
"--background-radial-gradient": `radial-gradient(transparent,transparent, ${
|
||||||
!Array.isArray(color) ? color : color.join(',')
|
!Array.isArray(color) ? color : color.join(",")
|
||||||
},transparent,transparent)`,
|
},transparent,transparent)`,
|
||||||
} as React.CSSProperties
|
} 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]`}
|
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>
|
<div className="z-[1] h-full w-full rounded-[--border-radius-child]">
|
||||||
<div className="z-[1] h-full w-full rounded-[--border-radius-child]">
|
{children}
|
||||||
{children}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,51 @@
|
|||||||
'use client'
|
"use client";
|
||||||
import { type AnimationProps, motion } from 'framer-motion'
|
import { type AnimationProps, motion } from "framer-motion";
|
||||||
|
|
||||||
const animationProps = {
|
const animationProps = {
|
||||||
initial: { '--x': '100%', 'scale': 0.8 },
|
initial: { "--x": "100%", scale: 0.8 },
|
||||||
animate: { '--x': '-100%', 'scale': 1 },
|
animate: { "--x": "-100%", scale: 1 },
|
||||||
whileTap: { scale: 0.95 },
|
whileTap: { scale: 0.95 },
|
||||||
transition: {
|
transition: {
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
repeatType: 'loop',
|
repeatType: "loop",
|
||||||
repeatDelay: 1,
|
repeatDelay: 1,
|
||||||
type: 'spring',
|
type: "spring",
|
||||||
stiffness: 20,
|
stiffness: 20,
|
||||||
damping: 15,
|
damping: 15,
|
||||||
mass: 2,
|
mass: 2,
|
||||||
scale: {
|
scale: {
|
||||||
type: 'spring',
|
type: "spring",
|
||||||
stiffness: 200,
|
stiffness: 200,
|
||||||
damping: 5,
|
damping: 5,
|
||||||
mass: 0.5,
|
mass: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as AnimationProps
|
} as AnimationProps;
|
||||||
|
|
||||||
function ShinyButton({ text = 'shiny-button' }) {
|
function ShinyButton({ text = "shiny-button" }) {
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
{...animationProps}
|
{...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%)]"
|
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
|
<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%)]"
|
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={{
|
style={{
|
||||||
maskImage:
|
maskImage:
|
||||||
'linear-gradient(-75deg,hsl(var(--primary)) calc(var(--x) + 20%),transparent calc(var(--x) + 30%),hsl(var(--primary)) calc(var(--x) + 100%))',
|
"linear-gradient(-75deg,hsl(var(--primary)) calc(var(--x) + 20%),transparent calc(var(--x) + 30%),hsl(var(--primary)) calc(var(--x) + 100%))",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
mask: 'linear-gradient(rgb(0,0,0), rgb(0,0,0)) content-box,linear-gradient(rgb(0,0,0), rgb(0,0,0))',
|
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',
|
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"
|
className="absolute inset-0 z-10 block rounded-[inherit] bg-[linear-gradient(-75deg,hsl(var(--primary)/10%)_calc(var(--x)+20%),hsl(var(--primary)/50%)_calc(var(--x)+25%),hsl(var(--primary)/10%)_calc(var(--x)+100%))] p-px"
|
||||||
>
|
></span>
|
||||||
</span>
|
</motion.button>
|
||||||
</motion.button>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ShinyButton
|
export default ShinyButton;
|
||||||
|
|||||||
@@ -1,136 +1,189 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as SliderPrimitive from '@radix-ui/react-slider'
|
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> {
|
interface SliderProps
|
||||||
showSteps?: 'none' | 'half' | 'full'
|
extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
|
||||||
formatLabel?: (value: number) => string
|
showSteps?: "none" | "half" | "full";
|
||||||
formatLabelSide?: string
|
formatLabel?: (value: number) => string;
|
||||||
|
formatLabelSide?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
const Slider = React.forwardRef<
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
SliderProps
|
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)
|
className,
|
||||||
const stepLines = Array.from({ length: numberOfSteps }, (_, index) => index * step + min)
|
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 initialValue = Array.isArray(value)
|
||||||
const [localValues, setLocalValues] = React.useState<number[]>(initialValue)
|
? value
|
||||||
|
: Array.isArray(defaultValue)
|
||||||
|
? defaultValue
|
||||||
|
: [min, max];
|
||||||
|
const [localValues, setLocalValues] =
|
||||||
|
React.useState<number[]>(initialValue);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isEqual(value, localValues))
|
if (!isEqual(value, localValues))
|
||||||
setLocalValues(Array.isArray(value) ? value : (Array.isArray(defaultValue) ? defaultValue : [min, max]))
|
setLocalValues(
|
||||||
}, [min, max, value])
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: Array.isArray(defaultValue)
|
||||||
|
? defaultValue
|
||||||
|
: [min, max],
|
||||||
|
);
|
||||||
|
}, [min, max, value]);
|
||||||
|
|
||||||
const handleValueChange = (newValues: number[]) => {
|
const handleValueChange = (newValues: number[]) => {
|
||||||
setLocalValues(newValues)
|
setLocalValues(newValues);
|
||||||
if (onValueChange)
|
if (onValueChange) onValueChange(newValues);
|
||||||
onValueChange(newValues)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function isEqual(array1: number[] | undefined, array2: number[] | undefined) {
|
function isEqual(
|
||||||
array1 = array1 ?? []
|
array1: number[] | undefined,
|
||||||
array2 = array2 ?? []
|
array2: number[] | undefined,
|
||||||
|
) {
|
||||||
|
array1 = array1 ?? [];
|
||||||
|
array2 = array2 ?? [];
|
||||||
|
|
||||||
if (array1.length !== array2.length)
|
if (array1.length !== array2.length) return false;
|
||||||
return false
|
|
||||||
|
|
||||||
for (let i = 0; i < array1.length; i++) {
|
for (let i = 0; i < array1.length; i++) {
|
||||||
if (array1[i] !== array2[i])
|
if (array1[i] !== array2[i]) return false;
|
||||||
return false
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SliderPrimitive.Root
|
<SliderPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'relative flex cursor-pointer touch-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
"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',
|
orientation === "horizontal"
|
||||||
className,
|
? "w-full items-center"
|
||||||
)}
|
: "h-full justify-center",
|
||||||
min={min}
|
className,
|
||||||
max={max}
|
)}
|
||||||
step={step}
|
min={min}
|
||||||
value={localValues}
|
max={max}
|
||||||
onValueChange={value => handleValueChange(value)}
|
step={step}
|
||||||
{...props}
|
value={localValues}
|
||||||
onFocus={() => setHoveredThumbIndex(true)}
|
onValueChange={(value) => handleValueChange(value)}
|
||||||
onBlur={() => setHoveredThumbIndex(false)}
|
{...props}
|
||||||
>
|
onFocus={() => setHoveredThumbIndex(true)}
|
||||||
<SliderPrimitive.Track className={ny(
|
onBlur={() => setHoveredThumbIndex(false)}
|
||||||
'bg-primary/20 relative grow overflow-hidden rounded-full',
|
>
|
||||||
orientation === 'horizontal' ? 'h-1.5 w-full' : 'h-full w-1.5',
|
<SliderPrimitive.Track
|
||||||
)}
|
className={ny(
|
||||||
>
|
"relative grow overflow-hidden rounded-full bg-primary/20",
|
||||||
<SliderPrimitive.Range className={ny(
|
orientation === "horizontal" ? "h-1.5 w-full" : "h-full w-1.5",
|
||||||
'bg-primary absolute',
|
)}
|
||||||
orientation === 'horizontal' ? 'h-full' : 'w-full',
|
>
|
||||||
)}
|
<SliderPrimitive.Range
|
||||||
/>
|
className={ny(
|
||||||
{showSteps !== undefined && showSteps !== 'none' && stepLines.map((value, index) => {
|
"absolute bg-primary",
|
||||||
if (value === min || value === max)
|
orientation === "horizontal" ? "h-full" : "w-full",
|
||||||
return null
|
)}
|
||||||
|
/>
|
||||||
|
{showSteps !== undefined &&
|
||||||
|
showSteps !== "none" &&
|
||||||
|
stepLines.map((value, index) => {
|
||||||
|
if (value === min || value === max) return null;
|
||||||
|
|
||||||
const positionPercentage = ((value - min) / (max - min)) * 100
|
const positionPercentage = ((value - min) / (max - min)) * 100;
|
||||||
const adjustedPosition = 50 + (positionPercentage - 50) * 0.96
|
const adjustedPosition = 50 + (positionPercentage - 50) * 0.96;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={ny(
|
className={ny(
|
||||||
{ 'w-0.5 h-2': orientation !== 'vertical', 'w-2 h-0.5': orientation === 'vertical' },
|
{
|
||||||
'bg-muted-foreground absolute',
|
"h-2 w-0.5": orientation !== "vertical",
|
||||||
{
|
"h-0.5 w-2": orientation === "vertical",
|
||||||
'left-1': orientation === 'vertical' && showSteps === 'half',
|
},
|
||||||
'top-1': orientation !== 'vertical' && showSteps === 'half',
|
"absolute bg-muted-foreground",
|
||||||
'left-0': orientation === 'vertical' && showSteps === 'full',
|
{
|
||||||
'top-0': orientation !== 'vertical' && showSteps === 'full',
|
"left-1":
|
||||||
'-translate-x-1/2': orientation !== 'vertical',
|
orientation === "vertical" && showSteps === "half",
|
||||||
'-translate-y-1/2': orientation === 'vertical',
|
"top-1":
|
||||||
},
|
orientation !== "vertical" && showSteps === "half",
|
||||||
)}
|
"left-0":
|
||||||
style={{
|
orientation === "vertical" && showSteps === "full",
|
||||||
[orientation === 'vertical' ? 'bottom' : 'left']: `${adjustedPosition}%`,
|
"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>
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
{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
|
export { Slider };
|
||||||
|
|
||||||
export { Slider }
|
|
||||||
|
|||||||
@@ -1,152 +1,151 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from "framer-motion";
|
||||||
import type { CSSProperties, ReactElement } from 'react'
|
import type { CSSProperties, ReactElement } from "react";
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from "react";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface Sparkle {
|
interface Sparkle {
|
||||||
id: string
|
id: string;
|
||||||
x: string
|
x: string;
|
||||||
y: string
|
y: string;
|
||||||
color: string
|
color: string;
|
||||||
delay: number
|
delay: number;
|
||||||
scale: number
|
scale: number;
|
||||||
lifespan: number
|
lifespan: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SparklesTextProps {
|
interface SparklesTextProps {
|
||||||
/**
|
/**
|
||||||
* @default <div />
|
* @default <div />
|
||||||
* @type ReactElement
|
* @type ReactElement
|
||||||
* @description
|
* @description
|
||||||
* The component to be rendered as the text
|
* The component to be rendered as the text
|
||||||
*/
|
*/
|
||||||
as?: ReactElement
|
as?: ReactElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default ""
|
* @default ""
|
||||||
* @type string
|
* @type string
|
||||||
* @description
|
* @description
|
||||||
* The className of the text
|
* The className of the text
|
||||||
*/
|
*/
|
||||||
className?: string
|
className?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @required
|
* @required
|
||||||
* @type string
|
* @type string
|
||||||
* @description
|
* @description
|
||||||
* The text to be displayed
|
* The text to be displayed
|
||||||
*/
|
*/
|
||||||
text: string
|
text: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default 10
|
* @default 10
|
||||||
* @type number
|
* @type number
|
||||||
* @description
|
* @description
|
||||||
* The count of sparkles
|
* The count of sparkles
|
||||||
*/
|
*/
|
||||||
sparklesCount?: number
|
sparklesCount?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default "{first: '#A07CFE', second: '#FE8FB5'}"
|
* @default "{first: '#A07CFE', second: '#FE8FB5'}"
|
||||||
* @type string
|
* @type string
|
||||||
* @description
|
* @description
|
||||||
* The colors of the sparkles
|
* The colors of the sparkles
|
||||||
*/
|
*/
|
||||||
colors?: {
|
colors?: {
|
||||||
first: string
|
first: string;
|
||||||
second: string
|
second: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SparklesText: React.FC<SparklesTextProps> = ({
|
const SparklesText: React.FC<SparklesTextProps> = ({
|
||||||
text,
|
text,
|
||||||
colors = { first: '#A07CFE', second: '#FE8FB5' },
|
colors = { first: "#A07CFE", second: "#FE8FB5" },
|
||||||
className,
|
className,
|
||||||
sparklesCount = 10,
|
sparklesCount = 10,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [sparkles, setSparkles] = useState<Sparkle[]>([])
|
const [sparkles, setSparkles] = useState<Sparkle[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const generateStar = (): Sparkle => {
|
const generateStar = (): Sparkle => {
|
||||||
const starX = `${Math.random() * 100}%`
|
const starX = `${Math.random() * 100}%`;
|
||||||
const starY = `${Math.random() * 100}%`
|
const starY = `${Math.random() * 100}%`;
|
||||||
const color = Math.random() > 0.5 ? colors.first : colors.second
|
const color = Math.random() > 0.5 ? colors.first : colors.second;
|
||||||
const delay = Math.random() * 2
|
const delay = Math.random() * 2;
|
||||||
const scale = Math.random() * 1 + 0.3
|
const scale = Math.random() * 1 + 0.3;
|
||||||
const lifespan = Math.random() * 10 + 5
|
const lifespan = Math.random() * 10 + 5;
|
||||||
const id = `${starX}-${starY}-${Date.now()}`
|
const id = `${starX}-${starY}-${Date.now()}`;
|
||||||
return { id, x: starX, y: starY, color, delay, scale, lifespan }
|
return { id, x: starX, y: starY, color, delay, scale, lifespan };
|
||||||
}
|
};
|
||||||
|
|
||||||
const initializeStars = () => {
|
const initializeStars = () => {
|
||||||
const newSparkles = Array.from({ length: sparklesCount }, generateStar)
|
const newSparkles = Array.from({ length: sparklesCount }, generateStar);
|
||||||
setSparkles(newSparkles)
|
setSparkles(newSparkles);
|
||||||
}
|
};
|
||||||
|
|
||||||
const updateStars = () => {
|
const updateStars = () => {
|
||||||
setSparkles(currentSparkles =>
|
setSparkles((currentSparkles) =>
|
||||||
currentSparkles.map((star) => {
|
currentSparkles.map((star) => {
|
||||||
if (star.lifespan <= 0)
|
if (star.lifespan <= 0) return generateStar();
|
||||||
return generateStar()
|
else return { ...star, lifespan: star.lifespan - 0.1 };
|
||||||
else return { ...star, lifespan: star.lifespan - 0.1 }
|
}),
|
||||||
}),
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
initializeStars()
|
initializeStars();
|
||||||
const interval = setInterval(updateStars, 100)
|
const interval = setInterval(updateStars, 100);
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval);
|
||||||
}, [colors.first, colors.second])
|
}, [colors.first, colors.second]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ny('text-6xl font-bold', className)}
|
className={ny("text-6xl font-bold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--sparkles-first-color': `${colors.first}`,
|
"--sparkles-first-color": `${colors.first}`,
|
||||||
'--sparkles-second-color': `${colors.second}`,
|
"--sparkles-second-color": `${colors.second}`,
|
||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="relative inline-block">
|
<span className="relative inline-block">
|
||||||
{sparkles.map(sparkle => (
|
{sparkles.map((sparkle) => (
|
||||||
<Sparkle key={sparkle.id} {...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">
|
<strong className="bg-gradient-to-r from-[var(--sparkles-first-color)] to-[var(--sparkles-second-color)] bg-clip-text text-transparent">
|
||||||
{text}
|
{text}
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const Sparkle: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {
|
const Sparkle: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {
|
||||||
return (
|
return (
|
||||||
<motion.svg
|
<motion.svg
|
||||||
key={id}
|
key={id}
|
||||||
className="pointer-events-none absolute z-20"
|
className="pointer-events-none absolute z-20"
|
||||||
initial={{ opacity: 0, left: x, top: y }}
|
initial={{ opacity: 0, left: x, top: y }}
|
||||||
animate={{
|
animate={{
|
||||||
opacity: [0, 1, 0],
|
opacity: [0, 1, 0],
|
||||||
scale: [0, scale, 0],
|
scale: [0, scale, 0],
|
||||||
rotate: [75, 120, 150],
|
rotate: [75, 120, 150],
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.8, repeat: Infinity, delay }}
|
transition={{ duration: 0.8, repeat: Infinity, delay }}
|
||||||
width="21"
|
width="21"
|
||||||
height="21"
|
height="21"
|
||||||
viewBox="0 0 21 21"
|
viewBox="0 0 21 21"
|
||||||
>
|
>
|
||||||
<path
|
<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"
|
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}
|
fill={color}
|
||||||
/>
|
/>
|
||||||
</motion.svg>
|
</motion.svg>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SparklesText
|
export default SparklesText;
|
||||||
|
|||||||
@@ -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<
|
const Table = React.forwardRef<
|
||||||
HTMLTableElement,
|
HTMLTableElement,
|
||||||
React.HTMLAttributes<HTMLTableElement>
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div className="relative w-full overflow-auto">
|
<div className="relative w-full overflow-auto">
|
||||||
<table
|
<table
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('w-full caption-bottom text-sm', className)}
|
className={ny("w-full caption-bottom text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
));
|
||||||
Table.displayName = 'Table'
|
Table.displayName = "Table";
|
||||||
|
|
||||||
const TableHeader = React.forwardRef<
|
const TableHeader = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<thead ref={ref} className={ny('[&_tr]:border-b', className)} {...props} />
|
<thead ref={ref} className={ny("[&_tr]:border-b", className)} {...props} />
|
||||||
))
|
));
|
||||||
TableHeader.displayName = 'TableHeader'
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
const TableBody = React.forwardRef<
|
const TableBody = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tbody
|
<tbody
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('[&_tr:last-child]:border-0', className)}
|
className={ny("[&_tr:last-child]:border-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableBody.displayName = 'TableBody'
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
const TableFooter = React.forwardRef<
|
const TableFooter = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tfoot
|
<tfoot
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableFooter.displayName = 'TableFooter'
|
TableFooter.displayName = "TableFooter";
|
||||||
|
|
||||||
const TableRow = React.forwardRef<
|
const TableRow = React.forwardRef<
|
||||||
HTMLTableRowElement,
|
HTMLTableRowElement,
|
||||||
React.HTMLAttributes<HTMLTableRowElement>
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<tr
|
<tr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableRow.displayName = 'TableRow'
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
const TableHead = React.forwardRef<
|
const TableHead = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<th
|
<th
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableHead.displayName = 'TableHead'
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
const TableCell = React.forwardRef<
|
const TableCell = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<td
|
<td
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableCell.displayName = 'TableCell'
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
const TableCaption = React.forwardRef<
|
const TableCaption = React.forwardRef<
|
||||||
HTMLTableCaptionElement,
|
HTMLTableCaptionElement,
|
||||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<caption
|
<caption
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny('text-muted-foreground mt-4 text-sm', className)}
|
className={ny("mt-4 text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableCaption.displayName = 'TableCaption'
|
TableCaption.displayName = "TableCaption";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,55 +1,55 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react";
|
||||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
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<
|
const TabsList = React.forwardRef<
|
||||||
React.ElementRef<typeof TabsPrimitive.List>,
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
|
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef<
|
const TabsTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const TabsContent = React.forwardRef<
|
const TabsContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={ny(
|
className={ny(
|
||||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
|
|||||||
@@ -1,64 +1,64 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { motion, useScroll, useTransform } from 'framer-motion'
|
import { motion, useScroll, useTransform } from "framer-motion";
|
||||||
import type { FC, ReactNode } from 'react'
|
import type { FC, ReactNode } from "react";
|
||||||
import { useRef } from 'react'
|
import { useRef } from "react";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface TextRevealByWordProps {
|
interface TextRevealByWordProps {
|
||||||
text: string
|
text: string;
|
||||||
className?: string
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextRevealByWord: FC<TextRevealByWordProps> = ({
|
export const TextRevealByWord: FC<TextRevealByWordProps> = ({
|
||||||
text,
|
text,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const targetRef = useRef<HTMLDivElement | null>(null)
|
const targetRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const { scrollYProgress } = useScroll({
|
const { scrollYProgress } = useScroll({
|
||||||
target: targetRef,
|
target: targetRef,
|
||||||
})
|
});
|
||||||
const words = text.split(' ')
|
const words = text.split(" ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={targetRef} className={ny('relative z-0 h-[200vh]', className)}>
|
<div ref={targetRef} className={ny("relative z-0 h-[200vh]", className)}>
|
||||||
<div className="sticky top-0 mx-auto flex h-[50%] max-w-4xl items-center bg-transparent px-[1rem] py-[5rem]">
|
<div className="sticky top-0 mx-auto flex h-[50%] max-w-4xl items-center bg-transparent px-[1rem] py-[5rem]">
|
||||||
<p
|
<p
|
||||||
ref={targetRef}
|
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"
|
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) => {
|
{words.map((word, i) => {
|
||||||
const start = i / words.length
|
const start = i / words.length;
|
||||||
const end = start + 1 / words.length
|
const end = start + 1 / words.length;
|
||||||
return (
|
return (
|
||||||
<Word key={i} progress={scrollYProgress} range={[start, end]}>
|
<Word key={i} progress={scrollYProgress} range={[start, end]}>
|
||||||
{word}
|
{word}
|
||||||
</Word>
|
</Word>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface WordProps {
|
interface WordProps {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
progress: any
|
progress: any;
|
||||||
range: [number, number]
|
range: [number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Word: FC<WordProps> = ({ children, progress, range }) => {
|
const Word: FC<WordProps> = ({ children, progress, range }) => {
|
||||||
const opacity = useTransform(progress, range, [0, 1])
|
const opacity = useTransform(progress, range, [0, 1]);
|
||||||
return (
|
return (
|
||||||
<span className="xl:lg-3 relative mx-1 lg:mx-2.5">
|
<span className="xl:lg-3 relative mx-1 lg:mx-2.5">
|
||||||
<span className="absolute opacity-30">{children}</span>
|
<span className="absolute opacity-30">{children}</span>
|
||||||
<motion.span style={{ opacity }} className="text-black dark:text-white">
|
<motion.span style={{ opacity }} className="text-black dark:text-white">
|
||||||
{children}
|
{children}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default TextRevealByWord
|
export default TextRevealByWord;
|
||||||
|
|||||||
@@ -1,46 +1,45 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from "react";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface TypingAnimationProps {
|
interface TypingAnimationProps {
|
||||||
text: string
|
text: string;
|
||||||
duration?: number
|
duration?: number;
|
||||||
className?: string
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TypingAnimation({
|
export default function TypingAnimation({
|
||||||
text,
|
text,
|
||||||
duration = 200,
|
duration = 200,
|
||||||
className,
|
className,
|
||||||
}: TypingAnimationProps) {
|
}: TypingAnimationProps) {
|
||||||
const [displayedText, setDisplayedText] = useState<string>('')
|
const [displayedText, setDisplayedText] = useState<string>("");
|
||||||
const [i, setI] = useState<number>(0)
|
const [i, setI] = useState<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const typingEffect = setInterval(() => {
|
const typingEffect = setInterval(() => {
|
||||||
if (i < text.length) {
|
if (i < text.length) {
|
||||||
setDisplayedText(text.substring(0, i + 1))
|
setDisplayedText(text.substring(0, i + 1));
|
||||||
setI(i + 1)
|
setI(i + 1);
|
||||||
}
|
} else {
|
||||||
else {
|
clearInterval(typingEffect);
|
||||||
clearInterval(typingEffect)
|
}
|
||||||
}
|
}, duration);
|
||||||
}, duration)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(typingEffect)
|
clearInterval(typingEffect);
|
||||||
}
|
};
|
||||||
}, [duration, i])
|
}, [duration, i]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h1
|
<h1
|
||||||
className={ny(
|
className={ny(
|
||||||
'font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm',
|
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{displayedText || text}
|
{displayedText || text}
|
||||||
</h1>
|
</h1>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,58 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from "react";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface WavyTextProps {
|
interface WavyTextProps {
|
||||||
word: string
|
word: string;
|
||||||
className?: string
|
className?: string;
|
||||||
variant?: {
|
variant?: {
|
||||||
hidden: { y: number }
|
hidden: { y: number };
|
||||||
visible: { y: number }
|
visible: { y: number };
|
||||||
}
|
};
|
||||||
duration?: number
|
duration?: number;
|
||||||
delay?: number
|
delay?: number;
|
||||||
}
|
}
|
||||||
function WavyText({
|
function WavyText({
|
||||||
word,
|
word,
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
duration = 0.5,
|
duration = 0.5,
|
||||||
delay = 0.05,
|
delay = 0.05,
|
||||||
}: WavyTextProps) {
|
}: WavyTextProps) {
|
||||||
const defaultVariants = {
|
const defaultVariants = {
|
||||||
hidden: { y: 10 },
|
hidden: { y: 10 },
|
||||||
visible: { y: -10 },
|
visible: { y: -10 },
|
||||||
}
|
};
|
||||||
const combinedVariants = variant || defaultVariants
|
const combinedVariants = variant || defaultVariants;
|
||||||
const characters = useMemo(() => word.split(''), [word])
|
const characters = useMemo(() => word.split(""), [word]);
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center space-x-2 overflow-hidden p-3">
|
<div className="flex justify-center space-x-2 overflow-hidden p-3">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{characters.map((char, i) => (
|
{characters.map((char, i) => (
|
||||||
<motion.h1
|
<motion.h1
|
||||||
key={i}
|
key={i}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
exit="hidden"
|
exit="hidden"
|
||||||
variants={combinedVariants}
|
variants={combinedVariants}
|
||||||
transition={{
|
transition={{
|
||||||
yoyo: Infinity,
|
yoyo: Infinity,
|
||||||
duration,
|
duration,
|
||||||
delay: i * delay,
|
delay: i * delay,
|
||||||
}}
|
}}
|
||||||
className={ny(
|
className={ny(
|
||||||
className,
|
className,
|
||||||
'font-display text-center text-4xl font-bold tracking-[-0.15em] md:text-7xl',
|
"font-display text-center text-4xl font-bold tracking-[-0.15em] md:text-7xl",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{char}
|
{char}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WavyText
|
export default WavyText;
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import type { Variants } from 'framer-motion'
|
import type { Variants } from "framer-motion";
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from "framer-motion";
|
||||||
import { ny } from '@/lib/utils'
|
import { ny } from "@/lib/utils";
|
||||||
|
|
||||||
interface WordPullUpProps {
|
interface WordPullUpProps {
|
||||||
words: string
|
words: string;
|
||||||
delayMultiple?: number
|
delayMultiple?: number;
|
||||||
wrapperFramerProps?: Variants
|
wrapperFramerProps?: Variants;
|
||||||
framerProps?: Variants
|
framerProps?: Variants;
|
||||||
className?: string
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WordPullUp({
|
export default function WordPullUp({
|
||||||
words,
|
words,
|
||||||
wrapperFramerProps = {
|
wrapperFramerProps = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
show: {
|
show: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
staggerChildren: 0.2,
|
staggerChildren: 0.2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
framerProps = {
|
framerProps = {
|
||||||
hidden: { y: 20, opacity: 0 },
|
hidden: { y: 20, opacity: 0 },
|
||||||
show: { y: 0, opacity: 1 },
|
show: { y: 0, opacity: 1 },
|
||||||
},
|
},
|
||||||
className,
|
className,
|
||||||
}: WordPullUpProps) {
|
}: WordPullUpProps) {
|
||||||
return (
|
return (
|
||||||
<motion.h1
|
<motion.h1
|
||||||
variants={wrapperFramerProps}
|
variants={wrapperFramerProps}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="show"
|
animate="show"
|
||||||
className={ny(
|
className={ny(
|
||||||
'font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm',
|
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{words.split(' ').map((word, i) => (
|
{words.split(" ").map((word, i) => (
|
||||||
<motion.span
|
<motion.span
|
||||||
key={i}
|
key={i}
|
||||||
variants={framerProps}
|
variants={framerProps}
|
||||||
style={{ display: 'inline-block', paddingRight: '8px' }}
|
style={{ display: "inline-block", paddingRight: "8px" }}
|
||||||
>
|
>
|
||||||
{word === '' ? <span> </span> : word}
|
{word === "" ? <span> </span> : word}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
))}
|
))}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,18 @@
|
|||||||
import WordPullUp from "./ui/word-pull-up";
|
import WordPullUp from "./ui/word-pull-up";
|
||||||
|
|
||||||
export default function WelcomePage() {
|
export default function WelcomePage() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full relative min-h-screen flex flex-col items-center justify-center">
|
<div className="relative flex min-h-screen w-full flex-col items-center justify-center">
|
||||||
<WordPullUp className="text-6xl text-center" words="Welcome to Zen Browser!" />
|
<WordPullUp
|
||||||
<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>
|
className="text-center text-6xl"
|
||||||
</div>
|
words="Welcome to Zen Browser!"
|
||||||
);
|
/>
|
||||||
|
<p className="max-w-90 mt-12 text-lg">
|
||||||
|
A Firefox based browser with a focus on privacy and customization.
|
||||||
|
<br />
|
||||||
|
Start using it by clicking on the sidebar icon or trying out the split
|
||||||
|
view feature!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
|
|
||||||
export const LOGO_COLORS = [
|
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
@@ -1,46 +1,46 @@
|
|||||||
export const releases: any = {
|
export const releases: any = {
|
||||||
WindowsInstaller: "zen.installer.exe",
|
WindowsInstaller: "zen.installer.exe",
|
||||||
WindowsInstallerGeneric: "zen.installer-generic.exe",
|
WindowsInstallerGeneric: "zen.installer-generic.exe",
|
||||||
|
|
||||||
WindowsZip: "zen.win-specific.zip",
|
|
||||||
WindowsZipGeneric: "zen.win-generic.zip",
|
|
||||||
|
|
||||||
MacOS: "zen.macos-aarch64.dmg",
|
WindowsZip: "zen.win-specific.zip",
|
||||||
MacOSIntel: "zen.macos-x64.dmg",
|
WindowsZipGeneric: "zen.win-generic.zip",
|
||||||
|
|
||||||
Linux: "zen.linux-specific.tar.bz2",
|
MacOS: "zen.macos-aarch64.dmg",
|
||||||
LinuxGeneric: "zen.linux-generic.tar.bz2",
|
MacOSIntel: "zen.macos-x64.dmg",
|
||||||
|
|
||||||
LinuxAppImage: "zen-specific.AppImage",
|
Linux: "zen.linux-specific.tar.bz2",
|
||||||
LinuxAppImageGeneric: "zen-generic.AppImage",
|
LinuxGeneric: "zen.linux-generic.tar.bz2",
|
||||||
|
|
||||||
|
LinuxAppImage: "zen-specific.AppImage",
|
||||||
|
LinuxAppImageGeneric: "zen-generic.AppImage",
|
||||||
};
|
};
|
||||||
|
|
||||||
// platform
|
// platform
|
||||||
// -> arch
|
// -> arch
|
||||||
// -> file
|
// -> file
|
||||||
export const releaseTree: any = {
|
export const releaseTree: any = {
|
||||||
windows: {
|
windows: {
|
||||||
specific: {
|
specific: {
|
||||||
installer: "WindowsInstaller",
|
installer: "WindowsInstaller",
|
||||||
portable: "WindowsZip",
|
portable: "WindowsZip",
|
||||||
},
|
},
|
||||||
generic: {
|
generic: {
|
||||||
installer: "WindowsInstallerGeneric",
|
installer: "WindowsInstallerGeneric",
|
||||||
portable: "WindowsZipGeneric",
|
portable: "WindowsZipGeneric",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
macos: {
|
macos: {
|
||||||
generic: "MacOSIntel",
|
generic: "MacOSIntel",
|
||||||
specific: "MacOS",
|
specific: "MacOS",
|
||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
specific: {
|
specific: {
|
||||||
portable: "Linux",
|
portable: "Linux",
|
||||||
appimage: "LinuxAppImage",
|
appimage: "LinuxAppImage",
|
||||||
},
|
},
|
||||||
generic: {
|
generic: {
|
||||||
portable: "LinuxGeneric",
|
portable: "LinuxGeneric",
|
||||||
appimage: "LinuxAppImageGeneric",
|
appimage: "LinuxAppImageGeneric",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,25 +5,25 @@ import { useServerInsertedHTML } from "next/navigation";
|
|||||||
import { ServerStyleSheet, StyleSheetManager } from "styled-components";
|
import { ServerStyleSheet, StyleSheetManager } from "styled-components";
|
||||||
|
|
||||||
export default function StyledComponentsRegistry({
|
export default function StyledComponentsRegistry({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
// Only create stylesheet once with lazy initial state
|
// Only create stylesheet once with lazy initial state
|
||||||
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
|
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
|
||||||
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
|
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
|
||||||
|
|
||||||
useServerInsertedHTML(() => {
|
useServerInsertedHTML(() => {
|
||||||
const styles = styledComponentsStyleSheet.getStyleElement();
|
const styles = styledComponentsStyleSheet.getStyleElement();
|
||||||
styledComponentsStyleSheet.instance.clearTag();
|
styledComponentsStyleSheet.instance.clearTag();
|
||||||
return <>{styles}</>;
|
return <>{styles}</>;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof window !== "undefined") return <>{children}</>;
|
if (typeof window !== "undefined") return <>{children}</>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
|
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
|
||||||
{children}
|
{children}
|
||||||
</StyleSheetManager>
|
</StyleSheetManager>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,63 @@
|
|||||||
|
|
||||||
export interface ZenTheme {
|
export interface ZenTheme {
|
||||||
name: string
|
name: string;
|
||||||
description: string
|
description: string;
|
||||||
image: string
|
image: string;
|
||||||
downloadUrl: string
|
downloadUrl: string;
|
||||||
id: string
|
id: string;
|
||||||
homepage?: string
|
homepage?: string;
|
||||||
readme: string
|
readme: string;
|
||||||
preferences?: string
|
preferences?: string;
|
||||||
isColorTheme: boolean
|
isColorTheme: boolean;
|
||||||
author: string
|
author: string;
|
||||||
version: string
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEME_API = "https://zen-browser.github.io/theme-store/themes.json";
|
const THEME_API = "https://zen-browser.github.io/theme-store/themes.json";
|
||||||
const CACHE_OPTIONS = { next: {
|
const CACHE_OPTIONS = {
|
||||||
revalidate: 60,
|
next: {
|
||||||
} } as RequestInit;
|
revalidate: 60,
|
||||||
|
},
|
||||||
|
} as RequestInit;
|
||||||
|
|
||||||
export async function getAllThemes() {
|
export async function getAllThemes() {
|
||||||
// Fetch from the API
|
// Fetch from the API
|
||||||
const response = await fetch(THEME_API, CACHE_OPTIONS);
|
const response = await fetch(THEME_API, CACHE_OPTIONS);
|
||||||
const themes = await response.json();
|
const themes = await response.json();
|
||||||
// transform in to a ZenTheme[] as it is currently an object
|
// transform in to a ZenTheme[] as it is currently an object
|
||||||
let themesArray: ZenTheme[] = [];
|
let themesArray: ZenTheme[] = [];
|
||||||
for (let key in themes) {
|
for (let key in themes) {
|
||||||
themesArray.push(themes[key]);
|
themesArray.push(themes[key]);
|
||||||
}
|
}
|
||||||
return themesArray;
|
return themesArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThemesFromSearch(themes: ZenTheme[], query: string, tags: string[]): ZenTheme[] {
|
export function getThemesFromSearch(
|
||||||
let filtered = themes.filter((theme) => theme.name.toLowerCase().includes(query.toLowerCase()));
|
themes: ZenTheme[],
|
||||||
if (tags.includes("all")) return filtered;
|
query: string,
|
||||||
const isSearchingForColorScheme = tags.includes("color-scheme");
|
tags: string[],
|
||||||
const isSearchingForUtility = !isSearchingForColorScheme && tags.includes("utility");
|
): ZenTheme[] {
|
||||||
return filtered.filter((theme) => {
|
let filtered = themes.filter((theme) =>
|
||||||
if (isSearchingForColorScheme && theme.isColorTheme) return true;
|
theme.name.toLowerCase().includes(query.toLowerCase()),
|
||||||
if (isSearchingForUtility && !theme.isColorTheme) return true;
|
);
|
||||||
return false;
|
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) {
|
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) {
|
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 {
|
export function getThemeAuthorLink(theme: ZenTheme): string {
|
||||||
return `https://github.com/${theme.author}`;
|
return `https://github.com/${theme.author}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function ny(...inputs: ClassValue[]) {
|
export function ny(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,154 +1,156 @@
|
|||||||
import type { Config } from "tailwindcss"
|
import type { Config } from "tailwindcss";
|
||||||
import { fontFamily } from 'tailwindcss/defaultTheme'
|
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
'./pages/**/*.{ts,tsx}',
|
"./pages/**/*.{ts,tsx}",
|
||||||
'./components/**/*.{ts,tsx}',
|
"./components/**/*.{ts,tsx}",
|
||||||
'./app/**/*.{ts,tsx}',
|
"./app/**/*.{ts,tsx}",
|
||||||
'./src/**/*.{ts,tsx}',
|
"./src/**/*.{ts,tsx}",
|
||||||
],
|
],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
padding: '2rem',
|
padding: "2rem",
|
||||||
screens: {
|
screens: {
|
||||||
'2xl': '1400px',
|
"2xl": "1400px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['var(--font-sans)', ...fontFamily.sans],
|
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
border: 'hsl(var(--border))',
|
border: "hsl(var(--border))",
|
||||||
input: 'hsl(var(--input))',
|
input: "hsl(var(--input))",
|
||||||
ring: 'hsl(var(--ring))',
|
ring: "hsl(var(--ring))",
|
||||||
background: 'hsl(var(--background))',
|
background: "hsl(var(--background))",
|
||||||
foreground: 'hsl(var(--foreground))',
|
foreground: "hsl(var(--foreground))",
|
||||||
surface: "var(--surface)",
|
surface: "var(--surface)",
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: 'hsl(var(--primary))',
|
DEFAULT: "hsl(var(--primary))",
|
||||||
foreground: 'hsl(var(--primary-foreground))',
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: 'hsl(var(--secondary))',
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
foreground: 'hsl(var(--secondary-foreground))',
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: 'hsl(var(--destructive))',
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
foreground: 'hsl(var(--destructive-foreground))',
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: 'hsl(var(--muted))',
|
DEFAULT: "hsl(var(--muted))",
|
||||||
foreground: 'hsl(var(--muted-foreground))',
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: 'hsl(var(--accent))',
|
DEFAULT: "hsl(var(--accent))",
|
||||||
foreground: 'hsl(var(--accent-foreground))',
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: 'hsl(var(--popover))',
|
DEFAULT: "hsl(var(--popover))",
|
||||||
foreground: 'hsl(var(--popover-foreground))',
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: 'hsl(var(--card))',
|
DEFAULT: "hsl(var(--card))",
|
||||||
foreground: 'hsl(var(--card-foreground))',
|
foreground: "hsl(var(--card-foreground))",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: 'var(--radius)',
|
lg: "var(--radius)",
|
||||||
md: 'calc(var(--radius) - 2px)',
|
md: "calc(var(--radius) - 2px)",
|
||||||
sm: 'calc(var(--radius) - 4px)',
|
sm: "calc(var(--radius) - 4px)",
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
orbit: {
|
orbit: {
|
||||||
"0%": {
|
"0%": {
|
||||||
transform: "rotate(0deg) translateY(calc(var(--radius) * 1px)) rotate(0deg)",
|
transform:
|
||||||
},
|
"rotate(0deg) translateY(calc(var(--radius) * 1px)) rotate(0deg)",
|
||||||
"100%": {
|
},
|
||||||
transform: "rotate(360deg) translateY(calc(var(--radius) * 1px)) rotate(-360deg)",
|
"100%": {
|
||||||
},
|
transform:
|
||||||
},
|
"rotate(360deg) translateY(calc(var(--radius) * 1px)) rotate(-360deg)",
|
||||||
"shine-pulse": {
|
},
|
||||||
"0%": {
|
},
|
||||||
"background-position": "0% 0%",
|
"shine-pulse": {
|
||||||
},
|
"0%": {
|
||||||
"50%": {
|
"background-position": "0% 0%",
|
||||||
"background-position": "100% 100%",
|
},
|
||||||
},
|
"50%": {
|
||||||
to: {
|
"background-position": "100% 100%",
|
||||||
"background-position": "0% 0%",
|
},
|
||||||
},
|
to: {
|
||||||
},
|
"background-position": "0% 0%",
|
||||||
'accordion-down': {
|
},
|
||||||
from: { height: '0' },
|
},
|
||||||
to: { height: 'var(--radix-accordion-content-height)' },
|
"accordion-down": {
|
||||||
},
|
from: { height: "0" },
|
||||||
'accordion-up': {
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
from: { height: 'var(--radix-accordion-content-height)' },
|
},
|
||||||
to: { height: '0' },
|
"accordion-up": {
|
||||||
},
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
'border-beam': {
|
to: { height: "0" },
|
||||||
'100%': {
|
},
|
||||||
'offset-distance': '100%',
|
"border-beam": {
|
||||||
},
|
"100%": {
|
||||||
},
|
"offset-distance": "100%",
|
||||||
'image-glow': {
|
},
|
||||||
'0%': {
|
},
|
||||||
'opacity': '0',
|
"image-glow": {
|
||||||
'animation-timing-function': 'cubic-bezier(0.74, 0.25, 0.76, 1)',
|
"0%": {
|
||||||
},
|
opacity: "0",
|
||||||
'10%': {
|
"animation-timing-function": "cubic-bezier(0.74, 0.25, 0.76, 1)",
|
||||||
'opacity': '0.7',
|
},
|
||||||
'animation-timing-function': 'cubic-bezier(0.12, 0.01, 0.08, 0.99)',
|
"10%": {
|
||||||
},
|
opacity: "0.7",
|
||||||
'100%': {
|
"animation-timing-function": "cubic-bezier(0.12, 0.01, 0.08, 0.99)",
|
||||||
opacity: '0.4',
|
},
|
||||||
},
|
"100%": {
|
||||||
},
|
opacity: "0.4",
|
||||||
'fade-in': {
|
},
|
||||||
from: { opacity: '0', transform: 'translateY(-10px)' },
|
},
|
||||||
to: { opacity: '1', transform: 'none' },
|
"fade-in": {
|
||||||
},
|
from: { opacity: "0", transform: "translateY(-10px)" },
|
||||||
'fade-up': {
|
to: { opacity: "1", transform: "none" },
|
||||||
from: { opacity: '0', transform: 'translateY(20px)' },
|
},
|
||||||
to: { opacity: '1', transform: 'none' },
|
"fade-up": {
|
||||||
},
|
from: { opacity: "0", transform: "translateY(20px)" },
|
||||||
'shimmer': {
|
to: { opacity: "1", transform: "none" },
|
||||||
'0%, 90%, 100%': {
|
},
|
||||||
'background-position': 'calc(-100% - var(--shimmer-width)) 0',
|
shimmer: {
|
||||||
},
|
"0%, 90%, 100%": {
|
||||||
'30%, 60%': {
|
"background-position": "calc(-100% - var(--shimmer-width)) 0",
|
||||||
'background-position': 'calc(100% + var(--shimmer-width)) 0',
|
},
|
||||||
},
|
"30%, 60%": {
|
||||||
},
|
"background-position": "calc(100% + var(--shimmer-width)) 0",
|
||||||
'marquee': {
|
},
|
||||||
from: { transform: 'translateX(0)' },
|
},
|
||||||
to: { transform: 'translateX(calc(-100% - var(--gap)))' },
|
marquee: {
|
||||||
},
|
from: { transform: "translateX(0)" },
|
||||||
'marquee-vertical': {
|
to: { transform: "translateX(calc(-100% - var(--gap)))" },
|
||||||
from: { transform: 'translateY(0)' },
|
},
|
||||||
to: { transform: 'translateY(calc(-100% - var(--gap)))' },
|
"marquee-vertical": {
|
||||||
},
|
from: { transform: "translateY(0)" },
|
||||||
},
|
to: { transform: "translateY(calc(-100% - var(--gap)))" },
|
||||||
animation: {
|
},
|
||||||
orbit: "orbit calc(var(--duration)*1s) linear infinite",
|
},
|
||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
animation: {
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
orbit: "orbit calc(var(--duration)*1s) linear infinite",
|
||||||
'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear',
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
'image-glow': 'image-glow 4100ms 600ms ease-out forwards',
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
'fade-in': 'fade-in 1000ms var(--animation-delay, 0ms) ease forwards',
|
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
|
||||||
'fade-up': 'fade-up 1000ms var(--animation-delay, 0ms) ease forwards',
|
"image-glow": "image-glow 4100ms 600ms ease-out forwards",
|
||||||
'shimmer': 'shimmer 8s infinite',
|
"fade-in": "fade-in 1000ms var(--animation-delay, 0ms) ease forwards",
|
||||||
'marquee': 'marquee var(--duration) infinite linear',
|
"fade-up": "fade-up 1000ms var(--animation-delay, 0ms) ease forwards",
|
||||||
'marquee-vertical': 'marquee-vertical var(--duration) linear infinite',
|
shimmer: "shimmer 8s infinite",
|
||||||
},
|
marquee: "marquee var(--duration) infinite linear",
|
||||||
},
|
"marquee-vertical": "marquee-vertical var(--duration) linear infinite",
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
},
|
||||||
} satisfies Config
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
} satisfies Config;
|
||||||
|
|
||||||
export default config
|
export default config;
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user