Initial commit
2
.env
Normal file
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_API_URL=https://api.webild.io
|
||||
NEXT_PUBLIC_PROJECT_ID=770737a9-62d1-4974-b468-a2c6107b7ee3
|
||||
57
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Code Check
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch to check'
|
||||
required: true
|
||||
default: 'main'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ gitea.event.inputs.branch }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm ci --prefer-offline --no-audit 2>&1 | tee install.log
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
|
||||
- name: TypeScript check
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm run typecheck 2>&1 | tee build.log
|
||||
|
||||
- name: ESLint check
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm run lint 2>&1 | tee build.log
|
||||
|
||||
- name: Upload build log on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-log
|
||||
path: build.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Check completed
|
||||
if: success()
|
||||
run: echo "Typecheck and lint passed successfully"
|
||||
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
116
THEME_PROVIDER_OPTIONS.txt
Normal file
@@ -0,0 +1,116 @@
|
||||
================================================================================
|
||||
THEME PROVIDER OPTIONS
|
||||
================================================================================
|
||||
|
||||
PROPS (All Required except showBlurBottom)
|
||||
-------------------------------------------
|
||||
|
||||
1. defaultButtonVariant
|
||||
• "hover-magnetic"
|
||||
• "hover-bubble"
|
||||
• "expand-hover"
|
||||
• "elastic-effect"
|
||||
• "bounce-effect"
|
||||
• "icon-arrow"
|
||||
• "shift-hover"
|
||||
• "text-stagger"
|
||||
• "text-shift"
|
||||
• "directional-hover"
|
||||
• "slide-background"
|
||||
|
||||
2. defaultTextAnimation
|
||||
• "entrance-slide"
|
||||
• "reveal-blur"
|
||||
• "background-highlight"
|
||||
|
||||
3. borderRadius
|
||||
• "sharp"
|
||||
• "rounded"
|
||||
• "soft"
|
||||
• "pill"
|
||||
|
||||
4. contentWidth
|
||||
• "small"
|
||||
• "smallMedium"
|
||||
• "compact"
|
||||
• "mediumSmall"
|
||||
• "medium"
|
||||
• "mediumLarge"
|
||||
|
||||
5. sizing
|
||||
• "medium"
|
||||
• "mediumLarge"
|
||||
• "largeSmall"
|
||||
• "large"
|
||||
• "mediumSizeLargeTitles"
|
||||
• "mediumLargeSizeLargeTitles"
|
||||
• "largeSmallSizeLargeTitles"
|
||||
• "largeSizeMediumTitles"
|
||||
• "mediumLargeSizeMediumTitles"
|
||||
• "largeSmallSizeMediumTitles"
|
||||
|
||||
6. background
|
||||
• "none"
|
||||
• "circleGradient"
|
||||
• "aurora"
|
||||
• "floatingGradient"
|
||||
|
||||
7. cardStyle
|
||||
• "solid"
|
||||
• "outline"
|
||||
• "elevated"
|
||||
• "gradient-mesh"
|
||||
• "gradient-radial"
|
||||
• "inset"
|
||||
• "glass-elevated"
|
||||
• "glass-depth"
|
||||
• "gradient-bordered"
|
||||
• "layered-gradient"
|
||||
• "soft-shadow"
|
||||
• "subtle-shadow"
|
||||
|
||||
8. primaryButtonStyle
|
||||
• "gradient"
|
||||
• "shadow"
|
||||
• "flat"
|
||||
• "radial-glow"
|
||||
• "diagonal-gradient"
|
||||
• "inset-glow"
|
||||
• "double-inset"
|
||||
• "primary-glow"
|
||||
|
||||
9. secondaryButtonStyle
|
||||
• "glass"
|
||||
• "solid"
|
||||
• "layered"
|
||||
• "radial-glow"
|
||||
|
||||
10. headingFontWeight
|
||||
• "light"
|
||||
• "normal"
|
||||
• "medium"
|
||||
• "semibold"
|
||||
• "bold"
|
||||
• "extrabold"
|
||||
|
||||
================================================================================
|
||||
|
||||
EXAMPLE USAGE:
|
||||
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="text-stagger"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="pill"
|
||||
contentWidth="medium"
|
||||
sizing="medium"
|
||||
background="aurora"
|
||||
cardStyle="glass-elevated"
|
||||
primaryButtonStyle="gradient"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="medium"
|
||||
showBlurBottom={false}
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
|
||||
================================================================================
|
||||
473
colorThemes.css
Normal file
@@ -0,0 +1,473 @@
|
||||
/* ============================================ */
|
||||
/* LIGHT THEME VARIANTS */
|
||||
/* ============================================ */
|
||||
|
||||
/* Light Theme - Minimal Dark Blue */
|
||||
:root[data-theme="minimal-dark-blue"] {
|
||||
--background: #ffffff;
|
||||
--card: #f9f9f9;
|
||||
--foreground: #000612e6;
|
||||
--primary-cta: #15479c;
|
||||
--secondary-cta: #f9f9f9;
|
||||
--accent: #e2e2e2;
|
||||
--background-accent: #c4c4c4;
|
||||
}
|
||||
|
||||
/* Light Theme - Minimal Dark Green */
|
||||
:root[data-theme="minimal-dark-green"] {
|
||||
--background: #ffffff;
|
||||
--card: #f9f9f9;
|
||||
--foreground: #000f06e6;
|
||||
--primary-cta: #0a7039;
|
||||
--secondary-cta: #f9f9f9;
|
||||
--accent: #e2e2e2;
|
||||
--background-accent: #c4c4c4;
|
||||
}
|
||||
|
||||
/* Light Theme - Minimal Light Red */
|
||||
:root[data-theme="minimal-light-red"] {
|
||||
--background: #ffffff;
|
||||
--card: #f9f9f9;
|
||||
--foreground: #120006e6;
|
||||
--primary-cta: #e63946;
|
||||
--secondary-cta: #f9f9f9;
|
||||
--accent: #e2e2e2;
|
||||
--background-accent: #c4c4c4;
|
||||
}
|
||||
|
||||
/* Light Theme - Minimal Light Orange */
|
||||
:root[data-theme="minimal-light-orange"] {
|
||||
--background: #ffffff;
|
||||
--card: #f9f9f9;
|
||||
--foreground: #120a00e6;
|
||||
--primary-cta: #ff8c42;
|
||||
--secondary-cta: #f9f9f9;
|
||||
--accent: #e2e2e2;
|
||||
--background-accent: #c4c4c4;
|
||||
}
|
||||
|
||||
/* Light Theme - Dark Blue */
|
||||
:root[data-theme="dark-blue"] {
|
||||
--background: #f5faff;
|
||||
--card: #f1f8ff;
|
||||
--foreground: #001122;
|
||||
--primary-cta: #15479c;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #a8cce8;
|
||||
--background-accent: #7ba3cf;
|
||||
}
|
||||
|
||||
/* Light Theme - Dark Green */
|
||||
:root[data-theme="dark-green"] {
|
||||
--background: #fafffb;
|
||||
--card: #f7fffa;
|
||||
--foreground: #001a0a;
|
||||
--primary-cta: #0a7039;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #a8d9be;
|
||||
--background-accent: #6bbf8e;
|
||||
}
|
||||
|
||||
/* Light Theme - Light Red */
|
||||
:root[data-theme="light-red"] {
|
||||
--background: #fffafa;
|
||||
--card: #fff7f7;
|
||||
--foreground: #1a0000;
|
||||
--primary-cta: #e63946;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #f5c4c7;
|
||||
--background-accent: #f09199;
|
||||
}
|
||||
|
||||
/* Light Theme - Light Purple */
|
||||
:root[data-theme="light-purple"] {
|
||||
--background: #fbfaff;
|
||||
--card: #f7f5ff;
|
||||
--foreground: #0f0022;
|
||||
--primary-cta: #8b5cf6;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #d8cef5;
|
||||
--background-accent: #c4a8f9;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Cream */
|
||||
:root[data-theme="warm-cream"] {
|
||||
--background: #f6f0e9;
|
||||
--card: #efe7dd;
|
||||
--foreground: #2b180a;
|
||||
--primary-cta: #2b180a;
|
||||
--secondary-cta: #efe7dd;
|
||||
--accent: #94877c;
|
||||
--background-accent: #afa094;
|
||||
}
|
||||
|
||||
/* Light Theme - Gray Blue Accent */
|
||||
:root[data-theme="gray-blue-accent"] {
|
||||
--background: #f5f5f5;
|
||||
--card: #ffffff;
|
||||
--foreground: #1c1c1c;
|
||||
--primary-cta: #1c1c1c;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #15479c;
|
||||
--background-accent: #a8cce8;
|
||||
}
|
||||
|
||||
/* Light Theme - Gray Green Accent */
|
||||
:root[data-theme="gray-green-accent"] {
|
||||
--background: #f5f5f5;
|
||||
--card: #ffffff;
|
||||
--foreground: #1c1c1c;
|
||||
--primary-cta: #1c1c1c;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #159c49;
|
||||
--background-accent: #a8e8ba;
|
||||
}
|
||||
|
||||
/* Light Theme - Gray Red Accent */
|
||||
:root[data-theme="gray-red-accent"] {
|
||||
--background: #f5f5f5;
|
||||
--card: #ffffff;
|
||||
--foreground: #1c1c1c;
|
||||
--primary-cta: #1c1c1c;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #e63946;
|
||||
--background-accent: #e8bea8;
|
||||
}
|
||||
|
||||
/* Light Theme - Gray Purple Accent */
|
||||
:root[data-theme="gray-purple-accent"] {
|
||||
--background: #f5f5f5;
|
||||
--card: #ffffff;
|
||||
--foreground: #1c1c1c;
|
||||
--primary-cta: #1c1c1c;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #6139e6;
|
||||
--background-accent: #b3a8e8;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Beige */
|
||||
:root[data-theme="warm-beige"] {
|
||||
--background: #efebe5;
|
||||
--card: #f7f2ea;
|
||||
--foreground: #000000;
|
||||
--primary-cta: #000000;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #ffffff;
|
||||
--background-accent: #e1b875;
|
||||
}
|
||||
|
||||
/* Light Theme - Gray Teal Green */
|
||||
:root[data-theme="gray-teal-green"] {
|
||||
--background: #f5f5f5;
|
||||
--card: #ffffff;
|
||||
--foreground: #1c1c1c;
|
||||
--primary-cta: #1f514c;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #159c49;
|
||||
--background-accent: #a8e8ba;
|
||||
}
|
||||
|
||||
/* Light Theme - Gray Navy Blue */
|
||||
:root[data-theme="gray-navy-blue"] {
|
||||
--background: #f5f5f5;
|
||||
--card: #ffffff;
|
||||
--foreground: #1c1c1c;
|
||||
--primary-cta: #1f3251;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #15479c;
|
||||
--background-accent: #a8cce8;
|
||||
}
|
||||
|
||||
/* Light Theme - Gray Burgundy Red */
|
||||
:root[data-theme="gray-burgundy-red"] {
|
||||
--background: #f5f5f5;
|
||||
--card: #ffffff;
|
||||
--foreground: #1c1c1c;
|
||||
--primary-cta: #511f1f;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #e63946;
|
||||
--background-accent: #e8bea8;
|
||||
}
|
||||
|
||||
/* Light Theme - Gray Indigo Purple */
|
||||
:root[data-theme="gray-indigo-purple"] {
|
||||
--background: #f5f5f5;
|
||||
--card: #ffffff;
|
||||
--foreground: #1c1c1c;
|
||||
--primary-cta: #341f51;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #6139e6;
|
||||
--background-accent: #b3a8e8;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Gray Pink */
|
||||
:root[data-theme="warmgray-pink"] {
|
||||
--background: #f7f6f7;
|
||||
--card: #ffffff;
|
||||
--foreground: #1b0c25;
|
||||
--primary-cta: #1b0c25;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #ff93e4;
|
||||
--background-accent: #e8a8c3;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Gray Orange */
|
||||
:root[data-theme="warmgray-orange"] {
|
||||
--background: #f7f6f7;
|
||||
--card: #ffffff;
|
||||
--foreground: #25190c;
|
||||
--primary-cta: #ff6207;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #ffce93;
|
||||
--background-accent: #e8cfa8;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Gray Blue */
|
||||
:root[data-theme="warmgray-blue"] {
|
||||
--background: #f7f6f7;
|
||||
--card: #ffffff;
|
||||
--foreground: #0c1325;
|
||||
--primary-cta: #0798ff;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #93c7ff;
|
||||
--background-accent: #a8cde8;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Gray Indigo */
|
||||
:root[data-theme="warmgray-indigo"] {
|
||||
--background: #f7f6f7;
|
||||
--card: #ffffff;
|
||||
--foreground: #0c1325;
|
||||
--primary-cta: #0b07ff;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #93b7ff;
|
||||
--background-accent: #a8bae8;
|
||||
}
|
||||
|
||||
/* Light Theme - Lavender Peach */
|
||||
:root[data-theme="lavender-peach"] {
|
||||
--background: #e3deea;
|
||||
--card: #ffffff;
|
||||
--foreground: #27231f;
|
||||
--primary-cta: #27231f;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #c68a62;
|
||||
--background-accent: #c68a62;
|
||||
}
|
||||
|
||||
/* Light Theme - Lavender Blue */
|
||||
:root[data-theme="lavender-blue"] {
|
||||
--background: #e3deea;
|
||||
--card: #ffffff;
|
||||
--foreground: #1f2027;
|
||||
--primary-cta: #1f2027;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #627dc6;
|
||||
--background-accent: #627dc6;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Stone */
|
||||
:root[data-theme="warm-stone"] {
|
||||
--background: #f5f4ef;
|
||||
--card: #dad6cd;
|
||||
--foreground: #2a2928;
|
||||
--primary-cta: #2a2928;
|
||||
--secondary-cta: #ecebea;
|
||||
--accent: #ffffff;
|
||||
--background-accent: #c6b180;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Sand */
|
||||
:root[data-theme="warm-sand"] {
|
||||
--background: #fcf6ec;
|
||||
--card: #f3ede2;
|
||||
--foreground: #2e2521;
|
||||
--primary-cta: #2e2521;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #b2a28b;
|
||||
--background-accent: #b2a28b;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Gray Green */
|
||||
:root[data-theme="warmgray-green"] {
|
||||
--background: #f7f6f7;
|
||||
--card: #ffffff;
|
||||
--foreground: #032419;
|
||||
--primary-cta: #2bb889;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #09b97e;
|
||||
--background-accent: #a8e8c3;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Gray Red */
|
||||
:root[data-theme="warmgray-red"] {
|
||||
--background: #f7f6f7;
|
||||
--card: #ffffff;
|
||||
--foreground: #250c0d;
|
||||
--primary-cta: #b82b40;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #b90941;
|
||||
--background-accent: #e8a8b6;
|
||||
}
|
||||
|
||||
/* Light Theme - Warm Gray Yellow */
|
||||
:root[data-theme="warmgray-yellow"] {
|
||||
--background: #f7f6f7;
|
||||
--card: #ffffff;
|
||||
--foreground: #251a0c;
|
||||
--primary-cta: #f4c408;
|
||||
--secondary-cta: #ffffff;
|
||||
--accent: #f4ca26;
|
||||
--background-accent: #e8daa8;
|
||||
}
|
||||
|
||||
|
||||
/* ============================================ */
|
||||
/* DARK THEME VARIANTS */
|
||||
/* ============================================ */
|
||||
|
||||
/* Dark Theme - Minimal */
|
||||
:root[data-theme="dark"] {
|
||||
--background: #0a0a0a;
|
||||
--card: #1a1a1a;
|
||||
--foreground: #ffffffe6;
|
||||
--primary-cta: #e6e6e6;
|
||||
--secondary-cta: #1a1a1a;
|
||||
--accent: #737373;
|
||||
--background-accent: #737373;
|
||||
}
|
||||
|
||||
/* Dark Theme - Minimal Light Blue */
|
||||
:root[data-theme="dark-minimal-light-blue"] {
|
||||
--background: #0a0a0a;
|
||||
--card: #1a1a1a;
|
||||
--foreground: #f0f8ffe6;
|
||||
--primary-cta: #cee7ff;
|
||||
--secondary-cta: #1a1a1a;
|
||||
--accent: #737373;
|
||||
--background-accent: #737373;
|
||||
}
|
||||
|
||||
/* Dark Theme - Minimal Light Green */
|
||||
:root[data-theme="dark-minimal-light-green"] {
|
||||
--background: #0a0a0a;
|
||||
--card: #1a1a1a;
|
||||
--foreground: #f5fffae6;
|
||||
--primary-cta: #80da9b;
|
||||
--secondary-cta: #1a1a1a;
|
||||
--accent: #737373;
|
||||
--background-accent: #737373;
|
||||
}
|
||||
|
||||
/* Dark Theme - Minimal Light Red */
|
||||
:root[data-theme="dark-minimal-light-red"] {
|
||||
--background: #0a0a0a;
|
||||
--card: #1a1a1a;
|
||||
--foreground: #fff5f5e6;
|
||||
--primary-cta: #ff7a7a;
|
||||
--secondary-cta: #1a1a1a;
|
||||
--accent: #737373;
|
||||
--background-accent: #737373;
|
||||
}
|
||||
|
||||
/* Dark Theme - Minimal Light Purple */
|
||||
:root[data-theme="dark-minimal-light-purple"] {
|
||||
--background: #0a0a0a;
|
||||
--card: #1a1a1a;
|
||||
--foreground: #f8f5ffe6;
|
||||
--primary-cta: #c89bff;
|
||||
--secondary-cta: #1a1a1a;
|
||||
--accent: #737373;
|
||||
--background-accent: #737373;
|
||||
}
|
||||
|
||||
/* Dark Theme - Minimal Light Orange */
|
||||
:root[data-theme="dark-minimal-light-orange"] {
|
||||
--background: #0a0a0a;
|
||||
--card: #1a1a1a;
|
||||
--foreground: #fffaf5e6;
|
||||
--primary-cta: #ffaa70;
|
||||
--secondary-cta: #1a1a1a;
|
||||
--accent: #737373;
|
||||
--background-accent: #737373;
|
||||
}
|
||||
|
||||
|
||||
/* Dark Theme - Minimal Light Yellow */
|
||||
:root[data-theme="dark-minimal-light-yellow"] {
|
||||
--background: #0a0a0a;
|
||||
--card: #1a1a1a;
|
||||
--foreground: #fffffae6;
|
||||
--primary-cta: #fde047;
|
||||
--secondary-cta: #1a1a1a;
|
||||
--accent: #737373;
|
||||
--background-accent: #737373;
|
||||
}
|
||||
|
||||
/* Dark Theme - Light Blue */
|
||||
:root[data-theme="dark-light-blue"] {
|
||||
--background: #010912;
|
||||
--card: #152840;
|
||||
--foreground: #e6f0ff;
|
||||
--primary-cta: #cee7ff;
|
||||
--secondary-cta: #0e1a29;
|
||||
--accent: #3f5c79;
|
||||
--background-accent: #004a93;
|
||||
}
|
||||
|
||||
/* Dark Theme - Light Green */
|
||||
:root[data-theme="dark-light-green"] {
|
||||
--background: #000802;
|
||||
--card: #0b1a0b;
|
||||
--foreground: #e6ffe6;
|
||||
--primary-cta: #80da9b;
|
||||
--secondary-cta: #07170b;
|
||||
--accent: #38714a;
|
||||
--background-accent: #2c6541;
|
||||
}
|
||||
|
||||
/* Dark Theme - Light Red */
|
||||
:root[data-theme="dark-light-red"] {
|
||||
--background: #080000;
|
||||
--card: #1e0d0d;
|
||||
--foreground: #ffe6e6;
|
||||
--primary-cta: #ff7a7a;
|
||||
--secondary-cta: #1e0909;
|
||||
--accent: #7b4242;
|
||||
--background-accent: #65292c;
|
||||
}
|
||||
|
||||
/* Dark Theme - Dark Red */
|
||||
:root[data-theme="dark-dark-red"] {
|
||||
--background: #060000;
|
||||
--card: #1d0d0d;
|
||||
--foreground: #ffe6e6;
|
||||
--primary-cta: #ff3d4a;
|
||||
--secondary-cta: #1f0a0a;
|
||||
--accent: #7b2d2d;
|
||||
--background-accent: #b8111f;
|
||||
}
|
||||
|
||||
/* Dark Theme - Light Purple */
|
||||
:root[data-theme="dark-light-purple"] {
|
||||
--background: #020008;
|
||||
--card: #0b1a1d;
|
||||
--foreground: #f0e6ff;
|
||||
--primary-cta: #c89bff;
|
||||
--secondary-cta: #0b0717;
|
||||
--accent: #684f7b;
|
||||
--background-accent: #65417c;
|
||||
}
|
||||
|
||||
/* Dark Theme - Light Orange */
|
||||
:root[data-theme="dark-light-orange"] {
|
||||
--background: #080200;
|
||||
--card: #1a0d0b;
|
||||
--foreground: #ffe6d5;
|
||||
--primary-cta: #ffaa70;
|
||||
--secondary-cta: #170b07;
|
||||
--accent: #7b5e4a;
|
||||
--background-accent: #b8541e;
|
||||
}
|
||||
|
||||
|
||||
384
colorThemes.json
Normal file
@@ -0,0 +1,384 @@
|
||||
{
|
||||
"lightTheme": {
|
||||
"minimalDarkBlue": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#000612e6",
|
||||
"--primary-cta": "#15479c",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4"
|
||||
},
|
||||
"minimalDarkGreen": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#000f06e6",
|
||||
"--primary-cta": "#0a7039",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4"
|
||||
},
|
||||
"minimalLightRed": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#120006e6",
|
||||
"--primary-cta": "#e63946",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4"
|
||||
},
|
||||
"minimalLightOrange": {
|
||||
"--background": "#ffffff",
|
||||
"--card": "#f9f9f9",
|
||||
"--foreground": "#120a00e6",
|
||||
"--primary-cta": "#ff8c42",
|
||||
"--secondary-cta": "#f9f9f9",
|
||||
"--accent": "#e2e2e2",
|
||||
"--background-accent": "#c4c4c4"
|
||||
},
|
||||
"darkBlue": {
|
||||
"--background": "#f5faff",
|
||||
"--card": "#f1f8ff",
|
||||
"--foreground": "#001122",
|
||||
"--primary-cta": "#15479c",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#a8cce8",
|
||||
"--background-accent": "#7ba3cf"
|
||||
},
|
||||
"darkGreen": {
|
||||
"--background": "#fafffb",
|
||||
"--card": "#f7fffa",
|
||||
"--foreground": "#001a0a",
|
||||
"--primary-cta": "#0a7039",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#a8d9be",
|
||||
"--background-accent": "#6bbf8e"
|
||||
},
|
||||
"lightRed": {
|
||||
"--background": "#fffafa",
|
||||
"--card": "#fff7f7",
|
||||
"--foreground": "#1a0000",
|
||||
"--primary-cta": "#e63946",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#f5c4c7",
|
||||
"--background-accent": "#f09199"
|
||||
},
|
||||
"lightPurple": {
|
||||
"--background": "#fbfaff",
|
||||
"--card": "#f7f5ff",
|
||||
"--foreground": "#0f0022",
|
||||
"--primary-cta": "#8b5cf6",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#d8cef5",
|
||||
"--background-accent": "#c4a8f9"
|
||||
},
|
||||
"warmCream": {
|
||||
"--background": "#f6f0e9",
|
||||
"--card": "#efe7dd",
|
||||
"--foreground": "#2b180a",
|
||||
"--primary-cta": "#2b180a",
|
||||
"--secondary-cta": "#efe7dd",
|
||||
"--accent": "#94877c",
|
||||
"--background-accent": "#afa094"
|
||||
},
|
||||
"grayBlueAccent": {
|
||||
"--background": "#f5f5f5",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#1c1c1c",
|
||||
"--primary-cta": "#1c1c1c",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#15479c",
|
||||
"--background-accent": "#a8cce8"
|
||||
},
|
||||
"grayGreenAccent": {
|
||||
"--background": "#f5f5f5",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#1c1c1c",
|
||||
"--primary-cta": "#1c1c1c",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#159c49",
|
||||
"--background-accent": "#a8e8ba"
|
||||
},
|
||||
"grayRedAccent": {
|
||||
"--background": "#f5f5f5",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#1c1c1c",
|
||||
"--primary-cta": "#1c1c1c",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#e63946",
|
||||
"--background-accent": "#e8bea8"
|
||||
},
|
||||
"grayPurpleAccent": {
|
||||
"--background": "#f5f5f5",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#1c1c1c",
|
||||
"--primary-cta": "#1c1c1c",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#6139e6",
|
||||
"--background-accent": "#b3a8e8"
|
||||
},
|
||||
"warmBeige": {
|
||||
"--background": "#efebe5",
|
||||
"--card": "#f7f2ea",
|
||||
"--foreground": "#000000",
|
||||
"--primary-cta": "#000000",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#ffffff",
|
||||
"--background-accent": "#e1b875"
|
||||
},
|
||||
"grayTealGreen": {
|
||||
"--background": "#f5f5f5",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#1c1c1c",
|
||||
"--primary-cta": "#1f514c",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#159c49",
|
||||
"--background-accent": "#a8e8ba"
|
||||
},
|
||||
"grayNavyBlue": {
|
||||
"--background": "#f5f5f5",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#1c1c1c",
|
||||
"--primary-cta": "#1f3251",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#15479c",
|
||||
"--background-accent": "#a8cce8"
|
||||
},
|
||||
"grayBurgundyRed": {
|
||||
"--background": "#f5f5f5",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#1c1c1c",
|
||||
"--primary-cta": "#511f1f",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#e63946",
|
||||
"--background-accent": "#e8bea8"
|
||||
},
|
||||
"grayIndigoPurple": {
|
||||
"--background": "#f5f5f5",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#1c1c1c",
|
||||
"--primary-cta": "#341f51",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#6139e6",
|
||||
"--background-accent": "#b3a8e8"
|
||||
},
|
||||
"warmgrayPink": {
|
||||
"--background": "#f7f6f7",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#1b0c25",
|
||||
"--primary-cta": "#1b0c25",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#ff93e4",
|
||||
"--background-accent": "#e8a8c3"
|
||||
},
|
||||
"warmgrayOrange": {
|
||||
"--background": "#f7f6f7",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#25190c",
|
||||
"--primary-cta": "#ff6207",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#ffce93",
|
||||
"--background-accent": "#e8cfa8"
|
||||
},
|
||||
"warmgrayBlue": {
|
||||
"--background": "#f7f6f7",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#0c1325",
|
||||
"--primary-cta": "#0798ff",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#93c7ff",
|
||||
"--background-accent": "#a8cde8"
|
||||
},
|
||||
"warmgrayIndigo": {
|
||||
"--background": "#f7f6f7",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#0c1325",
|
||||
"--primary-cta": "#0b07ff",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#93b7ff",
|
||||
"--background-accent": "#a8bae8"
|
||||
},
|
||||
"lavenderPeach": {
|
||||
"--background": "#e3deea",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#27231f",
|
||||
"--primary-cta": "#27231f",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#c68a62",
|
||||
"--background-accent": "#c68a62"
|
||||
},
|
||||
"lavenderBlue": {
|
||||
"--background": "#e3deea",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#1f2027",
|
||||
"--primary-cta": "#1f2027",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#627dc6",
|
||||
"--background-accent": "#627dc6"
|
||||
},
|
||||
"warmStone": {
|
||||
"--background": "#f5f4ef",
|
||||
"--card": "#dad6cd",
|
||||
"--foreground": "#2a2928",
|
||||
"--primary-cta": "#2a2928",
|
||||
"--secondary-cta": "#ecebea",
|
||||
"--accent": "#ffffff",
|
||||
"--background-accent": "#c6b180"
|
||||
},
|
||||
"warmSand": {
|
||||
"--background": "#fcf6ec",
|
||||
"--card": "#f3ede2",
|
||||
"--foreground": "#2e2521",
|
||||
"--primary-cta": "#2e2521",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#b2a28b",
|
||||
"--background-accent": "#b2a28b"
|
||||
},
|
||||
"warmgrayGreen": {
|
||||
"--background": "#f7f6f7",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#032419",
|
||||
"--primary-cta": "#2bb889",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#09b97e",
|
||||
"--background-accent": "#a8e8c3"
|
||||
},
|
||||
"warmgrayRed": {
|
||||
"--background": "#f7f6f7",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#250c0d",
|
||||
"--primary-cta": "#b82b40",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#b90941",
|
||||
"--background-accent": "#e8a8b6"
|
||||
},
|
||||
"warmgrayYellow": {
|
||||
"--background": "#f7f6f7",
|
||||
"--card": "#ffffff",
|
||||
"--foreground": "#251a0c",
|
||||
"--primary-cta": "#f4c408",
|
||||
"--secondary-cta": "#ffffff",
|
||||
"--accent": "#f4ca26",
|
||||
"--background-accent": "#e8daa8"
|
||||
}
|
||||
},
|
||||
"darkTheme": {
|
||||
"minimal": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#ffffffe6",
|
||||
"--primary-cta": "#e6e6e6",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373"
|
||||
},
|
||||
"minimalLightBlue": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#f0f8ffe6",
|
||||
"--primary-cta": "#cee7ff",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373"
|
||||
},
|
||||
"minimalLightGreen": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#f5fffae6",
|
||||
"--primary-cta": "#80da9b",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373"
|
||||
},
|
||||
"minimalLightRed": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#fff5f5e6",
|
||||
"--primary-cta": "#ff7a7a",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373"
|
||||
},
|
||||
"minimalLightPurple": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#f8f5ffe6",
|
||||
"--primary-cta": "#c89bff",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373"
|
||||
},
|
||||
"minimalLightOrange": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#fffaf5e6",
|
||||
"--primary-cta": "#ffaa70",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373"
|
||||
},
|
||||
"minimalLightYellow": {
|
||||
"--background": "#0a0a0a",
|
||||
"--card": "#1a1a1a",
|
||||
"--foreground": "#fffffae6",
|
||||
"--primary-cta": "#fde047",
|
||||
"--secondary-cta": "#1a1a1a",
|
||||
"--accent": "#737373",
|
||||
"--background-accent": "#737373"
|
||||
},
|
||||
"lightBlue": {
|
||||
"--background": "#010912",
|
||||
"--card": "#152840",
|
||||
"--foreground": "#e6f0ff",
|
||||
"--primary-cta": "#cee7ff",
|
||||
"--secondary-cta": "#0e1a29",
|
||||
"--accent": "#3f5c79",
|
||||
"--background-accent": "#004a93"
|
||||
},
|
||||
"lightGreen": {
|
||||
"--background": "#000802",
|
||||
"--card": "#0b1a0b",
|
||||
"--foreground": "#e6ffe6",
|
||||
"--primary-cta": "#80da9b",
|
||||
"--secondary-cta": "#07170b",
|
||||
"--accent": "#38714a",
|
||||
"--background-accent": "#2c6541"
|
||||
},
|
||||
"lightRed": {
|
||||
"--background": "#080000",
|
||||
"--card": "#1e0d0d",
|
||||
"--foreground": "#ffe6e6",
|
||||
"--primary-cta": "#ff7a7a",
|
||||
"--secondary-cta": "#1e0909",
|
||||
"--accent": "#7b4242",
|
||||
"--background-accent": "#65292c"
|
||||
},
|
||||
"darkRed": {
|
||||
"--background": "#060000",
|
||||
"--card": "#1d0d0d",
|
||||
"--foreground": "#ffe6e6",
|
||||
"--primary-cta": "#ff3d4a",
|
||||
"--secondary-cta": "#1f0a0a",
|
||||
"--accent": "#7b2d2d",
|
||||
"--background-accent": "#b8111f"
|
||||
},
|
||||
"lightPurple": {
|
||||
"--background": "#020008",
|
||||
"--card": "#0b1a1d",
|
||||
"--foreground": "#f0e6ff",
|
||||
"--primary-cta": "#c89bff",
|
||||
"--secondary-cta": "#0b0717",
|
||||
"--accent": "#684f7b",
|
||||
"--background-accent": "#65417c"
|
||||
},
|
||||
"lightOrange": {
|
||||
"--background": "#080200",
|
||||
"--card": "#1a0d0b",
|
||||
"--foreground": "#ffe6d5",
|
||||
"--primary-cta": "#ffaa70",
|
||||
"--secondary-cta": "#170b07",
|
||||
"--accent": "#7b5e4a",
|
||||
"--background-accent": "#b8541e"
|
||||
}
|
||||
}
|
||||
}
|
||||
500
docs/ACCESSIBILITY.md
Normal file
@@ -0,0 +1,500 @@
|
||||
# Accessibility Standards
|
||||
|
||||
This document outlines accessibility (a11y) requirements for all components in the library, ensuring compatibility with screen readers and assistive technologies.
|
||||
|
||||
## Interactive Components
|
||||
|
||||
For buttons, links, and other interactive elements.
|
||||
|
||||
### Required Props
|
||||
|
||||
```tsx
|
||||
interface ButtonProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
// Accessibility props
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```tsx
|
||||
const Button = ({
|
||||
text,
|
||||
onClick,
|
||||
className = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
}: ButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel || text}
|
||||
className={cls(
|
||||
"base-button-styles",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Key Points
|
||||
|
||||
**ariaLabel:**
|
||||
- Optional prop with sensible fallback
|
||||
- Falls back to `text` content for buttons
|
||||
- Provides context for screen readers
|
||||
|
||||
**type:**
|
||||
- Default: `"button"`
|
||||
- Options: `"button" | "submit" | "reset"`
|
||||
- Prevents accidental form submission
|
||||
|
||||
**disabled:**
|
||||
- Default: `false`
|
||||
- Includes visual disabled states:
|
||||
- `disabled:cursor-not-allowed` - Shows not-allowed cursor
|
||||
- `disabled:opacity-50` - Reduces opacity for visual feedback
|
||||
|
||||
## Media Components
|
||||
|
||||
### Images
|
||||
|
||||
**Required Props:**
|
||||
```tsx
|
||||
interface ImageProps {
|
||||
imageSrc: string;
|
||||
imageAlt?: string; // Empty string for decorative images
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
```tsx
|
||||
const ImageComponent = ({
|
||||
imageSrc,
|
||||
imageAlt = "",
|
||||
className = "",
|
||||
}: ImageProps) => {
|
||||
return (
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={imageAlt}
|
||||
aria-hidden={imageAlt === ""}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- `imageAlt` - Alt text for images
|
||||
- Provide descriptive alt text for meaningful images
|
||||
- Use empty string (`""`) for decorative images
|
||||
- `aria-hidden={true}` - When alt is empty, mark as decorative
|
||||
- Screen readers will skip decorative images
|
||||
|
||||
### Videos
|
||||
|
||||
**Required Props:**
|
||||
```tsx
|
||||
interface VideoProps {
|
||||
videoSrc: string;
|
||||
videoAriaLabel?: string;
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
```tsx
|
||||
const VideoComponent = ({
|
||||
videoSrc,
|
||||
videoAriaLabel = "Video content",
|
||||
className = "",
|
||||
}: VideoProps) => {
|
||||
return (
|
||||
<video
|
||||
src={videoSrc}
|
||||
aria-label={videoAriaLabel}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- `videoAriaLabel` - Descriptive label for video element
|
||||
- Default: Sensible fallback like "Video content" or "Hero video"
|
||||
- Always include for screen reader context
|
||||
|
||||
### Media Content Pattern
|
||||
|
||||
For components supporting both images and videos:
|
||||
|
||||
```tsx
|
||||
interface HeroProps {
|
||||
imageSrc?: string;
|
||||
imageAlt?: string;
|
||||
videoSrc?: string;
|
||||
videoAriaLabel?: string;
|
||||
}
|
||||
|
||||
const Hero = ({
|
||||
imageSrc,
|
||||
imageAlt = "",
|
||||
videoSrc,
|
||||
videoAriaLabel = "Hero video",
|
||||
}: HeroProps) => {
|
||||
return (
|
||||
<>
|
||||
{videoSrc ? (
|
||||
<video
|
||||
src={videoSrc}
|
||||
aria-label={videoAriaLabel}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
) : (
|
||||
imageSrc && (
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={imageAlt}
|
||||
aria-hidden={imageAlt === ""}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Section Components
|
||||
|
||||
### Semantic HTML
|
||||
|
||||
Use semantic HTML elements for proper document structure:
|
||||
|
||||
```tsx
|
||||
<section> // For sections of content
|
||||
<header> // For page/section headers
|
||||
<nav> // For navigation
|
||||
<footer> // For page/section footers
|
||||
<article> // For self-contained content
|
||||
<aside> // For tangentially related content
|
||||
<main> // For main content area
|
||||
```
|
||||
|
||||
### Section Pattern
|
||||
|
||||
```tsx
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
description: string;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Section = ({
|
||||
title,
|
||||
description,
|
||||
ariaLabel = "Section name",
|
||||
className = "",
|
||||
}: SectionProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("w-full py-20", className)}
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<h2>{title}</h2>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Sensible Default aria-labels
|
||||
|
||||
Each section type should have a descriptive default:
|
||||
|
||||
```tsx
|
||||
// Hero section
|
||||
ariaLabel = "Hero section"
|
||||
|
||||
// About section
|
||||
ariaLabel = "About section"
|
||||
|
||||
// Feature section
|
||||
ariaLabel = "Features section"
|
||||
|
||||
// Testimonial section
|
||||
ariaLabel = "Testimonials section"
|
||||
|
||||
// Footer
|
||||
ariaLabel = "Footer"
|
||||
|
||||
// Navigation
|
||||
ariaLabel = "Navigation"
|
||||
```
|
||||
|
||||
### Heading Hierarchy
|
||||
|
||||
Maintain proper heading levels:
|
||||
|
||||
```tsx
|
||||
<h1> // Page title (once per page)
|
||||
<h2> // Section titles
|
||||
<h3> // Subsection titles
|
||||
<h4> // Card/component titles
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```tsx
|
||||
<section aria-label="Features section">
|
||||
<h2>Our Features</h2> {/* Section title */}
|
||||
<div>
|
||||
<h3>Feature One</h3> {/* Feature title */}
|
||||
<p>Description...</p>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
## Form Components
|
||||
|
||||
### Input Fields
|
||||
|
||||
```tsx
|
||||
interface InputProps {
|
||||
label: string;
|
||||
id: string;
|
||||
type?: string;
|
||||
required?: boolean;
|
||||
ariaLabel?: string;
|
||||
ariaDescribedBy?: string;
|
||||
}
|
||||
|
||||
const Input = ({
|
||||
label,
|
||||
id,
|
||||
type = "text",
|
||||
required = false,
|
||||
ariaLabel,
|
||||
ariaDescribedBy,
|
||||
}: InputProps) => {
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={id}>
|
||||
{label}
|
||||
{required && <span aria-label="required">*</span>}
|
||||
</label>
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
required={required}
|
||||
aria-label={ariaLabel || label}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Always associate `<label>` with input using `htmlFor` and `id`
|
||||
- Mark required fields visually and semantically
|
||||
- Use `aria-describedby` for error messages or hints
|
||||
|
||||
### Form Validation
|
||||
|
||||
```tsx
|
||||
const [error, setError] = useState("");
|
||||
|
||||
<input
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={error ? "error-message" : undefined}
|
||||
/>
|
||||
{error && (
|
||||
<p id="error-message" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
```
|
||||
|
||||
## Focus Management
|
||||
|
||||
### Focus Indicators
|
||||
|
||||
Never remove focus outlines without providing alternatives:
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG
|
||||
className="outline-none"
|
||||
|
||||
// ✅ CORRECT - Custom focus indicator
|
||||
className="focus:outline-none focus:ring-2 focus:ring-foreground/50"
|
||||
```
|
||||
|
||||
### Focus Trap (for modals/dialogs)
|
||||
|
||||
When implementing modals or dialogs, ensure:
|
||||
- Focus moves to modal when opened
|
||||
- Tab/Shift+Tab cycles through modal elements only
|
||||
- Focus returns to trigger element when closed
|
||||
- Escape key closes modal
|
||||
|
||||
## Keyboard Navigation
|
||||
|
||||
### Interactive Elements
|
||||
|
||||
All interactive elements must be keyboard accessible:
|
||||
|
||||
```tsx
|
||||
// Buttons and links work by default
|
||||
<button onClick={handleClick}>Click me</button>
|
||||
<a href="/page">Link</a>
|
||||
|
||||
// Custom interactive elements need tabIndex and keyboard handlers
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Custom button
|
||||
</div>
|
||||
```
|
||||
|
||||
### Skip Links
|
||||
|
||||
For navigation-heavy pages, provide skip links:
|
||||
|
||||
```tsx
|
||||
<a href="#main-content" className="sr-only focus:not-sr-only">
|
||||
Skip to main content
|
||||
</a>
|
||||
```
|
||||
|
||||
## Screen Reader Only Content
|
||||
|
||||
Use the `sr-only` class for content that should only be read by screen readers:
|
||||
|
||||
```tsx
|
||||
<span className="sr-only">Additional context for screen readers</span>
|
||||
```
|
||||
|
||||
Tailwind's `sr-only` class:
|
||||
```css
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
```
|
||||
|
||||
## ARIA Roles
|
||||
|
||||
Use ARIA roles when semantic HTML isn't sufficient:
|
||||
|
||||
```tsx
|
||||
// Navigation
|
||||
<nav role="navigation" aria-label="Main navigation">
|
||||
|
||||
// Button (for non-button elements)
|
||||
<div role="button" tabIndex={0}>
|
||||
|
||||
// Alert/Status messages
|
||||
<div role="alert">Error message</div>
|
||||
<div role="status">Loading...</div>
|
||||
|
||||
// Presentation (decorative)
|
||||
<div role="presentation">
|
||||
|
||||
// Dialog/Modal
|
||||
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
||||
```
|
||||
|
||||
## Loading States
|
||||
|
||||
Provide feedback for loading states:
|
||||
|
||||
```tsx
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
<button disabled={isLoading} aria-busy={isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<span className="sr-only">Loading...</span>
|
||||
<Spinner aria-hidden="true" />
|
||||
</>
|
||||
) : (
|
||||
"Submit"
|
||||
)}
|
||||
</button>
|
||||
```
|
||||
|
||||
## Accessibility Checklist
|
||||
|
||||
### Interactive Components
|
||||
- [ ] Add `ariaLabel` prop (optional with sensible fallback)
|
||||
- [ ] Add `type` prop for buttons (default: `"button"`)
|
||||
- [ ] Add `disabled` prop with visual states
|
||||
- [ ] Include disabled state styling (`disabled:cursor-not-allowed disabled:opacity-50`)
|
||||
- [ ] Ensure keyboard accessibility (Enter/Space for custom elements)
|
||||
- [ ] Provide custom focus indicators if removing default outline
|
||||
|
||||
### Media Components
|
||||
- [ ] Images: Add `imageAlt` prop
|
||||
- [ ] Images: Use `aria-hidden={true}` when alt is empty (decorative)
|
||||
- [ ] Videos: Add `videoAriaLabel` prop with sensible default
|
||||
- [ ] Provide meaningful default labels
|
||||
|
||||
### Section Components
|
||||
- [ ] Use semantic HTML (`<section>`, `<header>`, `<nav>`, `<footer>`)
|
||||
- [ ] Add `ariaLabel` prop with sensible default
|
||||
- [ ] Follow proper heading hierarchy (h1 → h2 → h3)
|
||||
- [ ] Use `w-full py-20` for section spacing (except hero/footer)
|
||||
- [ ] Use `w-content-width mx-auto` for content wrapper
|
||||
|
||||
### Form Components
|
||||
- [ ] Associate labels with inputs using `htmlFor` and `id`
|
||||
- [ ] Mark required fields semantically
|
||||
- [ ] Use `aria-invalid` for validation errors
|
||||
- [ ] Use `aria-describedby` for error messages/hints
|
||||
- [ ] Provide `role="alert"` for error messages
|
||||
|
||||
### General
|
||||
- [ ] Test with keyboard navigation (Tab, Shift+Tab, Enter, Space, Escape)
|
||||
- [ ] Test with screen reader (VoiceOver, NVDA, JAWS)
|
||||
- [ ] Ensure sufficient color contrast (WCAG AA minimum)
|
||||
- [ ] Provide focus indicators for all interactive elements
|
||||
- [ ] Use semantic HTML before ARIA roles
|
||||
- [ ] Include screen reader only text when needed (`sr-only`)
|
||||
491
docs/CARDSTACK_SECTIONS.md
Normal file
@@ -0,0 +1,491 @@
|
||||
# CardStack Section Pattern
|
||||
|
||||
This document covers the CardStack pattern used in Feature, Product, Pricing, Testimonial, Team, Blog, and Metrics section components.
|
||||
|
||||
## Required Type Imports
|
||||
|
||||
```tsx
|
||||
// Centralized type definitions
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
|
||||
```
|
||||
|
||||
**Key Types:**
|
||||
- `TextboxLayout` - Layout options for section headers ("default" | "split" | "split-actions" | "split-description" | "inline-image")
|
||||
- `InvertedBackground` - Background inversion options ("noInvert" | "invertDefault")
|
||||
- `TitleSegment` - Type for inline image segments ({ type: "text" | "image", content/src, alt? })
|
||||
- `ButtonConfig` - Button configuration interface
|
||||
- `GridVariant` - Grid layout variants
|
||||
- `CardAnimationType` - Card animation types
|
||||
|
||||
## Overview
|
||||
|
||||
CardStack is an intelligent layout component that automatically switches between grid, carousel, and timeline layouts based on item count and configuration.
|
||||
|
||||
**Mode Selection (Automatic):**
|
||||
- **1-4 items**: Grid mode (displays as bento grid)
|
||||
- **5+ items**: Carousel mode (auto-scrolling or button-controlled)
|
||||
- **3-6 items with timeline variant**: Timeline layout (or carousel if 7+)
|
||||
|
||||
## Grid Variants
|
||||
|
||||
There are 9 bento grid layouts plus uniform layouts:
|
||||
|
||||
### Uniform Layouts
|
||||
- `uniform-all-items-equal` - All items same size (default)
|
||||
- `uniform-2-items` - Two equal columns
|
||||
- `uniform-3-items` - Three equal columns
|
||||
- `uniform-4-items` - Four equal columns
|
||||
|
||||
### Bento Layouts (Asymmetric)
|
||||
- `two-columns-alternating-heights` - Alternating tall/short columns
|
||||
- `asymmetric-60-wide-40-narrow` - 60% wide left, 40% narrow right
|
||||
- `three-columns-all-equal-width` - Three equal columns
|
||||
- `four-items-2x2-equal-grid` - Perfect 2x2 grid
|
||||
- `one-large-right-three-stacked-left` - Left: 3 items, Right: 1 large
|
||||
- `items-top-row-full-width-bottom` - Top: full width, Bottom: items
|
||||
- `full-width-top-items-bottom-row` - Full width top, items below
|
||||
- `one-large-left-three-stacked-right` - Left: 1 large, Right: 3 items
|
||||
- `timeline` - Zigzag timeline layout
|
||||
|
||||
## Height Control Pattern
|
||||
|
||||
### uniformGridCustomHeightClasses Prop
|
||||
|
||||
All CardStack-based components should accept this optional prop to control item heights in both grid and carousel modes.
|
||||
|
||||
```tsx
|
||||
interface SectionCardProps {
|
||||
items: ItemType[];
|
||||
gridVariant: GridVariant;
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
carouselMode?: "auto" | "buttons";
|
||||
// ... other props
|
||||
}
|
||||
```
|
||||
|
||||
### Default Values by Component Type
|
||||
|
||||
**Most components (Feature, Product, Pricing, Team, Metrics, Blog):**
|
||||
```tsx
|
||||
uniformGridCustomHeightClasses = "min-h-80 2xl:min-h-90"
|
||||
```
|
||||
|
||||
**Testimonial components (need flexible heights):**
|
||||
```tsx
|
||||
uniformGridCustomHeightClasses = "min-h-none"
|
||||
```
|
||||
|
||||
**Hero carousel components (no minimum):**
|
||||
```tsx
|
||||
uniformGridCustomHeightClasses = "min-h-0"
|
||||
```
|
||||
|
||||
**Feature components (optimized for compact layout):**
|
||||
```tsx
|
||||
// Hardcoded in FeatureCardFour
|
||||
uniformGridCustomHeightClasses = "min-h-0"
|
||||
```
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
The prop flows: Section Component → CardStack → GridLayout/Carousel
|
||||
|
||||
```tsx
|
||||
// In section component (e.g., ProductCardOne.tsx)
|
||||
const ProductCardOne = ({
|
||||
products,
|
||||
gridVariant,
|
||||
uniformGridCustomHeightClasses = "min-h-80 2xl:min-h-90",
|
||||
// ... other props
|
||||
}: ProductCardOneProps) => {
|
||||
return (
|
||||
<CardStack
|
||||
gridVariant={gridVariant}
|
||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
||||
// ... other props
|
||||
>
|
||||
{products.map((product) => (
|
||||
<div className="card">...</div>
|
||||
))}
|
||||
</CardStack>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Individual card elements must use `min-h-0`:**
|
||||
```tsx
|
||||
<div className={cls("card p-6 rounded-theme-capped h-full min-h-0")}>
|
||||
{/* Product content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
This prevents height conflicts and ensures proper flex behavior.
|
||||
|
||||
## Carousel Modes
|
||||
|
||||
### carouselMode Prop
|
||||
|
||||
```tsx
|
||||
carouselMode?: "auto" | "buttons"
|
||||
```
|
||||
|
||||
- **`"auto"`** - Auto-scrolling carousel (uses AutoCarousel with embla-carousel-auto-scroll)
|
||||
- **`"buttons"`** - Button-controlled carousel (uses ButtonCarousel with prev/next buttons)
|
||||
|
||||
**Default is typically `"buttons"`** for better accessibility and user control.
|
||||
|
||||
## TextBox Integration
|
||||
|
||||
CardStack components integrate with TextBox for section headers.
|
||||
|
||||
### TextBox Layout Options
|
||||
|
||||
```tsx
|
||||
import type { TextboxLayout } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
textboxLayout: TextboxLayout // "default" | "split" | "split-actions" | "split-description" | "inline-image"
|
||||
```
|
||||
|
||||
**Layout Modes:**
|
||||
|
||||
1. **`"default"`** - Title and description stacked vertically, centered or left-aligned
|
||||
```
|
||||
[Tag]
|
||||
Title
|
||||
Description
|
||||
[Buttons]
|
||||
```
|
||||
|
||||
2. **`"split"`** - Title and description on left (60%), buttons on right (40%)
|
||||
```
|
||||
[Tag]
|
||||
Title | Description | [Buttons]
|
||||
```
|
||||
|
||||
3. **`"split-actions"`** - Title and description on left, buttons on right (no description on right)
|
||||
```
|
||||
[Tag]
|
||||
Title | [Buttons]
|
||||
Description |
|
||||
```
|
||||
|
||||
4. **`"split-description"`** - Title on left, description on right, buttons below
|
||||
```
|
||||
[Tag]
|
||||
Title | Description
|
||||
[Buttons]
|
||||
```
|
||||
|
||||
5. **`"inline-image"`** - Centered heading with inline images between text segments, buttons below
|
||||
```
|
||||
Text [Image] Text [Image] Text
|
||||
[Buttons]
|
||||
```
|
||||
|
||||
**Special props for inline-image layout:**
|
||||
```tsx
|
||||
import type { TitleSegment } from "@/components/cardStack/types";
|
||||
|
||||
titleSegments?: TitleSegment[] // Array of text and image segments
|
||||
titleImageWrapperClassName?: string // Styling for image wrapper
|
||||
titleImageClassName?: string // Styling for the image itself
|
||||
```
|
||||
|
||||
**Example usage:**
|
||||
```tsx
|
||||
<FeatureCardOne
|
||||
titleSegments={[
|
||||
{ type: "text", content: "Discover" },
|
||||
{ type: "image", src: "/icon.png", alt: "Icon" },
|
||||
{ type: "text", content: "powerful features" }
|
||||
]}
|
||||
textboxLayout="inline-image"
|
||||
// ... other props
|
||||
/>
|
||||
```
|
||||
|
||||
**Inline Image Behavior:**
|
||||
- Images are styled with `primary-button` background
|
||||
- Automatic rotation alternation: 1st: -rotate-12, 2nd: rotate-12, etc.
|
||||
- Square aspect ratio (1.1em height)
|
||||
- Proper spacing with mx-1 margins
|
||||
- Supports both local paths and external URLs
|
||||
|
||||
### TextBox Props in CardStack
|
||||
|
||||
```tsx
|
||||
<CardStack
|
||||
title="Our Products"
|
||||
titleSegments={[
|
||||
{ type: "text", content: "Our" },
|
||||
{ type: "image", src: "/icon.png", alt: "Product Icon" },
|
||||
{ type: "text", content: "Products" }
|
||||
]} // Optional: use titleSegments for inline-image layout
|
||||
description="Discover our latest offerings"
|
||||
tag="Products"
|
||||
tagIcon={Package}
|
||||
buttons={[
|
||||
{ text: "View All", href: "/products" }
|
||||
]}
|
||||
textboxLayout="split" // or "inline-image" with titleSegments
|
||||
useInvertedBackground="noInvert" // "noInvert" | "invertDefault"
|
||||
// TextBox className overrides
|
||||
textBoxClassName=""
|
||||
titleClassName=""
|
||||
titleImageWrapperClassName="" // For inline-image layout
|
||||
titleImageClassName="" // For inline-image layout
|
||||
descriptionClassName=""
|
||||
tagClassName=""
|
||||
buttonContainerClassName=""
|
||||
buttonClassName=""
|
||||
buttonTextClassName=""
|
||||
// ... other props
|
||||
>
|
||||
```
|
||||
|
||||
## Button System
|
||||
|
||||
### ButtonConfig Interface
|
||||
|
||||
```tsx
|
||||
interface ButtonConfig {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
props?: Partial<ButtonPropsForVariant<CTAButtonVariant>>;
|
||||
// NO variant property - controlled by ThemeProvider
|
||||
}
|
||||
```
|
||||
|
||||
### Button Rendering Logic
|
||||
|
||||
Buttons are rendered with automatic primary/secondary styling:
|
||||
|
||||
- **Index 0**: Primary button (`primary-button` class)
|
||||
- **Index 1+**: Secondary button (`secondary-button` class)
|
||||
|
||||
**Maximum 2 buttons** per section (enforced in TextBox component).
|
||||
|
||||
```tsx
|
||||
const buttons: ButtonConfig[] = [
|
||||
{ text: "Get Started", href: "/signup" }, // Primary
|
||||
{ text: "Learn More", onClick: () => {} } // Secondary
|
||||
];
|
||||
```
|
||||
|
||||
The `variant` is determined by ThemeProvider's `defaultButtonVariant` setting.
|
||||
|
||||
## Complete CardStack Section Example
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import React, { memo } from "react";
|
||||
import CardStack from "@/components/cardStack/CardStack";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { GridVariant, ButtonConfig } from "@/components/cardStack/types";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
type Product = {
|
||||
title: string;
|
||||
description: string;
|
||||
price: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
interface ProductCardOneProps {
|
||||
products: Product[];
|
||||
carouselMode?: "auto" | "buttons";
|
||||
gridVariant: GridVariant;
|
||||
uniformGridCustomHeightClasses?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
buttons?: ButtonConfig[];
|
||||
textboxLayout: "default" | "split" | "split-actions" | "split-description";
|
||||
ariaLabel?: string;
|
||||
// Main wrapper
|
||||
className?: string;
|
||||
// CardStack customization
|
||||
cardStackClassName?: string;
|
||||
// TextBox customization
|
||||
textBoxClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
tagClassName?: string;
|
||||
buttonsWrapperClassName?: string;
|
||||
// Card customization
|
||||
cardClassName?: string;
|
||||
cardTitleClassName?: string;
|
||||
cardDescriptionClassName?: string;
|
||||
cardPriceClassName?: string;
|
||||
cardImageClassName?: string;
|
||||
}
|
||||
|
||||
const ProductCardOne = ({
|
||||
products,
|
||||
carouselMode = "buttons",
|
||||
gridVariant,
|
||||
uniformGridCustomHeightClasses = "min-h-80 2xl:min-h-90",
|
||||
title,
|
||||
description,
|
||||
tag,
|
||||
tagIcon,
|
||||
buttons,
|
||||
textboxLayout,
|
||||
ariaLabel = "Product section",
|
||||
className = "",
|
||||
cardStackClassName = "",
|
||||
textBoxClassName = "",
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
tagClassName = "",
|
||||
buttonsWrapperClassName = "",
|
||||
cardClassName = "",
|
||||
cardTitleClassName = "",
|
||||
cardDescriptionClassName = "",
|
||||
cardPriceClassName = "",
|
||||
cardImageClassName = "",
|
||||
}: ProductCardOneProps) => {
|
||||
return (
|
||||
<section
|
||||
aria-label={ariaLabel}
|
||||
className={cls("w-full py-20", className)}
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<CardStack
|
||||
mode={carouselMode}
|
||||
gridVariant={gridVariant}
|
||||
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
textboxLayout={textboxLayout}
|
||||
className={cardStackClassName}
|
||||
textBoxClassName={textBoxClassName}
|
||||
titleClassName={titleClassName}
|
||||
descriptionClassName={descriptionClassName}
|
||||
tagClassName={tagClassName}
|
||||
buttonsWrapperClassName={buttonsWrapperClassName}
|
||||
>
|
||||
{products.map((product, index) => (
|
||||
<div
|
||||
key={`${product.title}-${index}`}
|
||||
className={cls(
|
||||
"card p-6 rounded-theme-capped h-full min-h-0 flex flex-col",
|
||||
cardClassName
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.title}
|
||||
className={cls("w-full h-48 object-cover mb-4", cardImageClassName)}
|
||||
/>
|
||||
<h3 className={cls("text-xl font-semibold mb-2", cardTitleClassName)}>
|
||||
{product.title}
|
||||
</h3>
|
||||
<p className={cls("text-foreground/75 flex-1", cardDescriptionClassName)}>
|
||||
{product.description}
|
||||
</p>
|
||||
<p className={cls("text-2xl font-bold mt-4", cardPriceClassName)}>
|
||||
{product.price}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</CardStack>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
ProductCardOne.displayName = "ProductCardOne";
|
||||
|
||||
export default memo(ProductCardOne);
|
||||
```
|
||||
|
||||
## Animation Types
|
||||
|
||||
CardStack supports GSAP-powered scroll-triggered animations:
|
||||
|
||||
```tsx
|
||||
animationType?: "none" | "opacity" | "slide-up" | "scale-rotate" | "blur-reveal"
|
||||
```
|
||||
|
||||
**Animation Descriptions:**
|
||||
- **`none`** - No animation
|
||||
- **`opacity`** - Fade in
|
||||
- **`slide-up`** - Slide up from below with stagger
|
||||
- **`scale-rotate`** - Scale + rotate entrance with stagger
|
||||
- **`blur-reveal`** - Blur to clear reveal with stagger
|
||||
|
||||
Each animation uses GSAP ScrollTrigger with staggered effect on children.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO:
|
||||
|
||||
- Accept `uniformGridCustomHeightClasses` as optional prop with sensible default
|
||||
- Use `min-h-0` on individual card elements for proper flex behavior
|
||||
- Pass through all CardStack customization props (className overrides)
|
||||
- Use appropriate default height for your component type
|
||||
- Document the default value in registry propsSchema
|
||||
- Provide className props for all card sub-elements
|
||||
- Use `card` class for consistent card styling (theme-aware)
|
||||
- Use `rounded-theme-capped` for card border radius
|
||||
- Set `carouselMode` default to `"buttons"` for accessibility
|
||||
|
||||
### ❌ DO NOT:
|
||||
|
||||
- Hardcode height classes in CardStack (let it be controlled via prop)
|
||||
- Remove the `uniformGridCustomHeightClasses` prop without specific reason
|
||||
- Use different height classes for grid vs carousel (they should match)
|
||||
- Forget to apply `min-h-0` on card wrapper divs
|
||||
- Specify button `variant` in ButtonConfig (controlled by ThemeProvider)
|
||||
- Create more than 2 buttons per section
|
||||
- Use `lg:` or `xl:` breakpoints for layout changes
|
||||
|
||||
## CardStack Component Checklist
|
||||
|
||||
When creating a new CardStack-based section component:
|
||||
|
||||
### Props & Configuration
|
||||
- [ ] Accept `uniformGridCustomHeightClasses` prop with appropriate default
|
||||
- [ ] Accept `carouselMode` prop (default: `"buttons"`)
|
||||
- [ ] Accept `gridVariant` as required prop (or hardcode if single variant)
|
||||
- [ ] Accept `textboxLayout` for TextBox configuration
|
||||
- [ ] Accept `animationType` for scroll animations (optional)
|
||||
|
||||
### CardStack Integration
|
||||
- [ ] Pass `uniformGridCustomHeightClasses` to CardStack
|
||||
- [ ] Pass all TextBox props (title, description, tag, tagIcon, buttons)
|
||||
- [ ] Pass all TextBox className overrides
|
||||
- [ ] Pass cardStackClassName for CardStack wrapper customization
|
||||
|
||||
### Card Implementation
|
||||
- [ ] Apply `min-h-0` to individual card wrapper divs
|
||||
- [ ] Use `card` class for card background/border styling
|
||||
- [ ] Use `rounded-theme-capped` for border radius
|
||||
- [ ] Provide className override props for all card sub-elements
|
||||
- [ ] Use `h-full` on cards for consistent heights within grid
|
||||
|
||||
### Button System
|
||||
- [ ] Use ButtonConfig type for buttons array
|
||||
- [ ] Do NOT specify variant in ButtonConfig
|
||||
- [ ] Maximum 2 buttons
|
||||
- [ ] Let ThemeProvider control button variant
|
||||
|
||||
### Section Structure
|
||||
- [ ] Use semantic `<section>` tag with aria-label
|
||||
- [ ] Use `w-full py-20` on section
|
||||
- [ ] Use `w-content-width mx-auto` wrapper
|
||||
- [ ] Provide section className override
|
||||
|
||||
### Documentation
|
||||
- [ ] Document `uniformGridCustomHeightClasses` default in registry
|
||||
- [ ] Document all grid variants supported
|
||||
- [ ] Document carousel mode options
|
||||
- [ ] Include usage example with typical props
|
||||
433
docs/COMPONENT_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,433 @@
|
||||
# Component Implementation Standards
|
||||
|
||||
This document outlines the core implementation patterns for creating components in this library, optimized for AI website builders.
|
||||
|
||||
## Component Structure Template
|
||||
|
||||
Every component should follow this structure:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface ComponentProps {
|
||||
// Required props first
|
||||
text: string;
|
||||
// Optional props with explicit types
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
}
|
||||
|
||||
const Component = ({
|
||||
text,
|
||||
onClick,
|
||||
className = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
}: ComponentProps) => {
|
||||
return (
|
||||
<element
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel || text}
|
||||
className={cls("base-classes", "disabled-states", className)}
|
||||
>
|
||||
{text}
|
||||
</element>
|
||||
);
|
||||
};
|
||||
|
||||
Component.displayName = "Component";
|
||||
|
||||
export default React.memo(Component);
|
||||
```
|
||||
|
||||
**Key Requirements:**
|
||||
- `"use client"` directive when needed (interactive components, hooks)
|
||||
- Named exports with `displayName` for debugging
|
||||
- Wrap in `React.memo()` for performance optimization
|
||||
- Use `cls()` utility for class composition (never plain string concatenation)
|
||||
|
||||
## Prop Structure & Defaults
|
||||
|
||||
### Required Props
|
||||
Core content props should be **required** with no default values:
|
||||
- Section components: `title`, `description`
|
||||
- Button components: `text`
|
||||
- Media components: `imageSrc` or `videoSrc` (when applicable)
|
||||
|
||||
### Optional Props with Defaults
|
||||
|
||||
**Standard className defaults:**
|
||||
```tsx
|
||||
className = "",
|
||||
textClassName = "",
|
||||
iconClassName = "",
|
||||
containerClassName = "",
|
||||
```
|
||||
|
||||
Empty string defaults prevent undefined checks and are standard practice.
|
||||
|
||||
**Common optional props:**
|
||||
```tsx
|
||||
disabled = false,
|
||||
type = "button",
|
||||
ariaLabel, // No default, falls back to sensible value in component
|
||||
```
|
||||
|
||||
**Component-specific props:**
|
||||
Document defaults clearly in both code and registry:
|
||||
```tsx
|
||||
strengthFactor = 20,
|
||||
carouselMode = "buttons",
|
||||
uniformGridCustomHeightClasses = "min-h-80 2xl:min-h-90",
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Section Components (Hero, About, Feature, etc.)
|
||||
|
||||
**✅ CORRECT:**
|
||||
```tsx
|
||||
interface HeroProps {
|
||||
title: string; // Primary heading
|
||||
description: string; // Supporting text
|
||||
buttons?: ButtonConfig[];
|
||||
}
|
||||
```
|
||||
|
||||
**❌ WRONG:**
|
||||
```tsx
|
||||
interface HeroProps {
|
||||
heading: string; // Should be "title"
|
||||
subtitle: string; // Should be "description"
|
||||
text: string; // Ambiguous
|
||||
}
|
||||
```
|
||||
|
||||
### Button Components
|
||||
|
||||
**✅ CORRECT:**
|
||||
```tsx
|
||||
interface ButtonProps {
|
||||
text: string; // Button label
|
||||
onClick?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**❌ WRONG:**
|
||||
```tsx
|
||||
interface ButtonProps {
|
||||
title: string; // Should be "text"
|
||||
label: string; // Should be "text"
|
||||
}
|
||||
```
|
||||
|
||||
### Button Config (for sections)
|
||||
|
||||
```tsx
|
||||
interface ButtonConfig {
|
||||
text: string; // Button label (not "title" or "label")
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
props?: Partial<ButtonPropsForVariant<CTAButtonVariant>>;
|
||||
// NO variant property - controlled by ThemeProvider
|
||||
}
|
||||
```
|
||||
|
||||
**Consistency is critical:**
|
||||
- All hero sections must use the same prop names
|
||||
- All about sections must use the same prop names
|
||||
- Registry documentation must match component prop names exactly
|
||||
|
||||
## Component Customizability
|
||||
|
||||
Provide className props for **all major elements** to allow full styling control:
|
||||
|
||||
```tsx
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
description: string;
|
||||
// Main wrapper
|
||||
className?: string;
|
||||
// Inner container
|
||||
containerClassName?: string;
|
||||
// Content areas
|
||||
textClassName?: string;
|
||||
mediaWrapperClassName?: string;
|
||||
imageClassName?: string;
|
||||
}
|
||||
|
||||
const Section = ({
|
||||
title,
|
||||
description,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
textClassName = "",
|
||||
mediaWrapperClassName = "",
|
||||
imageClassName = "",
|
||||
}: SectionProps) => {
|
||||
return (
|
||||
<section className={cls("base-section-styles", className)}>
|
||||
<div className={cls("base-container-styles", containerClassName)}>
|
||||
<div className={cls("base-text-styles", textClassName)}>
|
||||
{/* content */}
|
||||
</div>
|
||||
<div className={cls("base-media-wrapper-styles", mediaWrapperClassName)}>
|
||||
<img className={cls("base-image-styles", imageClassName)} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Naming convention:**
|
||||
- `className` - Main wrapper element
|
||||
- `containerClassName` - Inner container
|
||||
- `[element]ClassName` - Specific elements (e.g., `textClassName`, `imageClassName`)
|
||||
|
||||
## Component Composition & Base Styles
|
||||
|
||||
When composing higher-level components from base components, **set sensible base styles** while accepting className overrides:
|
||||
|
||||
```tsx
|
||||
interface HeroProps {
|
||||
title: string;
|
||||
description: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
textBoxClassName?: string;
|
||||
}
|
||||
|
||||
const Hero = ({
|
||||
title,
|
||||
description,
|
||||
titleClassName = "",
|
||||
descriptionClassName = "",
|
||||
textBoxClassName = "",
|
||||
}: HeroProps) => {
|
||||
return (
|
||||
<section>
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
// Set base styles, allow overrides
|
||||
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
|
||||
titleClassName={cls("text-6xl font-medium", titleClassName)}
|
||||
descriptionClassName={cls("text-lg leading-[1.2]", descriptionClassName)}
|
||||
center={true}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Key principles:**
|
||||
- Base styles come first in `cls()`, overrides second
|
||||
- This ensures good defaults while maintaining full customizability
|
||||
- AI builders can use components without styling knowledge, but advanced users can override
|
||||
- Use `cls()` utility for proper class merging (prevents Tailwind conflicts)
|
||||
|
||||
## Type Safety
|
||||
|
||||
### Use Explicit Prop Interfaces
|
||||
```tsx
|
||||
// ✅ CORRECT - Clear and explicit
|
||||
interface ButtonProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
variant?: "primary" | "secondary";
|
||||
}
|
||||
|
||||
// ❌ WRONG - Over-complicated
|
||||
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
|
||||
// ... harder for AI to understand
|
||||
}
|
||||
```
|
||||
|
||||
### Use Discriminated Unions for Variants
|
||||
```tsx
|
||||
type MediaProps =
|
||||
| {
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
videoSrc?: never;
|
||||
}
|
||||
| {
|
||||
videoSrc: string;
|
||||
videoAriaLabel?: string;
|
||||
imageSrc?: never;
|
||||
};
|
||||
```
|
||||
|
||||
### Export Reusable Types
|
||||
```tsx
|
||||
export type ButtonConfig = {
|
||||
text: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export type GridVariant =
|
||||
| "uniform-all-items-equal"
|
||||
| "two-columns-alternating-heights"
|
||||
| "asymmetric-60-wide-40-narrow"
|
||||
// ... etc
|
||||
```
|
||||
|
||||
## Responsive Design
|
||||
|
||||
### Mobile-First Approach
|
||||
|
||||
**Default styles apply to mobile devices:**
|
||||
```tsx
|
||||
// ✅ CORRECT - Mobile until md breakpoint (768px)
|
||||
<div className="flex-col md:flex-row">
|
||||
<img className="w-full h-auto md:h-8 md:w-auto" />
|
||||
</div>
|
||||
|
||||
// ❌ WRONG - Using lg: breakpoint
|
||||
<div className="flex-col lg:flex-row">
|
||||
<img className="w-full h-auto lg:h-8 lg:w-auto" />
|
||||
</div>
|
||||
```
|
||||
|
||||
**Breakpoint Rules:**
|
||||
- **Mobile styles**: No prefix (default)
|
||||
- **Desktop styles**: `md:` prefix only (768px breakpoint)
|
||||
- **Never use**: `lg:`, `xl:`, `2xl:` breakpoints for layout changes
|
||||
|
||||
**Exceptions:** Only use larger breakpoints for minor tweaks:
|
||||
```tsx
|
||||
// Acceptable for minor adjustments
|
||||
className="min-h-80 2xl:min-h-90"
|
||||
```
|
||||
|
||||
## Content Width Pattern
|
||||
|
||||
All section content must follow this structure:
|
||||
|
||||
```tsx
|
||||
<section aria-label={ariaLabel || "Section name"} className="w-full py-20">
|
||||
<div className="w-content-width mx-auto">
|
||||
{/* content */}
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Section: `w-full py-20` (full width with vertical padding)
|
||||
- Inner div: `w-content-width mx-auto` (centered content with max width)
|
||||
- `w-content-width` is controlled by ThemeProvider (small/medium/large)
|
||||
|
||||
**Exceptions:**
|
||||
- Heroes and footers do NOT use `py-20` (they have custom spacing)
|
||||
- Full-bleed sections may skip inner wrapper
|
||||
|
||||
## Vertical Spacing
|
||||
|
||||
**Standard sections:**
|
||||
```tsx
|
||||
className="w-full py-20"
|
||||
```
|
||||
|
||||
**Exceptions (NO py-20):**
|
||||
- Hero sections (custom spacing)
|
||||
- Footer sections (custom spacing)
|
||||
- Full-bleed sections with background colors
|
||||
|
||||
## Text Constraints
|
||||
|
||||
For button text and short labels:
|
||||
```tsx
|
||||
{
|
||||
"text": {
|
||||
"required": true,
|
||||
"example": "Get Started",
|
||||
"minChars": 2,
|
||||
"maxChars": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Button text rules:**
|
||||
- Minimum: 2 characters
|
||||
- Maximum: 15 characters
|
||||
- Single-line only (no multiline support)
|
||||
|
||||
## Section Structure Pattern
|
||||
|
||||
```tsx
|
||||
<section
|
||||
aria-label={ariaLabel || "Default section label"}
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground === "invertDefault" && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<TextBox
|
||||
title={title}
|
||||
description={description}
|
||||
tag={tag}
|
||||
tagIcon={tagIcon}
|
||||
buttons={buttons}
|
||||
textboxLayout={textboxLayout}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
// ... className overrides
|
||||
/>
|
||||
|
||||
{/* Section-specific content */}
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Key Pattern Notes:**
|
||||
- `useInvertedBackground` is a required prop: `"noInvert" | "invertDefault"`
|
||||
- `"invertDefault"` creates a full-width inverted section with `bg-foreground`
|
||||
- `"noInvert"` is the standard section with no background
|
||||
- Always use explicit string equality checks (not truthy/falsy)
|
||||
- Text colors should check for `"invertDefault"` mode
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Core Requirements
|
||||
- [ ] Add `"use client"` directive if needed (hooks, interactivity)
|
||||
- [ ] Use explicit prop interfaces (no over-complicated types)
|
||||
- [ ] Set appropriate defaults for optional props
|
||||
- [ ] Add `displayName` for debugging
|
||||
- [ ] Wrap in `React.memo()` for performance
|
||||
- [ ] Use semantic HTML tags (`<section>`, `<button>`, etc.)
|
||||
|
||||
### Customizability
|
||||
- [ ] Provide className props for all major elements
|
||||
- [ ] Use `cls()` utility for class composition
|
||||
- [ ] Set base styles with override capability
|
||||
- [ ] Follow naming convention (className, containerClassName, [element]ClassName)
|
||||
|
||||
### Responsive Design
|
||||
- [ ] Mobile-first styles (no prefix)
|
||||
- [ ] Desktop styles with `md:` prefix only
|
||||
- [ ] Avoid `lg:`, `xl:`, `2xl:` for layout changes
|
||||
- [ ] Use `w-content-width mx-auto` pattern
|
||||
|
||||
### Naming Conventions
|
||||
- [ ] Section components: Use `title` and `description`
|
||||
- [ ] Button components: Use `text`
|
||||
- [ ] Button configs: Use `text` (not variant - controlled by theme)
|
||||
- [ ] Consistent naming across similar component types
|
||||
|
||||
### Structure
|
||||
- [ ] Required props first in interface
|
||||
- [ ] Optional props with defaults after
|
||||
- [ ] Empty string defaults for className props
|
||||
- [ ] Document default values clearly
|
||||
499
docs/PREVIEW_PAGE_STANDARDS.md
Normal file
@@ -0,0 +1,499 @@
|
||||
# Preview Page Standards
|
||||
|
||||
This document outlines how to create preview pages for components in `/app/components/`.
|
||||
|
||||
## Purpose
|
||||
|
||||
Preview pages allow developers and AI builders to:
|
||||
- See components in isolation
|
||||
- Test component behavior and styling
|
||||
- Verify responsive design
|
||||
- Experiment with different prop configurations
|
||||
- Ensure smooth scrolling and theme integration
|
||||
|
||||
## File Structure
|
||||
|
||||
### Location Pattern
|
||||
|
||||
```
|
||||
/app/components/
|
||||
├── sections/
|
||||
│ ├── hero/
|
||||
│ │ ├── billboard/
|
||||
│ │ │ └── page.tsx // Preview for HeroBillboard
|
||||
│ │ ├── split/
|
||||
│ │ │ └── page.tsx // Preview for HeroSplit
|
||||
│ ├── feature/
|
||||
│ │ ├── card-one/
|
||||
│ │ │ └── page.tsx // Preview for FeatureCardOne
|
||||
├── buttons/
|
||||
│ ├── text-stagger/
|
||||
│ │ └── page.tsx // Preview for ButtonTextStagger
|
||||
└── page.tsx // Main components index
|
||||
```
|
||||
|
||||
**Pattern:** `/app/components/[category]/[component-name]/page.tsx`
|
||||
|
||||
**Component name formatting:**
|
||||
- Use kebab-case for folder names
|
||||
- `HeroBillboard` → `hero/billboard/`
|
||||
- `FeatureCardOne` → `feature/card-one/`
|
||||
- `ButtonTextStagger` → `buttons/text-stagger/`
|
||||
|
||||
## Preview Page Template
|
||||
|
||||
### Basic Template (Non-Section Components)
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import ReactLenis from "lenis/react";
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import ComponentName from "@/components/category/ComponentName";
|
||||
|
||||
export default function ComponentPreviewPage() {
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="text-stagger"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="rounded"
|
||||
contentWidth="medium"
|
||||
sizing="medium"
|
||||
background="plain"
|
||||
cardStyle="glass-flat"
|
||||
primaryButtonStyle="gradient"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="medium"
|
||||
>
|
||||
<ReactLenis root>
|
||||
<ComponentName
|
||||
// Add realistic props here
|
||||
text="Example"
|
||||
onClick={() => console.log("clicked")}
|
||||
/>
|
||||
</ReactLenis>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Section Component Template
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import ReactLenis from "lenis/react";
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import SectionName from "@/components/sections/category/SectionName";
|
||||
|
||||
export default function SectionPreviewPage() {
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="icon-arrow"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="pill"
|
||||
contentWidth="medium"
|
||||
sizing="medium"
|
||||
background="animatedGrid"
|
||||
cardStyle="glass-flat"
|
||||
primaryButtonStyle="gradient"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="medium"
|
||||
>
|
||||
<ReactLenis root>
|
||||
<SectionName
|
||||
title="Preview Section Title"
|
||||
description="This is a preview of the section component with example content."
|
||||
buttons={[
|
||||
{ text: "Get Started", href: "#" },
|
||||
{ text: "Learn More", onClick: () => console.log("Learn more") }
|
||||
]}
|
||||
// Add section-specific props
|
||||
/>
|
||||
</ReactLenis>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### CardStack Section Template
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import ReactLenis from "lenis/react";
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { Package, Zap, Shield, Sparkles } from "lucide-react";
|
||||
import FeatureCardOne from "@/components/sections/feature/FeatureCardOne";
|
||||
|
||||
export default function FeatureCardOnePreviewPage() {
|
||||
const features = [
|
||||
{
|
||||
icon: Package,
|
||||
title: "Feature One",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: "Feature Two",
|
||||
description: "Sed do eiusmod tempor incididunt ut labore et dolore."
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Feature Three",
|
||||
description: "Ut enim ad minim veniam, quis nostrud exercitation."
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: "Feature Four",
|
||||
description: "Duis aute irure dolor in reprehenderit in voluptate."
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="icon-arrow"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="rounded"
|
||||
contentWidth="medium"
|
||||
sizing="medium"
|
||||
background="animatedGrid"
|
||||
cardStyle="glass-flat"
|
||||
primaryButtonStyle="gradient"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="medium"
|
||||
>
|
||||
<ReactLenis root>
|
||||
<FeatureCardOne
|
||||
features={features}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
textboxLayout="default"
|
||||
title="Our Features"
|
||||
description="Discover what makes us unique"
|
||||
tag="Features"
|
||||
tagIcon={Sparkles}
|
||||
buttons={[
|
||||
{ text: "View All", href: "#" }
|
||||
]}
|
||||
/>
|
||||
</ReactLenis>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Critical Requirements
|
||||
|
||||
### Wrapper Order
|
||||
|
||||
**MUST follow this order:**
|
||||
|
||||
```tsx
|
||||
<ThemeProvider>
|
||||
<ReactLenis root>
|
||||
<Component />
|
||||
</ReactLenis>
|
||||
</ThemeProvider>
|
||||
```
|
||||
|
||||
**❌ WRONG:**
|
||||
```tsx
|
||||
<ReactLenis root>
|
||||
<ThemeProvider>
|
||||
<Component />
|
||||
</ThemeProvider>
|
||||
</ReactLenis>
|
||||
```
|
||||
|
||||
ReactLenis must be **inside** ThemeProvider, not outside.
|
||||
|
||||
### "use client" Directive
|
||||
|
||||
All preview pages must include `"use client"` at the top:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
// ...
|
||||
```
|
||||
|
||||
This is required because:
|
||||
- ReactLenis uses client-side hooks
|
||||
- ThemeProvider uses React Context
|
||||
- Components may use interactive features
|
||||
|
||||
### ReactLenis Root Prop
|
||||
|
||||
Always include the `root` prop:
|
||||
|
||||
```tsx
|
||||
<ReactLenis root>
|
||||
{/* components */}
|
||||
</ReactLenis>
|
||||
```
|
||||
|
||||
This enables page-wide smooth scrolling.
|
||||
|
||||
## Theme Configuration
|
||||
|
||||
### Recommended Defaults
|
||||
|
||||
**For most previews:**
|
||||
```tsx
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="icon-arrow"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="rounded"
|
||||
contentWidth="medium"
|
||||
sizing="medium"
|
||||
background="animatedGrid"
|
||||
cardStyle="glass-flat"
|
||||
primaryButtonStyle="gradient"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="medium"
|
||||
>
|
||||
```
|
||||
|
||||
**For button previews:**
|
||||
```tsx
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="text-stagger" // Match button being previewed
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="pill"
|
||||
contentWidth="medium"
|
||||
sizing="medium"
|
||||
background="plain"
|
||||
cardStyle="glass-flat"
|
||||
primaryButtonStyle="gradient"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="medium"
|
||||
>
|
||||
```
|
||||
|
||||
**For hero previews:**
|
||||
```tsx
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="icon-arrow"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="rounded"
|
||||
contentWidth="large" // Wider for heroes
|
||||
sizing="large" // Larger sizing for heroes
|
||||
background="aurora" // Visual background
|
||||
cardStyle="glass-flat"
|
||||
primaryButtonStyle="gradient"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="semibold" // Bolder for heroes
|
||||
>
|
||||
```
|
||||
|
||||
### When to Customize
|
||||
|
||||
Customize theme settings when:
|
||||
- Testing different button variants
|
||||
- Showcasing card styles
|
||||
- Demonstrating responsive behavior
|
||||
- Highlighting specific theme features
|
||||
|
||||
## Realistic Props
|
||||
|
||||
### Use Realistic Content
|
||||
|
||||
**✅ GOOD:**
|
||||
```tsx
|
||||
<HeroBillboard
|
||||
title="Build Amazing Websites Faster"
|
||||
description="Create stunning, responsive websites with our modern component library. Ship faster, iterate quicker."
|
||||
buttons={[
|
||||
{ text: "Get Started", href: "/signup" },
|
||||
{ text: "View Demo", onClick: () => window.open("/demo") }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
**❌ BAD:**
|
||||
```tsx
|
||||
<HeroBillboard
|
||||
title="Test"
|
||||
description="Test description"
|
||||
buttons={[
|
||||
{ text: "Click", href: "#" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Sample Data Patterns
|
||||
|
||||
**Features:**
|
||||
```tsx
|
||||
const features = [
|
||||
{
|
||||
icon: Package,
|
||||
title: "Fast Shipping",
|
||||
description: "Get your order delivered within 2-3 business days."
|
||||
},
|
||||
// ... more features
|
||||
];
|
||||
```
|
||||
|
||||
**Products:**
|
||||
```tsx
|
||||
const products = [
|
||||
{
|
||||
title: "Premium Headphones",
|
||||
description: "Wireless noise-cancelling headphones with 30-hour battery life.",
|
||||
price: "$299",
|
||||
image: "/images/headphones.jpg"
|
||||
},
|
||||
// ... more products
|
||||
];
|
||||
```
|
||||
|
||||
**Testimonials:**
|
||||
```tsx
|
||||
const testimonials = [
|
||||
{
|
||||
name: "Sarah Johnson",
|
||||
role: "CEO, TechCorp",
|
||||
content: "This component library transformed our development workflow. Highly recommend!",
|
||||
image: "/images/avatar-1.jpg",
|
||||
rating: 5
|
||||
},
|
||||
// ... more testimonials
|
||||
];
|
||||
```
|
||||
|
||||
## Multiple Sections Example
|
||||
|
||||
Preview pages can show multiple components together:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import ReactLenis from "lenis/react";
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import HeroBillboard from "@/components/sections/hero/HeroBillboard";
|
||||
import FeatureCardOne from "@/components/sections/feature/FeatureCardOne";
|
||||
import Footer from "@/components/sections/footer/FooterBase";
|
||||
|
||||
export default function FullPagePreview() {
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="icon-arrow"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="rounded"
|
||||
contentWidth="medium"
|
||||
sizing="medium"
|
||||
background="animatedGrid"
|
||||
cardStyle="glass-flat"
|
||||
primaryButtonStyle="gradient"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="medium"
|
||||
>
|
||||
<ReactLenis root>
|
||||
<HeroBillboard {...heroProps} />
|
||||
<FeatureCardOne {...featureProps} />
|
||||
<Footer {...footerProps} />
|
||||
</ReactLenis>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Preview Page Checklist
|
||||
|
||||
### File Setup
|
||||
- [ ] Create in correct location: `/app/components/[category]/[component-name]/page.tsx`
|
||||
- [ ] Use kebab-case for folder names
|
||||
- [ ] Add `"use client"` directive at top
|
||||
- [ ] Export default function with descriptive name
|
||||
|
||||
### Wrapper Configuration
|
||||
- [ ] Wrap in ThemeProvider (outer)
|
||||
- [ ] Wrap in ReactLenis with `root` prop (inner)
|
||||
- [ ] Correct order: ThemeProvider > ReactLenis > Component
|
||||
- [ ] Import both wrappers
|
||||
|
||||
### Component Props
|
||||
- [ ] Use realistic, representative content
|
||||
- [ ] Include all required props
|
||||
- [ ] Test with typical prop combinations
|
||||
- [ ] Use proper TypeScript types (no `any`)
|
||||
|
||||
### Theme Settings
|
||||
- [ ] Configure appropriate theme settings for component type
|
||||
- [ ] Use sensible defaults that showcase the component well
|
||||
- [ ] Test with different theme configurations if needed
|
||||
|
||||
### Quality Checks
|
||||
- [ ] Component renders without errors
|
||||
- [ ] Smooth scrolling works
|
||||
- [ ] Responsive design functions correctly
|
||||
- [ ] Animations trigger properly
|
||||
- [ ] No console warnings or errors
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Background for Preview
|
||||
|
||||
If the component needs a background color to be visible:
|
||||
|
||||
```tsx
|
||||
<ReactLenis root>
|
||||
<div className="min-h-screen bg-background">
|
||||
<Component {...props} />
|
||||
</div>
|
||||
</ReactLenis>
|
||||
```
|
||||
|
||||
### Centered Preview
|
||||
|
||||
For small components that need centering:
|
||||
|
||||
```tsx
|
||||
<ReactLenis root>
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<Component {...props} />
|
||||
</div>
|
||||
</ReactLenis>
|
||||
```
|
||||
|
||||
### Multiple Variants
|
||||
|
||||
Show multiple variants of the same component:
|
||||
|
||||
```tsx
|
||||
<ReactLenis root>
|
||||
<div className="min-h-screen bg-background py-20 space-y-20">
|
||||
<ComponentName variant="primary" {...props} />
|
||||
<ComponentName variant="secondary" {...props} />
|
||||
<ComponentName variant="ghost" {...props} />
|
||||
</div>
|
||||
</ReactLenis>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO:
|
||||
|
||||
- Use realistic, production-quality content
|
||||
- Test responsive behavior
|
||||
- Include all ThemeProvider configuration
|
||||
- Use ReactLenis for smooth scrolling
|
||||
- Follow naming conventions (kebab-case folders)
|
||||
- Include required props only (let optional props use defaults)
|
||||
- Test with different theme settings
|
||||
|
||||
### ❌ DO NOT:
|
||||
|
||||
- Use placeholder text like "Lorem ipsum" (use realistic content)
|
||||
- Skip ThemeProvider or ReactLenis
|
||||
- Put ReactLenis outside ThemeProvider
|
||||
- Use hardcoded colors that break theming
|
||||
- Create overly complex multi-component demos (keep focused)
|
||||
- Forget "use client" directive
|
||||
- Use incorrect folder structure
|
||||
145
docs/README.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Component Library Documentation
|
||||
|
||||
This directory contains focused documentation for building components in this AI-optimized component library.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
### 📋 [COMPONENT_IMPLEMENTATION.md](./COMPONENT_IMPLEMENTATION.md)
|
||||
**Core component implementation patterns**
|
||||
|
||||
Load when: Creating or modifying component code
|
||||
|
||||
Covers:
|
||||
- Component structure template
|
||||
- Prop structure & defaults
|
||||
- Naming conventions
|
||||
- Component customizability
|
||||
- Type safety
|
||||
- Responsive design
|
||||
- Content width pattern
|
||||
- Implementation checklist
|
||||
|
||||
---
|
||||
|
||||
### 🎴 [CARDSTACK_SECTIONS.md](./CARDSTACK_SECTIONS.md)
|
||||
**CardStack-based section components**
|
||||
|
||||
Load when: Creating Feature, Product, Pricing, Testimonial, Team, Blog, or Metrics sections
|
||||
|
||||
Covers:
|
||||
- CardStack pattern overview
|
||||
- Grid variants (10+ layouts)
|
||||
- Height control (`uniformGridCustomHeightClasses`)
|
||||
- Carousel modes
|
||||
- TextBox integration
|
||||
- Button system
|
||||
- Animation types
|
||||
- Complete examples & checklist
|
||||
|
||||
---
|
||||
|
||||
### 🎨 [THEME_AND_STYLING.md](./THEME_AND_STYLING.md)
|
||||
**Theme system and styling patterns**
|
||||
|
||||
Load when: Setting up themes, working with colors, or styling components
|
||||
|
||||
Covers:
|
||||
- Theme Provider configuration
|
||||
- Color theming (CSS variables)
|
||||
- Inverted background pattern
|
||||
- Content width pattern
|
||||
- Card styling
|
||||
- Border radius patterns
|
||||
- Button styling classes
|
||||
- Styling checklist
|
||||
|
||||
---
|
||||
|
||||
### ♿ [ACCESSIBILITY.md](./ACCESSIBILITY.md)
|
||||
**Accessibility (a11y) standards**
|
||||
|
||||
Load when: Adding interactive elements, media, or sections
|
||||
|
||||
Covers:
|
||||
- Interactive components (buttons, links)
|
||||
- Media components (images, videos)
|
||||
- Section components (semantic HTML)
|
||||
- Form components
|
||||
- Focus management
|
||||
- Keyboard navigation
|
||||
- ARIA roles
|
||||
- Accessibility checklist
|
||||
|
||||
---
|
||||
|
||||
### 📚 [REGISTRY_STANDARDS.md](./REGISTRY_STANDARDS.md)
|
||||
**Registry documentation rules**
|
||||
|
||||
Load when: Adding or updating component entries in `registry.json`
|
||||
|
||||
Covers:
|
||||
- Registry structure
|
||||
- Component entry format
|
||||
- Field descriptions
|
||||
- propsSchema rules
|
||||
- Constraints format
|
||||
- Usage examples
|
||||
- What to include/exclude
|
||||
- Validation checklist
|
||||
|
||||
---
|
||||
|
||||
### 📄 [PREVIEW_PAGE_STANDARDS.md](./PREVIEW_PAGE_STANDARDS.md)
|
||||
**Preview page setup and patterns**
|
||||
|
||||
Load when: Creating preview pages in `/app/components/`
|
||||
|
||||
Covers:
|
||||
- File structure & location
|
||||
- Preview page templates
|
||||
- Wrapper order (ThemeProvider > ReactLenis)
|
||||
- Theme configuration
|
||||
- Realistic props
|
||||
- Multiple sections example
|
||||
- Preview page checklist
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### When creating a new component:
|
||||
1. Load `COMPONENT_IMPLEMENTATION.md` for structure
|
||||
2. Load `ACCESSIBILITY.md` for a11y requirements
|
||||
3. Load `THEME_AND_STYLING.md` for styling patterns
|
||||
4. Load `CARDSTACK_SECTIONS.md` if using CardStack
|
||||
|
||||
### When updating the registry:
|
||||
1. Load `REGISTRY_STANDARDS.md` only
|
||||
|
||||
### When creating a preview page:
|
||||
1. Load `PREVIEW_PAGE_STANDARDS.md` only
|
||||
|
||||
### When modifying themes:
|
||||
1. Load `THEME_AND_STYLING.md` only
|
||||
|
||||
---
|
||||
|
||||
## Documentation Principles
|
||||
|
||||
These docs are optimized for:
|
||||
- **AI builders** (Lovable, V0, Claude Code)
|
||||
- **Focused context** (load only what you need)
|
||||
- **Quick reference** (checklists and examples)
|
||||
- **Consistency** (standardized patterns across all components)
|
||||
|
||||
Each file is:
|
||||
- **Single-purpose** - covers one concern thoroughly
|
||||
- **Self-contained** - minimal cross-references
|
||||
- **Example-driven** - shows good/bad patterns
|
||||
- **Checklist-equipped** - actionable validation steps
|
||||
|
||||
---
|
||||
|
||||
## Legacy Documentation
|
||||
|
||||
The original `COMPONENT_STANDARDS.md` file has been split into these focused documents. Refer to these new files for all component development.
|
||||
487
docs/REGISTRY_STANDARDS.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# Registry Documentation Standards
|
||||
|
||||
This document outlines how to document components in `registry.json` for AI website builders.
|
||||
|
||||
## Registry Structure
|
||||
|
||||
The registry is organized into two main sections:
|
||||
|
||||
```json
|
||||
{
|
||||
"componentRegistry": {
|
||||
"button": [...],
|
||||
"text": [...],
|
||||
"navbar": [...],
|
||||
"layout": [...]
|
||||
},
|
||||
"sectionRegistry": {
|
||||
"hero": [...],
|
||||
"about": [...],
|
||||
"feature": [...],
|
||||
"testimonial": [...],
|
||||
"pricing": [...],
|
||||
"team": [...],
|
||||
"product": [...],
|
||||
"metrics": [...],
|
||||
"blog": [...],
|
||||
"footer": [...],
|
||||
"contact": [...],
|
||||
"faq": [...],
|
||||
"socialProof": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**componentRegistry** - Base/utility components (buttons, text, navbar, CardStack, etc.)
|
||||
**sectionRegistry** - Section components (hero, about, feature, etc.)
|
||||
|
||||
## Component Entry Format
|
||||
|
||||
Every component entry must follow this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"import": "import ComponentName from '@/components/category/ComponentName';",
|
||||
"name": "ComponentName",
|
||||
"path": "@/components/category/ComponentName",
|
||||
"description": "Brief one-line description of what the component is.",
|
||||
"details": "Longer description of when to use it, behavior, and constraints.",
|
||||
"constraints": {
|
||||
"textRules": {
|
||||
"text": {
|
||||
"required": true,
|
||||
"example": "Example text",
|
||||
"minChars": 2,
|
||||
"maxChars": 15
|
||||
}
|
||||
}
|
||||
},
|
||||
"propsSchema": {
|
||||
"text": "string",
|
||||
"onClick?": "() => void",
|
||||
"className?": "string",
|
||||
"disabled?": "boolean (default: false)",
|
||||
"ariaLabel?": "string",
|
||||
"type?": "'button' | 'submit' | 'reset' (default: 'button')"
|
||||
},
|
||||
"usage": "<ComponentName text=\"Example\" onClick={() => console.log('clicked')} />"
|
||||
}
|
||||
```
|
||||
|
||||
## Field Descriptions
|
||||
|
||||
### import
|
||||
The exact import statement AI should use.
|
||||
|
||||
**Format:**
|
||||
```json
|
||||
"import": "import ComponentName from '@/components/category/ComponentName';"
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```json
|
||||
"import": "import HeroBillboard from '@/components/sections/hero/HeroBillboard';"
|
||||
"import": "import ButtonTextStagger from '@/components/button/text-stagger/ButtonTextStagger';"
|
||||
"import": "import CardStack from '@/components/cardStack/CardStack';"
|
||||
```
|
||||
|
||||
### name
|
||||
Component name exactly as exported.
|
||||
|
||||
**Format:**
|
||||
```json
|
||||
"name": "ComponentName"
|
||||
```
|
||||
|
||||
### path
|
||||
File path without extension.
|
||||
|
||||
**Format:**
|
||||
```json
|
||||
"path": "@/components/category/ComponentName"
|
||||
```
|
||||
|
||||
### description
|
||||
One-line summary of what the component is. Focus on visual/behavioral characteristics.
|
||||
|
||||
**Format:** 1 sentence, under 100 characters
|
||||
|
||||
**Good Examples:**
|
||||
```json
|
||||
"description": "CTA button with character stagger animation on hover."
|
||||
"description": "Full-width hero section with centered text and billboard layout."
|
||||
"description": "Feature section with grid or carousel layout for feature cards."
|
||||
```
|
||||
|
||||
**Bad Examples:**
|
||||
```json
|
||||
"description": "A really cool button component with lots of features and animations that can be used anywhere." // Too verbose
|
||||
"description": "Button" // Too vague
|
||||
```
|
||||
|
||||
### details
|
||||
Longer description covering:
|
||||
- When to use it
|
||||
- Key behavior notes
|
||||
- Important constraints
|
||||
|
||||
**Format:** 2-4 sentences
|
||||
|
||||
**Good Example:**
|
||||
```json
|
||||
"details": "Use for primary or secondary CTAs where subtle text motion adds emphasis. On hover, the label's characters animate in sequence (stagger). Includes background styling and supports all standard button props."
|
||||
```
|
||||
|
||||
**Bad Example:**
|
||||
```json
|
||||
"details": "This is a button that you can click and it will do something when you click it. It has animations." // Too obvious, not helpful
|
||||
```
|
||||
|
||||
### constraints
|
||||
|
||||
Defines text length constraints for string props.
|
||||
|
||||
**Format:**
|
||||
```json
|
||||
"constraints": {
|
||||
"textRules": {
|
||||
"propName": {
|
||||
"required": true,
|
||||
"example": "Example value",
|
||||
"minChars": 2,
|
||||
"maxChars": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Common Patterns:**
|
||||
|
||||
**Button text:**
|
||||
```json
|
||||
"constraints": {
|
||||
"textRules": {
|
||||
"text": {
|
||||
"required": true,
|
||||
"example": "Get Started",
|
||||
"minChars": 2,
|
||||
"maxChars": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Section titles and descriptions:**
|
||||
```json
|
||||
"constraints": {
|
||||
"textRules": {
|
||||
"title": {
|
||||
"required": true,
|
||||
"example": "Welcome to Our Platform",
|
||||
"minChars": 5,
|
||||
"maxChars": 60
|
||||
},
|
||||
"description": {
|
||||
"required": true,
|
||||
"example": "Build amazing websites with our component library",
|
||||
"minChars": 10,
|
||||
"maxChars": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### propsSchema
|
||||
|
||||
Documents all component props with types and defaults.
|
||||
|
||||
**Format Rules:**
|
||||
|
||||
**Required props:**
|
||||
```json
|
||||
"propName": "type"
|
||||
```
|
||||
|
||||
**Optional props:**
|
||||
```json
|
||||
"propName?": "type"
|
||||
```
|
||||
|
||||
**Optional props with defaults:**
|
||||
```json
|
||||
"propName?": "type (default: value)"
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
**Simple types:**
|
||||
```json
|
||||
"text": "string",
|
||||
"count": "number",
|
||||
"enabled": "boolean"
|
||||
```
|
||||
|
||||
**Functions:**
|
||||
```json
|
||||
"onClick?": "() => void",
|
||||
"onChange?": "(value: string) => void",
|
||||
"onSubmit?": "(data: FormData) => Promise<void>"
|
||||
```
|
||||
|
||||
**Union types:**
|
||||
```json
|
||||
"type?": "'button' | 'submit' | 'reset' (default: 'button')",
|
||||
"variant?": "'primary' | 'secondary' | 'ghost'",
|
||||
"size?": "'sm' | 'md' | 'lg' (default: 'md')"
|
||||
```
|
||||
|
||||
**Array types:**
|
||||
```json
|
||||
"items": "Array<{ title: string; description: string }>",
|
||||
"buttons?": "ButtonConfig[]",
|
||||
"features": "Feature[]"
|
||||
```
|
||||
|
||||
**Complex types:**
|
||||
```json
|
||||
"gridVariant": "'uniform-all-items-equal' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | ...",
|
||||
"icon?": "LucideIcon",
|
||||
"image?": "string (URL or path)"
|
||||
```
|
||||
|
||||
**With defaults:**
|
||||
```json
|
||||
"className?": "string",
|
||||
"disabled?": "boolean (default: false)",
|
||||
"carouselMode?": "'auto' | 'buttons' (default: 'buttons')",
|
||||
"uniformGridCustomHeightClasses?": "string (default: 'min-h-80 2xl:min-h-90')"
|
||||
```
|
||||
|
||||
### usage
|
||||
|
||||
Single-line example showing typical implementation.
|
||||
|
||||
**Format:** One line, realistic props, valid JSX
|
||||
|
||||
**Good Examples:**
|
||||
|
||||
**Button:**
|
||||
```json
|
||||
"usage": "<ButtonTextStagger text=\"Get Started\" onClick={() => console.log('clicked')} />"
|
||||
```
|
||||
|
||||
**Section with minimal props:**
|
||||
```json
|
||||
"usage": "<HeroBillboard title=\"Welcome\" description=\"Start building today\" buttons={[{ text: 'Get Started', href: '/signup' }]} />"
|
||||
```
|
||||
|
||||
**CardStack section:**
|
||||
```json
|
||||
"usage": "<FeatureCardOne features={featuresData} gridVariant=\"uniform-all-items-equal\" textboxLayout=\"default\" title=\"Features\" description=\"Our key features\" />"
|
||||
```
|
||||
|
||||
**Bad Examples:**
|
||||
```json
|
||||
"usage": "<Component />" // Missing required props
|
||||
"usage": "<Component\n prop1=\"value\"\n prop2=\"value\"\n/>" // Multi-line, hard to read
|
||||
"usage": "Component({ text: 'Hi' })" // Not JSX
|
||||
```
|
||||
|
||||
## What to Include
|
||||
|
||||
### ✅ DO Include:
|
||||
|
||||
**Default values in propsSchema** - Critical for AI to generate correct code
|
||||
```json
|
||||
"disabled?": "boolean (default: false)",
|
||||
"type?": "'button' | 'submit' | 'reset' (default: 'button')"
|
||||
```
|
||||
|
||||
**Usage examples** - Helps AI understand context
|
||||
```json
|
||||
"usage": "<ButtonTextStagger text=\"Click me\" onClick={() => alert('Hi')} />"
|
||||
```
|
||||
|
||||
**Text constraints** - Min/max character limits
|
||||
```json
|
||||
"constraints": {
|
||||
"textRules": {
|
||||
"text": {
|
||||
"required": true,
|
||||
"example": "Get Started",
|
||||
"minChars": 2,
|
||||
"maxChars": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Accurate descriptions** - Ensure description matches actual behavior
|
||||
```json
|
||||
"description": "CTA button with character stagger animation on hover.",
|
||||
"details": "Use for primary or secondary CTAs where subtle text motion adds emphasis."
|
||||
```
|
||||
|
||||
**Use case guidance** - When to use this vs alternatives
|
||||
```json
|
||||
"details": "Use for hero sections with centered content and billboard layout. Best for landing pages and marketing sites."
|
||||
```
|
||||
|
||||
## What NOT to Include
|
||||
|
||||
### ❌ DO NOT Include:
|
||||
|
||||
**Metadata field** - Unnecessary complexity
|
||||
```json
|
||||
// Don't do this:
|
||||
"metadata": {
|
||||
"category": "button",
|
||||
"version": "1.0.0",
|
||||
"author": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Verbose descriptions** - Keep concise and obvious
|
||||
```json
|
||||
// Don't do this:
|
||||
"description": "This is an amazing button component that you can use to create buttons with stagger animations that look really cool and modern and will make your website stand out from the competition."
|
||||
```
|
||||
|
||||
**Dependencies** - AI builders can infer from imports
|
||||
```json
|
||||
// Don't do this:
|
||||
"dependencies": ["lucide-react", "framer-motion"]
|
||||
```
|
||||
|
||||
**Over-documentation** - If it's obvious from the name, skip it
|
||||
```json
|
||||
// Don't do this:
|
||||
"details": "This is a button. You can click it. It accepts text to display on the button."
|
||||
```
|
||||
|
||||
**Implementation details** - Focus on usage, not internals
|
||||
```json
|
||||
// Don't do this:
|
||||
"details": "Uses GSAP ScrollTrigger with stagger: 0.1 and uses React.memo for performance and has displayName set to ButtonTextStagger."
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Button Component
|
||||
|
||||
```json
|
||||
{
|
||||
"import": "import ButtonTextStagger from '@/components/button/text-stagger/ButtonTextStagger';",
|
||||
"name": "ButtonTextStagger",
|
||||
"path": "@/components/button/text-stagger/ButtonTextStagger",
|
||||
"description": "CTA button with character stagger animation on hover.",
|
||||
"details": "Use for primary or secondary CTAs where subtle text motion adds emphasis. On hover, the label's characters animate in sequence (stagger). Includes background styling.",
|
||||
"constraints": {
|
||||
"textRules": {
|
||||
"text": {
|
||||
"required": true,
|
||||
"example": "Get Started",
|
||||
"minChars": 2,
|
||||
"maxChars": 15
|
||||
}
|
||||
}
|
||||
},
|
||||
"propsSchema": {
|
||||
"text": "string",
|
||||
"onClick?": "() => void",
|
||||
"href?": "string",
|
||||
"className?": "string",
|
||||
"textClassName?": "string",
|
||||
"disabled?": "boolean (default: false)",
|
||||
"ariaLabel?": "string",
|
||||
"type?": "'button' | 'submit' | 'reset' (default: 'button')"
|
||||
},
|
||||
"usage": "<ButtonTextStagger text=\"Get Started\" onClick={() => console.log('clicked')} />"
|
||||
}
|
||||
```
|
||||
|
||||
### Section Component (CardStack-based)
|
||||
|
||||
```json
|
||||
{
|
||||
"import": "import FeatureCardOne from '@/components/sections/feature/FeatureCardOne';",
|
||||
"name": "FeatureCardOne",
|
||||
"path": "@/components/sections/feature/FeatureCardOne",
|
||||
"description": "Feature section with grid or carousel layout for feature cards.",
|
||||
"details": "Displays feature cards in a responsive grid (1-4 items) or carousel (5+ items). Supports TextBox header with multiple layout options. Each card includes icon, title, and description.",
|
||||
"constraints": {
|
||||
"textRules": {
|
||||
"title": {
|
||||
"required": true,
|
||||
"example": "Our Features",
|
||||
"minChars": 5,
|
||||
"maxChars": 60
|
||||
},
|
||||
"description": {
|
||||
"required": true,
|
||||
"example": "Discover what makes us unique",
|
||||
"minChars": 10,
|
||||
"maxChars": 200
|
||||
}
|
||||
}
|
||||
},
|
||||
"propsSchema": {
|
||||
"features": "Array<{ icon: LucideIcon; title: string; description: string }>",
|
||||
"gridVariant": "'uniform-all-items-equal' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | ...",
|
||||
"title": "string",
|
||||
"description": "string",
|
||||
"tag?": "string",
|
||||
"tagIcon?": "LucideIcon",
|
||||
"buttons?": "ButtonConfig[]",
|
||||
"textboxLayout": "'default' | 'split' | 'split-actions' | 'split-description'",
|
||||
"carouselMode?": "'auto' | 'buttons' (default: 'buttons')",
|
||||
"uniformGridCustomHeightClasses?": "string (default: 'min-h-80 2xl:min-h-90')",
|
||||
"ariaLabel?": "string",
|
||||
"className?": "string"
|
||||
},
|
||||
"usage": "<FeatureCardOne features={featuresData} gridVariant=\"uniform-all-items-equal\" textboxLayout=\"default\" title=\"Features\" description=\"Our key features\" />"
|
||||
}
|
||||
```
|
||||
|
||||
## Registry Validation Checklist
|
||||
|
||||
When adding a new component to the registry:
|
||||
|
||||
### Required Fields
|
||||
- [ ] `import` - Exact import statement
|
||||
- [ ] `name` - Component name
|
||||
- [ ] `path` - File path without extension
|
||||
- [ ] `description` - One-line summary
|
||||
- [ ] `details` - When to use, behavior, constraints
|
||||
- [ ] `propsSchema` - All props documented
|
||||
- [ ] `usage` - Single-line example
|
||||
|
||||
### Constraints (if applicable)
|
||||
- [ ] Add `textRules` for text props
|
||||
- [ ] Set `minChars` and `maxChars`
|
||||
- [ ] Provide realistic `example` values
|
||||
- [ ] Mark `required: true` for required props
|
||||
|
||||
### propsSchema Format
|
||||
- [ ] Required props: `"prop": "type"`
|
||||
- [ ] Optional props: `"prop?": "type"`
|
||||
- [ ] Defaults: `"prop?": "type (default: value)"`
|
||||
- [ ] All types match component implementation
|
||||
- [ ] Union types use single quotes inside double quotes
|
||||
|
||||
### Quality Checks
|
||||
- [ ] Description is concise (under 100 chars)
|
||||
- [ ] Details provide use case guidance
|
||||
- [ ] Usage example is valid JSX
|
||||
- [ ] Usage example shows realistic props
|
||||
- [ ] Default values documented in propsSchema
|
||||
- [ ] No over-documentation or verbose descriptions
|
||||
- [ ] No unnecessary metadata or dependencies
|
||||
|
||||
### Consistency Checks
|
||||
- [ ] Component name matches file name
|
||||
- [ ] Path matches actual file location
|
||||
- [ ] Import statement is correct
|
||||
- [ ] Props match actual component interface
|
||||
- [ ] Defaults in registry match component defaults
|
||||
- [ ] Naming follows conventions (title/description for sections, text for buttons)
|
||||
521
docs/THEME_AND_STYLING.md
Normal file
@@ -0,0 +1,521 @@
|
||||
# Theme and Styling Standards
|
||||
|
||||
This document covers the centralized theme system, color theming, and styling patterns used throughout the component library.
|
||||
|
||||
## Theme Provider System
|
||||
|
||||
All sections and components use a centralized ThemeProvider to maintain consistent styling across the entire site.
|
||||
|
||||
### Location & Setup
|
||||
|
||||
**Import:**
|
||||
```tsx
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
```
|
||||
|
||||
**Usage:** Wrap the entire app/page (not individual sections) in a **single** ThemeProvider:
|
||||
|
||||
```tsx
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="text-stagger"
|
||||
defaultTextAnimation="entrance-slide"
|
||||
borderRadius="rounded"
|
||||
contentWidth="medium"
|
||||
sizing="medium"
|
||||
background="animatedGrid"
|
||||
cardStyle="glass-flat"
|
||||
primaryButtonStyle="gradient"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="medium"
|
||||
>
|
||||
<HeroBillboard {...props} />
|
||||
<FeatureSection {...props} />
|
||||
<Footer {...props} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Configuration Options
|
||||
|
||||
#### defaultButtonVariant
|
||||
Controls the button style for ALL buttons in sections.
|
||||
|
||||
**Options:**
|
||||
- `"text-stagger"` - Character stagger animation on hover (default)
|
||||
- `"shift-hover"` - Text shifts on hover
|
||||
- `"icon-arrow"` - Text + right arrow icon
|
||||
- `"hover-magnetic"` - Cursor-tracking magnetic effect
|
||||
- `"hover-bubble"` - Expanding circle on hover
|
||||
- `"expand-hover"` - Width expansion on hover
|
||||
|
||||
#### defaultTextAnimation
|
||||
Controls the text animation type for ALL text in sections.
|
||||
|
||||
**Options:**
|
||||
- `"entrance-slide"` - Slide up from below (default)
|
||||
- `"reveal-blur"` - Blur to clear reveal
|
||||
- `"background-highlight"` - Background highlight effect
|
||||
|
||||
#### borderRadius
|
||||
Controls border radius for buttons and cards.
|
||||
|
||||
**Options:**
|
||||
- `"rounded"` - Standard rounded corners
|
||||
- `"pill"` - Fully rounded (pill shape)
|
||||
- `"sharp"` - No border radius
|
||||
|
||||
#### contentWidth
|
||||
Controls the max width of section content.
|
||||
|
||||
**Options:**
|
||||
- `"small"` - Narrow content width
|
||||
- `"medium"` - Standard content width (default)
|
||||
- `"large"` - Wide content width
|
||||
|
||||
Maps to `w-content-width` CSS class.
|
||||
|
||||
#### sizing
|
||||
Controls spacing and size scale throughout components.
|
||||
|
||||
**Options:**
|
||||
- `"small"` - Compact spacing
|
||||
- `"medium"` - Standard spacing (default)
|
||||
- `"large"` - Generous spacing
|
||||
|
||||
#### background
|
||||
Default background pattern for the page.
|
||||
|
||||
**Options:**
|
||||
- `"plain"` - Solid background color
|
||||
- `"animatedGrid"` - Animated grid pattern
|
||||
- `"aurora"` - Aurora gradient effect
|
||||
- `"dotGrid"` - Dot grid pattern
|
||||
- And more...
|
||||
|
||||
#### cardStyle
|
||||
Visual style for all card components.
|
||||
|
||||
**Options:**
|
||||
- `"glass-flat"` - Flat glass effect (default)
|
||||
- `"glass-depth"` - Glass with depth/shadow
|
||||
- `"glass-outline"` - Outlined glass
|
||||
- `"solid-accent-light"` - Solid light accent
|
||||
- `"outline"` - Simple outline
|
||||
- `"elevated"` - Elevated shadow effect
|
||||
- `"frosted"` - Frosted glass
|
||||
- And more...
|
||||
|
||||
#### primaryButtonStyle
|
||||
Style for primary buttons (first button in array).
|
||||
|
||||
**Options:**
|
||||
- `"gradient"` - Gradient background
|
||||
- `"solid"` - Solid color background
|
||||
- `"glass"` - Glass effect
|
||||
- `"outline"` - Outlined button
|
||||
|
||||
#### secondaryButtonStyle
|
||||
Style for secondary buttons (second+ button in array).
|
||||
|
||||
**Options:**
|
||||
- `"glass"` - Glass effect (default)
|
||||
- `"outline"` - Outlined button
|
||||
- `"solid"` - Solid color background
|
||||
- `"gradient"` - Gradient background
|
||||
|
||||
#### headingFontWeight
|
||||
Font weight for all headings.
|
||||
|
||||
**Options:**
|
||||
- `"normal"` - Regular weight
|
||||
- `"medium"` - Medium weight (default)
|
||||
- `"semibold"` - Semi-bold weight
|
||||
- `"bold"` - Bold weight
|
||||
|
||||
### Using Theme in Components
|
||||
|
||||
#### useTheme Hook
|
||||
|
||||
```tsx
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
|
||||
const Component = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
// Access theme properties:
|
||||
// - theme.defaultButtonVariant
|
||||
// - theme.defaultTextAnimation
|
||||
// - theme.borderRadius
|
||||
// - theme.cardStyle
|
||||
// - theme.contentWidth
|
||||
// - theme.sizing
|
||||
// - theme.background
|
||||
// - theme.primaryButtonStyle
|
||||
// - theme.secondaryButtonStyle
|
||||
// - theme.headingFontWeight
|
||||
|
||||
return <div>{/* component */}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
#### TextBox Component Example
|
||||
|
||||
TextBox automatically applies theme defaults:
|
||||
|
||||
```tsx
|
||||
const TextBox = ({ type, buttons, ...props }: TextBoxProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// Use default text animation from theme if not specified
|
||||
const animationType = type || theme.defaultTextAnimation;
|
||||
|
||||
// Button variant comes from theme
|
||||
const variant = theme.defaultButtonVariant;
|
||||
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### Important Rules
|
||||
|
||||
❌ **DO NOT:**
|
||||
- Specify button `variant` in section button configs (controlled by ThemeProvider)
|
||||
- Wrap individual sections in ThemeProvider (use ONE provider per site/page)
|
||||
- Override theme defaults unless explicitly required by component design
|
||||
|
||||
✅ **DO:**
|
||||
- Wrap the entire app/page in a single ThemeProvider
|
||||
- Let all sections inherit theme defaults automatically
|
||||
- Use `useTheme()` hook to access theme configuration
|
||||
- Document when components don't follow theme defaults (with clear reason)
|
||||
|
||||
## Color & Theming
|
||||
|
||||
### CSS Custom Properties
|
||||
|
||||
**Always use CSS custom properties for colors** to ensure theme consistency:
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - Uses theme variables
|
||||
<div className="bg-background text-foreground">
|
||||
<button className="bg-foreground text-background">Click me</button>
|
||||
</div>
|
||||
|
||||
// ❌ WRONG - Hardcoded colors break theming
|
||||
<div className="bg-white text-black">
|
||||
<button className="bg-black text-white">Click me</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Standard Color Variables
|
||||
|
||||
**Background & Text:**
|
||||
- `bg-background` - Main background color
|
||||
- `text-foreground` - Main text color
|
||||
- `bg-foreground` - Inverse background (for buttons, accents)
|
||||
- `text-background` - Inverse text (for text on dark backgrounds)
|
||||
|
||||
**Cards & Surfaces:**
|
||||
- `card` - Card/surface background with border (theme-aware)
|
||||
- Maps to different styles based on `theme.cardStyle`
|
||||
|
||||
**Buttons:**
|
||||
- `primary-button` - Primary button styling (index 0)
|
||||
- `secondary-button` - Secondary button styling (index 1+)
|
||||
|
||||
**Opacity Modifiers:**
|
||||
```tsx
|
||||
text-foreground/75 // 75% opacity
|
||||
text-background/50 // 50% opacity
|
||||
bg-foreground/10 // 10% opacity
|
||||
```
|
||||
|
||||
### When to Use Theme Colors
|
||||
|
||||
✅ **Always prefer theme variables:**
|
||||
- Backgrounds, text, borders, shadows
|
||||
- Ensures proper dark mode support
|
||||
- Allows theme customization
|
||||
- Maintains visual consistency
|
||||
|
||||
❌ **Only use hardcoded colors:**
|
||||
- Very specific one-off cases with clear justification
|
||||
- Decorative elements that shouldn't change with theme
|
||||
- Must be documented in component comments
|
||||
|
||||
## Inverted Background Pattern
|
||||
|
||||
Section components support three modes for background styling, allowing flexible visual contrast and card-style layouts.
|
||||
|
||||
### Three Background Modes
|
||||
|
||||
**`"noInvert"`** - Standard background (default page background color)
|
||||
- No background color applied to section
|
||||
- Text uses standard `text-foreground` color
|
||||
- Full-width section
|
||||
|
||||
**`"invertDefault"`** - Full-width inverted background
|
||||
- Section gets `bg-foreground` background
|
||||
- Text uses `text-background` color for contrast
|
||||
- Full-width section with inverted colors
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```tsx
|
||||
import { cls } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
description: string;
|
||||
useInvertedBackground: "noInvert" | "invertDefault"; // Required
|
||||
// ... other props
|
||||
}
|
||||
|
||||
const Section = ({
|
||||
title,
|
||||
description,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
// ... other props
|
||||
}: SectionProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground === "invertDefault" && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="w-content-width mx-auto">
|
||||
<h1 className={cls(
|
||||
"text-6xl font-medium",
|
||||
useInvertedBackground === "invertDefault" && "text-background",
|
||||
titleClassName
|
||||
)}>
|
||||
{title}
|
||||
</h1>
|
||||
<p className={cls(
|
||||
"text-lg",
|
||||
useInvertedBackground === "invertDefault"
|
||||
? "text-background/75"
|
||||
: "text-foreground/75",
|
||||
descriptionClassName
|
||||
)}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Key Points
|
||||
|
||||
1. **Required Prop**: `useInvertedBackground` should be a required string union type (no `?`), forcing explicit choice
|
||||
2. **Two Modes**: `"noInvert"`, `"invertDefault"`
|
||||
3. **Section Width**: Always `w-full`
|
||||
4. **Background Color**:
|
||||
- `"invertDefault"` → `bg-foreground`
|
||||
- `"noInvert"` → no background
|
||||
5. **Text Colors**: Apply `text-background` or `text-background/75` for `"invertDefault"` mode
|
||||
6. **Relative Positioning**: Section needs `relative` class for proper z-index stacking
|
||||
7. **Conditional Logic**: Use explicit string equality checks (not truthy/falsy)
|
||||
|
||||
### Section className Pattern
|
||||
|
||||
**Standard pattern for all sections:**
|
||||
|
||||
```tsx
|
||||
<section
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground === "invertDefault" && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
```
|
||||
|
||||
### Text Color Pattern
|
||||
|
||||
**For text elements:**
|
||||
|
||||
```tsx
|
||||
// Single check for invert mode:
|
||||
useInvertedBackground === "invertDefault" && "text-background"
|
||||
|
||||
// Ternary for opacity variants:
|
||||
useInvertedBackground === "invertDefault"
|
||||
? "text-background/75"
|
||||
: "text-foreground/75"
|
||||
```
|
||||
|
||||
### For Components with Cards (Advanced)
|
||||
|
||||
When a section contains cards with light backgrounds, use the `shouldUseInvertedText` utility:
|
||||
|
||||
```tsx
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
|
||||
const SectionWithCards = ({
|
||||
useInvertedBackground,
|
||||
// ... other props
|
||||
}: SectionProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(
|
||||
useInvertedBackground,
|
||||
theme.cardStyle
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative py-20 w-full",
|
||||
useInvertedBackground === "invertDefault" && "bg-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* For elements inside cards or with card backgrounds */}
|
||||
<h3 className={cls(
|
||||
"text-xl",
|
||||
shouldUseLightText && "text-background",
|
||||
bulletTitleClassName
|
||||
)}>
|
||||
{point.title}
|
||||
</h3>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
The `shouldUseInvertedText` utility checks if:
|
||||
- `useInvertedBackground` is `"invertDefault"` AND
|
||||
- Current `cardStyle` is a "light" style (e.g., `glass-elevated`, `outline`, etc.)
|
||||
|
||||
This ensures text inside cards remains readable regardless of theme configuration.
|
||||
|
||||
### Width Classes Explained
|
||||
|
||||
**`w-content-width`** - Content container width
|
||||
- Controlled by `theme.contentWidth` (small/medium/large)
|
||||
- Used for inner content wrapper in all sections
|
||||
|
||||
## Content Width Pattern
|
||||
|
||||
All section content must use the `w-content-width` class:
|
||||
|
||||
```tsx
|
||||
<section className="w-full py-20">
|
||||
<div className="w-content-width mx-auto">
|
||||
{/* content */}
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Controlled by `theme.contentWidth` (small/medium/large)
|
||||
- Automatically adjusts max-width based on theme setting
|
||||
- Centers content with `mx-auto`
|
||||
|
||||
## Section Spacing
|
||||
|
||||
**Standard sections:**
|
||||
```tsx
|
||||
className="w-full py-20"
|
||||
```
|
||||
|
||||
Vertical padding of `py-20` (5rem = 80px) on top and bottom.
|
||||
|
||||
**Exceptions (NO py-20):**
|
||||
- Hero sections (custom spacing based on design)
|
||||
- Footer sections (custom spacing)
|
||||
- Full-bleed sections with background colors/images
|
||||
|
||||
## Card Styling Pattern
|
||||
|
||||
Use the `card` class for all card components:
|
||||
|
||||
```tsx
|
||||
<div className={cls("card p-6 rounded-theme-capped h-full min-h-0", cardClassName)}>
|
||||
{/* card content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Classes explained:**
|
||||
- `card` - Theme-aware background/border (maps to `theme.cardStyle`)
|
||||
- `p-6` - Standard padding (1.5rem = 24px)
|
||||
- `rounded-theme-capped` - Border radius from theme (respects `theme.borderRadius`)
|
||||
- `h-full` - Fill parent height (for grid layouts)
|
||||
- `min-h-0` - Prevent height conflicts (important for flex layouts)
|
||||
|
||||
## Border Radius Pattern
|
||||
|
||||
**For cards:**
|
||||
```tsx
|
||||
className="rounded-theme-capped"
|
||||
```
|
||||
|
||||
**For buttons:**
|
||||
```tsx
|
||||
className="rounded-theme"
|
||||
```
|
||||
|
||||
Both respect `theme.borderRadius` setting (rounded/pill/sharp).
|
||||
|
||||
## Button Styling Classes
|
||||
|
||||
**Primary button (first in array):**
|
||||
```tsx
|
||||
className="primary-button"
|
||||
```
|
||||
|
||||
**Secondary button (second+ in array):**
|
||||
```tsx
|
||||
className="secondary-button"
|
||||
```
|
||||
|
||||
These classes are theme-aware and map to:
|
||||
- `theme.primaryButtonStyle` (gradient/solid/glass/outline)
|
||||
- `theme.secondaryButtonStyle` (glass/outline/solid/gradient)
|
||||
|
||||
## Styling Checklist
|
||||
|
||||
### Color Usage
|
||||
- [ ] Use `bg-background` and `text-foreground` for main colors
|
||||
- [ ] Use `bg-foreground` and `text-background` for inverted sections
|
||||
- [ ] Use `card` class for card backgrounds
|
||||
- [ ] Use `primary-button` and `secondary-button` for buttons
|
||||
- [ ] Avoid hardcoded colors (white, black, gray-X) unless justified
|
||||
|
||||
### Theme Integration
|
||||
- [ ] Wrap app/page in single ThemeProvider
|
||||
- [ ] Use `useTheme()` hook when needed
|
||||
- [ ] Don't specify button variants in ButtonConfig
|
||||
- [ ] Let TextBox inherit default text animation
|
||||
- [ ] Use `w-content-width mx-auto` for content
|
||||
|
||||
### Inverted Background (if applicable)
|
||||
- [ ] Accept `useInvertedBackground` as required string union: `"noInvert" | "invertDefault"`
|
||||
- [ ] Add `relative` class to section
|
||||
- [ ] Implement two-mode section className pattern (invertDefault with bg-foreground, noInvert standard)
|
||||
- [ ] Apply `text-background` to text for `"invertDefault"` mode (not `"noInvert"`)
|
||||
- [ ] Use explicit string equality checks (not truthy/falsy)
|
||||
- [ ] Use `shouldUseInvertedText` for card content if needed
|
||||
|
||||
### Card Styling
|
||||
- [ ] Use `card` class for theme-aware background
|
||||
- [ ] Use `rounded-theme-capped` for border radius
|
||||
- [ ] Include `min-h-0` for flex compatibility
|
||||
- [ ] Provide `cardClassName` override prop
|
||||
|
||||
### Spacing
|
||||
- [ ] Use `py-20` on sections (except hero/footer)
|
||||
- [ ] Use `w-content-width mx-auto` wrapper
|
||||
- [ ] Follow theme spacing guidelines
|
||||
55
eslint.config.mjs
Normal file
@@ -0,0 +1,55 @@
|
||||
import tseslint from "typescript-eslint";
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
||||
import nextPlugin from "@next/eslint-plugin-next";
|
||||
|
||||
export default tseslint.config(
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
files: ["**/*.{js,jsx,ts,tsx}"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-unused-expressions": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/app/*.{js,jsx,ts,tsx}"],
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
"react-hooks": reactHooksPlugin,
|
||||
"@next/next": nextPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...reactPlugin.configs["jsx-runtime"].rules,
|
||||
...reactHooksPlugin.configs.recommended.rules,
|
||||
...nextPlugin.configs.recommended.rules,
|
||||
...nextPlugin.configs["core-web-vitals"].rules,
|
||||
"react/no-unescaped-entities": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
}
|
||||
);
|
||||
632
fontThemes.json
Normal file
@@ -0,0 +1,632 @@
|
||||
{
|
||||
"singleFonts": {
|
||||
"interTight": {
|
||||
"name": "Inter Tight",
|
||||
"import": "import { Inter_Tight } from \"next/font/google\";",
|
||||
"initialization": "const interTight = Inter_Tight({\n variable: \"--font-inter-tight\",\n subsets: [\"latin\"],\n weight: [\"100\", \"200\", \"300\", \"400\", \"500\", \"600\", \"700\", \"800\", \"900\"],\n});",
|
||||
"className": "${interTight.variable}",
|
||||
"cssVariable": "font-family: var(--font-inter-tight), sans-serif;"
|
||||
},
|
||||
"inter": {
|
||||
"name": "Inter",
|
||||
"import": "import { Inter } from \"next/font/google\";",
|
||||
"initialization": "const inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${inter.variable}",
|
||||
"cssVariable": "font-family: var(--font-inter), sans-serif;"
|
||||
},
|
||||
"poppins": {
|
||||
"name": "Poppins",
|
||||
"import": "import { Poppins } from \"next/font/google\";",
|
||||
"initialization": "const poppins = Poppins({\n variable: \"--font-poppins\",\n subsets: [\"latin\"],\n weight: [\"100\", \"200\", \"300\", \"400\", \"500\", \"600\", \"700\", \"800\", \"900\"],\n});",
|
||||
"className": "${poppins.variable}",
|
||||
"cssVariable": "font-family: var(--font-poppins), sans-serif;"
|
||||
},
|
||||
"montserrat": {
|
||||
"name": "Montserrat",
|
||||
"import": "import { Montserrat } from \"next/font/google\";",
|
||||
"initialization": "const montserrat = Montserrat({\n variable: \"--font-montserrat\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${montserrat.variable}",
|
||||
"cssVariable": "font-family: var(--font-montserrat), sans-serif;"
|
||||
},
|
||||
"roboto": {
|
||||
"name": "Roboto",
|
||||
"import": "import { Roboto } from \"next/font/google\";",
|
||||
"initialization": "const roboto = Roboto({\n variable: \"--font-roboto\",\n subsets: [\"latin\"],\n weight: [\"100\", \"300\", \"400\", \"500\", \"700\", \"900\"],\n});",
|
||||
"className": "${roboto.variable}",
|
||||
"cssVariable": "font-family: var(--font-roboto), sans-serif;"
|
||||
},
|
||||
"openSans": {
|
||||
"name": "Open Sans",
|
||||
"import": "import { Open_Sans } from \"next/font/google\";",
|
||||
"initialization": "const openSans = Open_Sans({\n variable: \"--font-open-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${openSans.variable}",
|
||||
"cssVariable": "font-family: var(--font-open-sans), sans-serif;"
|
||||
},
|
||||
"lato": {
|
||||
"name": "Lato",
|
||||
"import": "import { Lato } from \"next/font/google\";",
|
||||
"initialization": "const lato = Lato({\n variable: \"--font-lato\",\n subsets: [\"latin\"],\n weight: [\"100\", \"300\", \"400\", \"700\", \"900\"],\n});",
|
||||
"className": "${lato.variable}",
|
||||
"cssVariable": "font-family: var(--font-lato), sans-serif;"
|
||||
},
|
||||
"workSans": {
|
||||
"name": "Work Sans",
|
||||
"import": "import { Work_Sans } from \"next/font/google\";",
|
||||
"initialization": "const workSans = Work_Sans({\n variable: \"--font-work-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${workSans.variable}",
|
||||
"cssVariable": "font-family: var(--font-work-sans), sans-serif;"
|
||||
},
|
||||
"spaceGrotesk": {
|
||||
"name": "Space Grotesk",
|
||||
"import": "import { Space_Grotesk } from \"next/font/google\";",
|
||||
"initialization": "const spaceGrotesk = Space_Grotesk({\n variable: \"--font-space-grotesk\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${spaceGrotesk.variable}",
|
||||
"cssVariable": "font-family: var(--font-space-grotesk), sans-serif;"
|
||||
},
|
||||
"dmSans": {
|
||||
"name": "DM Sans",
|
||||
"import": "import { DM_Sans } from \"next/font/google\";",
|
||||
"initialization": "const dmSans = DM_Sans({\n variable: \"--font-dm-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${dmSans.variable}",
|
||||
"cssVariable": "font-family: var(--font-dm-sans), sans-serif;"
|
||||
},
|
||||
"plusJakartaSans": {
|
||||
"name": "Plus Jakarta Sans",
|
||||
"import": "import { Plus_Jakarta_Sans } from \"next/font/google\";",
|
||||
"initialization": "const plusJakartaSans = Plus_Jakarta_Sans({\n variable: \"--font-plus-jakarta-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${plusJakartaSans.variable}",
|
||||
"cssVariable": "font-family: var(--font-plus-jakarta-sans), sans-serif;"
|
||||
},
|
||||
"manrope": {
|
||||
"name": "Manrope",
|
||||
"import": "import { Manrope } from \"next/font/google\";",
|
||||
"initialization": "const manrope = Manrope({\n variable: \"--font-manrope\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${manrope.variable}",
|
||||
"cssVariable": "font-family: var(--font-manrope), sans-serif;"
|
||||
},
|
||||
"outfit": {
|
||||
"name": "Outfit",
|
||||
"import": "import { Outfit } from \"next/font/google\";",
|
||||
"initialization": "const outfit = Outfit({\n variable: \"--font-outfit\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${outfit.variable}",
|
||||
"cssVariable": "font-family: var(--font-outfit), sans-serif;"
|
||||
},
|
||||
"rubik": {
|
||||
"name": "Rubik",
|
||||
"import": "import { Rubik } from \"next/font/google\";",
|
||||
"initialization": "const rubik = Rubik({\n variable: \"--font-rubik\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${rubik.variable}",
|
||||
"cssVariable": "font-family: var(--font-rubik), sans-serif;"
|
||||
},
|
||||
"playfairDisplay": {
|
||||
"name": "Playfair Display",
|
||||
"import": "import { Playfair_Display } from \"next/font/google\";",
|
||||
"initialization": "const playfairDisplay = Playfair_Display({\n variable: \"--font-playfair-display\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${playfairDisplay.variable}",
|
||||
"cssVariable": "font-family: var(--font-playfair-display), serif;",
|
||||
"category": "serif"
|
||||
},
|
||||
"merriweather": {
|
||||
"name": "Merriweather",
|
||||
"import": "import { Merriweather } from \"next/font/google\";",
|
||||
"initialization": "const merriweather = Merriweather({\n variable: \"--font-merriweather\",\n subsets: [\"latin\"],\n weight: [\"300\", \"400\", \"700\", \"900\"],\n});",
|
||||
"className": "${merriweather.variable}",
|
||||
"cssVariable": "font-family: var(--font-merriweather), serif;",
|
||||
"category": "serif"
|
||||
},
|
||||
"sourceSerif4": {
|
||||
"name": "Source Serif 4",
|
||||
"import": "import { Source_Serif_4 } from \"next/font/google\";",
|
||||
"initialization": "const sourceSerif4 = Source_Serif_4({\n variable: \"--font-source-serif-4\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${sourceSerif4.variable}",
|
||||
"cssVariable": "font-family: var(--font-source-serif-4), serif;",
|
||||
"category": "serif"
|
||||
},
|
||||
"jetBrainsMono": {
|
||||
"name": "JetBrains Mono",
|
||||
"import": "import { JetBrains_Mono } from \"next/font/google\";",
|
||||
"initialization": "const jetBrainsMono = JetBrains_Mono({\n variable: \"--font-jetbrains-mono\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${jetBrainsMono.variable}",
|
||||
"cssVariable": "font-family: var(--font-jetbrains-mono), monospace;",
|
||||
"category": "monospace"
|
||||
},
|
||||
"firaCode": {
|
||||
"name": "Fira Code",
|
||||
"import": "import { Fira_Code } from \"next/font/google\";",
|
||||
"initialization": "const firaCode = Fira_Code({\n variable: \"--font-fira-code\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${firaCode.variable}",
|
||||
"cssVariable": "font-family: var(--font-fira-code), monospace;",
|
||||
"category": "monospace"
|
||||
},
|
||||
"sourceCodePro": {
|
||||
"name": "Source Code Pro",
|
||||
"import": "import { Source_Code_Pro } from \"next/font/google\";",
|
||||
"initialization": "const sourceCodePro = Source_Code_Pro({\n variable: \"--font-source-code-pro\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${sourceCodePro.variable}",
|
||||
"cssVariable": "font-family: var(--font-source-code-pro), monospace;",
|
||||
"category": "monospace"
|
||||
},
|
||||
"sourceSans3": {
|
||||
"name": "Source Sans 3",
|
||||
"import": "import { Source_Sans_3 } from \"next/font/google\";",
|
||||
"initialization": "const sourceSans3 = Source_Sans_3({\n variable: \"--font-source-sans-3\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${sourceSans3.variable}",
|
||||
"cssVariable": "font-family: var(--font-source-sans-3), sans-serif;"
|
||||
},
|
||||
"nunito": {
|
||||
"name": "Nunito",
|
||||
"import": "import { Nunito } from \"next/font/google\";",
|
||||
"initialization": "const nunito = Nunito({\n variable: \"--font-nunito\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${nunito.variable}",
|
||||
"cssVariable": "font-family: var(--font-nunito), sans-serif;"
|
||||
},
|
||||
"cormorantGaramond": {
|
||||
"name": "Cormorant Garamond",
|
||||
"import": "import { Cormorant_Garamond } from \"next/font/google\";",
|
||||
"initialization": "const cormorantGaramond = Cormorant_Garamond({\n variable: \"--font-cormorant-garamond\",\n subsets: [\"latin\"],\n weight: [\"300\", \"400\", \"500\", \"600\", \"700\"],\n});",
|
||||
"className": "${cormorantGaramond.variable}",
|
||||
"cssVariable": "font-family: var(--font-cormorant-garamond), serif;",
|
||||
"category": "serif"
|
||||
},
|
||||
"lora": {
|
||||
"name": "Lora",
|
||||
"import": "import { Lora } from \"next/font/google\";",
|
||||
"initialization": "const lora = Lora({\n variable: \"--font-lora\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${lora.variable}",
|
||||
"cssVariable": "font-family: var(--font-lora), serif;",
|
||||
"category": "serif"
|
||||
},
|
||||
"publicSans": {
|
||||
"name": "Public Sans",
|
||||
"import": "import { Public_Sans } from \"next/font/google\";",
|
||||
"initialization": "const publicSans = Public_Sans({\n variable: \"--font-public-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${publicSans.variable}",
|
||||
"cssVariable": "font-family: var(--font-public-sans), sans-serif;"
|
||||
},
|
||||
"notoSans": {
|
||||
"name": "Noto Sans",
|
||||
"import": "import { Noto_Sans } from \"next/font/google\";",
|
||||
"initialization": "const notoSans = Noto_Sans({\n variable: \"--font-noto-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${notoSans.variable}",
|
||||
"cssVariable": "font-family: var(--font-noto-sans), sans-serif;"
|
||||
},
|
||||
"mulish": {
|
||||
"name": "Mulish",
|
||||
"import": "import { Mulish } from \"next/font/google\";",
|
||||
"initialization": "const mulish = Mulish({\n variable: \"--font-mulish\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${mulish.variable}",
|
||||
"cssVariable": "font-family: var(--font-mulish), sans-serif;"
|
||||
},
|
||||
"fraunces": {
|
||||
"name": "Fraunces",
|
||||
"import": "import { Fraunces } from \"next/font/google\";",
|
||||
"initialization": "const fraunces = Fraunces({\n variable: \"--font-fraunces\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${fraunces.variable}",
|
||||
"cssVariable": "font-family: var(--font-fraunces), serif;",
|
||||
"category": "serif"
|
||||
},
|
||||
"dmSerifDisplay": {
|
||||
"name": "DM Serif Display",
|
||||
"import": "import { DM_Serif_Display } from \"next/font/google\";",
|
||||
"initialization": "const dmSerifDisplay = DM_Serif_Display({\n variable: \"--font-dm-serif-display\",\n subsets: [\"latin\"],\n weight: [\"400\"],\n});",
|
||||
"className": "${dmSerifDisplay.variable}",
|
||||
"cssVariable": "font-family: var(--font-dm-serif-display), serif;",
|
||||
"category": "serif"
|
||||
},
|
||||
"prata": {
|
||||
"name": "Prata",
|
||||
"import": "import { Prata } from \"next/font/google\";",
|
||||
"initialization": "const prata = Prata({\n variable: \"--font-prata\",\n subsets: [\"latin\"],\n weight: [\"400\"],\n});",
|
||||
"className": "${prata.variable}",
|
||||
"cssVariable": "font-family: var(--font-prata), serif;",
|
||||
"category": "serif"
|
||||
},
|
||||
"epilogue": {
|
||||
"name": "Epilogue",
|
||||
"import": "import { Epilogue } from \"next/font/google\";",
|
||||
"initialization": "const epilogue = Epilogue({\n variable: \"--font-epilogue\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${epilogue.variable}",
|
||||
"cssVariable": "font-family: var(--font-epilogue), sans-serif;"
|
||||
},
|
||||
"cormorant": {
|
||||
"name": "Cormorant",
|
||||
"import": "import { Cormorant } from \"next/font/google\";",
|
||||
"initialization": "const cormorant = Cormorant({\n variable: \"--font-cormorant\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${cormorant.variable}",
|
||||
"cssVariable": "font-family: var(--font-cormorant), serif;",
|
||||
"category": "serif"
|
||||
},
|
||||
"ibmPlexSans": {
|
||||
"name": "IBM Plex Sans",
|
||||
"import": "import { IBM_Plex_Sans } from \"next/font/google\";",
|
||||
"initialization": "const ibmPlexSans = IBM_Plex_Sans({\n variable: \"--font-ibm-plex-sans\",\n subsets: [\"latin\"],\n weight: [\"100\", \"200\", \"300\", \"400\", \"500\", \"600\", \"700\"],\n});",
|
||||
"className": "${ibmPlexSans.variable}",
|
||||
"cssVariable": "font-family: var(--font-ibm-plex-sans), sans-serif;"
|
||||
},
|
||||
"sora": {
|
||||
"name": "Sora",
|
||||
"import": "import { Sora } from \"next/font/google\";",
|
||||
"initialization": "const sora = Sora({\n variable: \"--font-sora\",\n subsets: [\"latin\"],\n});",
|
||||
"className": "${sora.variable}",
|
||||
"cssVariable": "font-family: var(--font-sora), sans-serif;"
|
||||
},
|
||||
"spectral": {
|
||||
"name": "Spectral",
|
||||
"import": "import { Spectral } from \"next/font/google\";",
|
||||
"initialization": "const spectral = Spectral({\n variable: \"--font-spectral\",\n subsets: [\"latin\"],\n weight: [\"200\", \"300\", \"400\", \"500\", \"600\", \"700\", \"800\"],\n});",
|
||||
"className": "${spectral.variable}",
|
||||
"cssVariable": "font-family: var(--font-spectral), serif;",
|
||||
"category": "serif"
|
||||
}
|
||||
},
|
||||
"fontPairings": {
|
||||
"interOpenSans": {
|
||||
"name": "Inter + Open Sans",
|
||||
"description": "Neutral headings with friendly body. Clean and approachable.",
|
||||
"headingFont": "inter",
|
||||
"bodyFont": "openSans",
|
||||
"imports": "import { Inter } from \"next/font/google\";\nimport { Open_Sans } from \"next/font/google\";",
|
||||
"initializations": "const inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});\n\nconst openSans = Open_Sans({\n variable: \"--font-open-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${inter.variable} ${openSans.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-open-sans), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-open-sans), sans-serif",
|
||||
"headingsFontFamily": "var(--font-inter), sans-serif"
|
||||
}
|
||||
},
|
||||
"dmSansInter": {
|
||||
"name": "DM Sans + Inter",
|
||||
"description": "Modern geometric headings with neutral body. Contemporary and clean.",
|
||||
"headingFont": "dmSans",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { DM_Sans } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const dmSans = DM_Sans({\n variable: \"--font-dm-sans\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${dmSans.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-dm-sans), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-dm-sans), sans-serif"
|
||||
}
|
||||
},
|
||||
"workSansSourceSans3": {
|
||||
"name": "Work Sans + Source Sans 3",
|
||||
"description": "Versatile headings with modern body. Professional and balanced.",
|
||||
"headingFont": "workSans",
|
||||
"bodyFont": "sourceSans3",
|
||||
"imports": "import { Work_Sans } from \"next/font/google\";\nimport { Source_Sans_3 } from \"next/font/google\";",
|
||||
"initializations": "const workSans = Work_Sans({\n variable: \"--font-work-sans\",\n subsets: [\"latin\"],\n});\n\nconst sourceSans3 = Source_Sans_3({\n variable: \"--font-source-sans-3\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${workSans.variable} ${sourceSans3.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-source-sans-3), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-work-sans), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-source-sans-3), sans-serif",
|
||||
"headingsFontFamily": "var(--font-work-sans), sans-serif"
|
||||
}
|
||||
},
|
||||
"manropeDmSans": {
|
||||
"name": "Manrope + DM Sans",
|
||||
"description": "Geometric headings with clean body. Modern and professional.",
|
||||
"headingFont": "manrope",
|
||||
"bodyFont": "dmSans",
|
||||
"imports": "import { Manrope } from \"next/font/google\";\nimport { DM_Sans } from \"next/font/google\";",
|
||||
"initializations": "const manrope = Manrope({\n variable: \"--font-manrope\",\n subsets: [\"latin\"],\n});\n\nconst dmSans = DM_Sans({\n variable: \"--font-dm-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${manrope.variable} ${dmSans.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-dm-sans), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-manrope), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-dm-sans), sans-serif",
|
||||
"headingsFontFamily": "var(--font-manrope), sans-serif"
|
||||
}
|
||||
},
|
||||
"playfairNunito": {
|
||||
"name": "Playfair Display + Nunito",
|
||||
"description": "Classic serif headings with friendly rounded body. Elegant and approachable.",
|
||||
"headingFont": "playfairDisplay",
|
||||
"bodyFont": "nunito",
|
||||
"imports": "import { Playfair_Display } from \"next/font/google\";\nimport { Nunito } from \"next/font/google\";",
|
||||
"initializations": "const playfairDisplay = Playfair_Display({\n variable: \"--font-playfair-display\",\n subsets: [\"latin\"],\n});\n\nconst nunito = Nunito({\n variable: \"--font-nunito\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${playfairDisplay.variable} ${nunito.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-nunito), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-playfair-display), serif;\n}",
|
||||
"bodyFontFamily": "var(--font-nunito), sans-serif",
|
||||
"headingsFontFamily": "var(--font-playfair-display), serif"
|
||||
}
|
||||
},
|
||||
"cormorantGaramondInter": {
|
||||
"name": "Cormorant Garamond + Inter",
|
||||
"description": "Elegant serif headings with neutral body. Sophisticated and modern.",
|
||||
"headingFont": "cormorantGaramond",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Cormorant_Garamond } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const cormorantGaramond = Cormorant_Garamond({\n variable: \"--font-cormorant-garamond\",\n subsets: [\"latin\"],\n weight: [\"300\", \"400\", \"500\", \"600\", \"700\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${cormorantGaramond.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-cormorant-garamond), serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-cormorant-garamond), serif"
|
||||
}
|
||||
},
|
||||
"loraSourceSans3": {
|
||||
"name": "Lora + Source Sans 3",
|
||||
"description": "Readable serif headings with modern body. Editorial and professional.",
|
||||
"headingFont": "lora",
|
||||
"bodyFont": "sourceSans3",
|
||||
"imports": "import { Lora } from \"next/font/google\";\nimport { Source_Sans_3 } from \"next/font/google\";",
|
||||
"initializations": "const lora = Lora({\n variable: \"--font-lora\",\n subsets: [\"latin\"],\n});\n\nconst sourceSans3 = Source_Sans_3({\n variable: \"--font-source-sans-3\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${lora.variable} ${sourceSans3.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-source-sans-3), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-lora), serif;\n}",
|
||||
"bodyFontFamily": "var(--font-source-sans-3), sans-serif",
|
||||
"headingsFontFamily": "var(--font-lora), serif"
|
||||
}
|
||||
},
|
||||
"publicSansInter": {
|
||||
"name": "Public Sans + Inter",
|
||||
"description": "Government-inspired headings with neutral body. Professional and trustworthy.",
|
||||
"headingFont": "publicSans",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Public_Sans } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const publicSans = Public_Sans({\n variable: \"--font-public-sans\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${publicSans.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-public-sans), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-public-sans), sans-serif"
|
||||
}
|
||||
},
|
||||
"notoSansInter": {
|
||||
"name": "Noto Sans + Inter",
|
||||
"description": "Universal headings with neutral body. Global and accessible.",
|
||||
"headingFont": "notoSans",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Noto_Sans } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const notoSans = Noto_Sans({\n variable: \"--font-noto-sans\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${notoSans.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-noto-sans), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-noto-sans), sans-serif"
|
||||
}
|
||||
},
|
||||
"openSansWorkSans": {
|
||||
"name": "Open Sans + Work Sans",
|
||||
"description": "Friendly headings with versatile body. Approachable and balanced.",
|
||||
"headingFont": "openSans",
|
||||
"bodyFont": "workSans",
|
||||
"imports": "import { Open_Sans } from \"next/font/google\";\nimport { Work_Sans } from \"next/font/google\";",
|
||||
"initializations": "const openSans = Open_Sans({\n variable: \"--font-open-sans\",\n subsets: [\"latin\"],\n});\n\nconst workSans = Work_Sans({\n variable: \"--font-work-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${openSans.variable} ${workSans.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-work-sans), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-open-sans), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-work-sans), sans-serif",
|
||||
"headingsFontFamily": "var(--font-open-sans), sans-serif"
|
||||
}
|
||||
},
|
||||
"mulishInter": {
|
||||
"name": "Mulish + Inter",
|
||||
"description": "Minimal headings with neutral body. Clean and modern.",
|
||||
"headingFont": "mulish",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Mulish } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const mulish = Mulish({\n variable: \"--font-mulish\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${mulish.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-mulish), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-mulish), sans-serif"
|
||||
}
|
||||
},
|
||||
"frauncesInter": {
|
||||
"name": "Fraunces + Inter",
|
||||
"description": "Distinctive serif headings with neutral body. Bold and modern.",
|
||||
"headingFont": "fraunces",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Fraunces } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const fraunces = Fraunces({\n variable: \"--font-fraunces\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${fraunces.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-fraunces), serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-fraunces), serif"
|
||||
}
|
||||
},
|
||||
"dmSerifDisplayDmSans": {
|
||||
"name": "DM Serif Display + DM Sans",
|
||||
"description": "Elegant serif headings with clean sans-serif body. Cohesive DM family pairing.",
|
||||
"headingFont": "dmSerifDisplay",
|
||||
"bodyFont": "dmSans",
|
||||
"imports": "import { DM_Serif_Display } from \"next/font/google\";\nimport { DM_Sans } from \"next/font/google\";",
|
||||
"initializations": "const dmSerifDisplay = DM_Serif_Display({\n variable: \"--font-dm-serif-display\",\n subsets: [\"latin\"],\n weight: [\"400\"],\n});\n\nconst dmSans = DM_Sans({\n variable: \"--font-dm-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${dmSerifDisplay.variable} ${dmSans.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-dm-sans), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-dm-serif-display), serif;\n}",
|
||||
"bodyFontFamily": "var(--font-dm-sans), sans-serif",
|
||||
"headingsFontFamily": "var(--font-dm-serif-display), serif"
|
||||
}
|
||||
},
|
||||
"prataInter": {
|
||||
"name": "Prata + Inter",
|
||||
"description": "Elegant display serif headings with neutral body. Sophisticated and refined.",
|
||||
"headingFont": "prata",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Prata } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const prata = Prata({\n variable: \"--font-prata\",\n subsets: [\"latin\"],\n weight: [\"400\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${prata.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-prata), serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-prata), serif"
|
||||
}
|
||||
},
|
||||
"epilogueInter": {
|
||||
"name": "Epilogue + Inter",
|
||||
"description": "Modern variable headings with neutral body. Contemporary and flexible.",
|
||||
"headingFont": "epilogue",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Epilogue } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const epilogue = Epilogue({\n variable: \"--font-epilogue\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${epilogue.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-epilogue), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-epilogue), sans-serif"
|
||||
}
|
||||
},
|
||||
"plusJakartaSansInter": {
|
||||
"name": "Plus Jakarta Sans + Inter",
|
||||
"description": "Friendly rounded headings with neutral body. Approachable and modern.",
|
||||
"headingFont": "plusJakartaSans",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Plus_Jakarta_Sans } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const plusJakartaSans = Plus_Jakarta_Sans({\n variable: \"--font-plus-jakarta-sans\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${plusJakartaSans.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-plus-jakarta-sans), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-plus-jakarta-sans), sans-serif"
|
||||
}
|
||||
},
|
||||
"nunitoInter": {
|
||||
"name": "Nunito + Inter",
|
||||
"description": "Rounded friendly headings with neutral body. Warm and approachable.",
|
||||
"headingFont": "nunito",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Nunito } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const nunito = Nunito({\n variable: \"--font-nunito\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${nunito.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-nunito), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-nunito), sans-serif"
|
||||
}
|
||||
},
|
||||
"merriweatherOpenSans": {
|
||||
"name": "Merriweather + Open Sans",
|
||||
"description": "Classic serif headings with friendly body. Readable and approachable.",
|
||||
"headingFont": "merriweather",
|
||||
"bodyFont": "openSans",
|
||||
"imports": "import { Merriweather } from \"next/font/google\";\nimport { Open_Sans } from \"next/font/google\";",
|
||||
"initializations": "const merriweather = Merriweather({\n variable: \"--font-merriweather\",\n subsets: [\"latin\"],\n weight: [\"300\", \"400\", \"700\", \"900\"],\n});\n\nconst openSans = Open_Sans({\n variable: \"--font-open-sans\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${merriweather.variable} ${openSans.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-open-sans), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-merriweather), serif;\n}",
|
||||
"bodyFontFamily": "var(--font-open-sans), sans-serif",
|
||||
"headingsFontFamily": "var(--font-merriweather), serif"
|
||||
}
|
||||
},
|
||||
"cormorantLora": {
|
||||
"name": "Cormorant + Lora",
|
||||
"description": "Elegant serif headings with readable serif body. Classic and refined.",
|
||||
"headingFont": "cormorant",
|
||||
"bodyFont": "lora",
|
||||
"imports": "import { Cormorant } from \"next/font/google\";\nimport { Lora } from \"next/font/google\";",
|
||||
"initializations": "const cormorant = Cormorant({\n variable: \"--font-cormorant\",\n subsets: [\"latin\"],\n});\n\nconst lora = Lora({\n variable: \"--font-lora\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${cormorant.variable} ${lora.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-lora), serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-cormorant), serif;\n}",
|
||||
"bodyFontFamily": "var(--font-lora), serif",
|
||||
"headingsFontFamily": "var(--font-cormorant), serif"
|
||||
}
|
||||
},
|
||||
"montserratInter": {
|
||||
"name": "Montserrat + Inter",
|
||||
"description": "Geometric sans-serif headings with neutral body. Popular and reliable.",
|
||||
"headingFont": "montserrat",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Montserrat } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const montserrat = Montserrat({\n variable: \"--font-montserrat\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${montserrat.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-montserrat), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-montserrat), sans-serif"
|
||||
}
|
||||
},
|
||||
"spaceGroteskInter": {
|
||||
"name": "Space Grotesk + Inter",
|
||||
"description": "Modern geometric headings with neutral body. Tech-forward and contemporary.",
|
||||
"headingFont": "spaceGrotesk",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Space_Grotesk } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const spaceGrotesk = Space_Grotesk({\n variable: \"--font-space-grotesk\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${spaceGrotesk.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-space-grotesk), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-space-grotesk), sans-serif"
|
||||
}
|
||||
},
|
||||
"spectralInter": {
|
||||
"name": "Spectral + Inter",
|
||||
"description": "Screen-optimized serif headings with neutral body. Digital-first and readable.",
|
||||
"headingFont": "spectral",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Spectral } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const spectral = Spectral({\n variable: \"--font-spectral\",\n subsets: [\"latin\"],\n weight: [\"200\", \"300\", \"400\", \"500\", \"600\", \"700\", \"800\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${spectral.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-spectral), serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-spectral), serif"
|
||||
}
|
||||
},
|
||||
"soraInter": {
|
||||
"name": "Sora + Inter",
|
||||
"description": "Modern geometric headings with neutral body. Perfect for SaaS and tech products.",
|
||||
"headingFont": "sora",
|
||||
"bodyFont": "inter",
|
||||
"imports": "import { Sora } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
|
||||
"initializations": "const sora = Sora({\n variable: \"--font-sora\",\n subsets: [\"latin\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
|
||||
"classNames": "${sora.variable} ${inter.variable}",
|
||||
"globalsCss": {
|
||||
"instructions": "Update the font-family property within the existing CSS rules in globals.css (@layer base section)",
|
||||
"bodyRule": "body {\n /* ... existing properties ... */\n font-family: var(--font-inter), sans-serif;\n}",
|
||||
"headingsRule": "h1, h2, h3, h4, h5, h6 {\n font-family: var(--font-sora), sans-serif;\n}",
|
||||
"bodyFontFamily": "var(--font-inter), sans-serif",
|
||||
"headingsFontFamily": "var(--font-sora), sans-serif"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
next.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
devIndicators: false,
|
||||
experimental: {
|
||||
serverComponentsHmrCache: false,
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'webuild-dev.s3.eu-north-1.amazonaws.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'img.b2bpic.net',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'freepik.com',
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
8299
package-lock.json
generated
Normal file
46
package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "webild-components-2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build --turbopack",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gsap/react": "^2.1.2",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.4.0",
|
||||
"@tsparticles/engine": "^3.9.1",
|
||||
"@tsparticles/react": "^3.0.0",
|
||||
"@tsparticles/slim": "^3.9.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cobe": "^0.6.5",
|
||||
"embla-carousel-auto-scroll": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"gsap": "^3.13.0",
|
||||
"lenis": "^1.3.15",
|
||||
"lucide-react": "^0.555.0",
|
||||
"motion-number": "^1.0.0",
|
||||
"next": "16.0.7",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"recharts": "^3.6.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"three": "^0.181.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.7",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
public/audio/click.mp3
Normal file
BIN
public/base/bg-texture.avif
Normal file
|
After Width: | Height: | Size: 15 KiB |
52
public/brand/logo-dot.svg
Normal file
@@ -0,0 +1,52 @@
|
||||
<svg width="7" height="7" viewBox="0 0 7 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_f_1295_3221)">
|
||||
<ellipse cx="3.59341" cy="3.53052" rx="1.8861" ry="1.90568" transform="rotate(25.4025 3.59341 3.53052)" fill="url(#paint0_linear_1295_3221)"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_f_1295_3221)">
|
||||
<circle cx="3.69632" cy="3.86088" r="1.65146" transform="rotate(25.4025 3.69632 3.86088)" fill="url(#paint1_linear_1295_3221)"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_f_1295_3221)">
|
||||
<circle cx="3.69633" cy="3.86069" r="1.65146" transform="rotate(-169.598 3.69633 3.86069)" fill="url(#paint2_linear_1295_3221)"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_f_1295_3221)">
|
||||
<circle cx="3.74882" cy="3.91343" r="1.65146" transform="rotate(-169.598 3.74882 3.91343)" fill="url(#paint3_linear_1295_3221)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_1295_3221" x="1.25185" y="1.17653" width="4.68307" height="4.70798" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0" result="effect1_foregroundBlur_1295_3221"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_1295_3221" x="0.569126" y="0.733676" width="6.25433" height="6.25433" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0" result="effect1_foregroundBlur_1295_3221"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_1295_3221" x="1.16377" y="1.3282" width="5.06515" height="5.06478" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.0330293" result="effect1_foregroundBlur_1295_3221"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_1295_3221" x="0.687796" y="0.852469" width="6.12209" height="6.12172" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.704625" result="effect1_foregroundBlur_1295_3221"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1295_3221" x1="3.68116" y1="3.8564" x2="5.08951" y2="5.37639" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0F3DA6"/>
|
||||
<stop offset="0.951923" stop-color="#3A9AFF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1295_3221" x1="2.5674" y1="3.78948" x2="3.91229" y2="5.46656" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.00961538" stop-color="#0D50E8"/>
|
||||
<stop offset="0.951923" stop-color="#3A9AFF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1295_3221" x1="5.38038" y1="5.11694" x2="2.76523" y2="1.84712" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0F3DA6" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#59ABFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_1295_3221" x1="5.43287" y1="5.16968" x2="4.3403" y2="2.57182" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0F3DA6" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#59ABFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
74
public/brand/logo.svg
Normal file
@@ -0,0 +1,74 @@
|
||||
<svg width="67" height="21" viewBox="0 0 67 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M26.4375 7.16162C28.1869 7.16171 29.6159 7.78044 30.6064 8.88428C31.5956 9.98684 32.1387 11.5637 32.1387 13.4653V14.4478H23.374V14.4917C23.3742 15.5488 23.6921 16.4168 24.249 17.019C24.8052 17.6203 25.6093 17.9673 26.6025 17.9673C28.0159 17.9672 29.0384 17.2571 29.3633 16.228L29.3877 16.1509H32.0156L31.9961 16.2778C31.6314 18.6096 29.4248 20.2515 26.543 20.2515C24.6949 20.2514 23.2009 19.6255 22.1699 18.4937C21.1401 17.3628 20.5822 15.7381 20.582 13.7603C20.582 11.7937 21.1488 10.1435 22.168 8.98389C23.1888 7.82281 24.6569 7.16165 26.4375 7.16162ZM36.1025 9.28955C36.8971 7.97245 38.2815 7.18506 39.9678 7.18506C41.5623 7.18515 42.8859 7.81845 43.8086 8.95557C44.7294 10.0905 45.2441 11.7187 45.2441 13.7007C45.2441 15.6882 44.7324 17.3191 43.8115 18.4556C42.8887 19.5944 41.5621 20.2279 39.9561 20.228C38.2187 20.228 36.8448 19.4336 36.0312 18.1226V20.0151H33.334V2.77295H36.1025V9.28955ZM67 20.0151H64.3027V18.1284C63.5158 19.417 62.1356 20.228 60.4141 20.228C58.8134 20.228 57.4778 19.588 56.5449 18.4468C55.614 17.3077 55.0899 15.677 55.0898 13.7007C55.0898 11.7241 55.6138 10.0956 56.542 8.95947C57.4719 7.82125 58.8017 7.18506 60.3906 7.18506C62.0846 7.18514 63.4609 7.98063 64.2207 9.2251V2.77295H67V20.0151ZM2.99902 2.85791L6.0918 15.9497L9.62793 2.85498L9.64941 2.77295H12.2715L12.2939 2.85498L15.8398 15.9507L18.9336 2.85791L18.9541 2.77295H21.9336L21.8955 2.91162L17.2949 19.9341L17.2734 20.0151H14.6172L14.5938 19.9351L10.9609 7.26416L7.33887 19.9351L7.31641 20.0151H4.64746L4.62598 19.9341L0.0371094 2.91162L0 2.77295H2.97949L2.99902 2.85791ZM53.9062 20.0151H51.1377V2.77295H53.9062V20.0151ZM49.0352 19.9839H46.2666V7.354H49.0352V19.9839ZM60.9092 17.9536C60.9674 17.9566 61.0263 17.9585 61.0859 17.9585V17.9575L60.9092 17.9536ZM60.6924 17.9351C60.7503 17.9417 60.8088 17.947 60.8682 17.9507C60.8088 17.947 60.7503 17.9417 60.6924 17.9351ZM60.4434 17.8979C60.5109 17.911 60.5797 17.9212 60.6494 17.9302C60.5797 17.9212 60.5109 17.911 60.4434 17.8979ZM39.2715 9.55225C38.3397 9.55229 37.5483 9.96027 36.9873 10.6792C36.4249 11.4002 36.0908 12.4394 36.0908 13.7007C36.0909 14.9739 36.425 16.0131 36.9873 16.731C37.548 17.4465 38.3393 17.8491 39.2715 17.8491C40.2407 17.8491 41.0189 17.4568 41.5576 16.7495C42.0989 16.0387 42.4052 15 42.4053 13.7007C42.4053 12.4132 42.0991 11.3737 41.5576 10.6597C41.0186 9.94925 40.2402 9.5523 39.2715 9.55225ZM61.0869 9.55225C60.1117 9.55225 59.3237 9.94972 58.7773 10.6606C58.2287 11.3747 57.917 12.4136 57.917 13.7007C57.917 14.9937 58.2288 16.0322 58.7773 16.7446C59.3236 17.454 60.1115 17.8491 61.0869 17.8491C62.0312 17.849 62.8158 17.449 63.3672 16.7358C63.9205 16.0199 64.2441 14.9807 64.2441 13.7007C64.2441 12.4325 63.9207 11.3935 63.3672 10.6743C62.8155 9.95775 62.0308 9.55241 61.0869 9.55225ZM41.2676 17.2271C41.3209 17.1788 41.3736 17.1293 41.4238 17.0767L41.4307 17.0688C41.3782 17.1242 41.3235 17.1766 41.2676 17.2271ZM57.8643 14.6216C57.8776 14.7252 57.8932 14.8268 57.9111 14.9263C57.8933 14.8268 57.8775 14.7252 57.8643 14.6216ZM57.8066 13.7007C57.8067 14.0028 57.8244 14.2919 57.8574 14.5669C57.8424 14.4409 57.8305 14.312 57.8223 14.1802L57.8076 13.7007C57.8076 13.6621 57.809 13.6237 57.8096 13.5854C57.809 13.6237 57.8066 13.6621 57.8066 13.7007ZM45.1035 14.5063C45.1121 14.3932 45.1182 14.2786 45.123 14.1626C45.1182 14.2786 45.112 14.3932 45.1035 14.5063ZM45.1338 13.7007C45.1338 13.8433 45.1302 13.9839 45.125 14.1226C45.1257 14.1037 45.1283 14.0849 45.1289 14.0659L45.1348 13.7007C45.1348 13.5487 45.1312 13.399 45.125 13.2515L45.1338 13.7007ZM57.8242 13.2104C57.8213 13.2551 57.8185 13.3 57.8164 13.3452C57.8185 13.3 57.8213 13.2551 57.8242 13.2104ZM57.8555 12.8501C57.8514 12.8844 57.8483 12.9189 57.8447 12.9536C57.8483 12.9189 57.8514 12.8844 57.8555 12.8501ZM57.9346 12.3618C57.9223 12.423 57.909 12.4846 57.8984 12.5474C57.909 12.4846 57.9223 12.423 57.9346 12.3618ZM26.4375 9.43506C24.7618 9.43512 23.5679 10.68 23.3984 12.4468H29.3623C29.3129 11.5693 29.0132 10.8288 28.5244 10.3032C28.0155 9.75613 27.2989 9.43515 26.4375 9.43506ZM57.999 12.0737C57.9868 12.1214 57.975 12.1695 57.9639 12.2183C57.975 12.1695 57.9868 12.1214 57.999 12.0737ZM58.1367 11.6313C58.1073 11.7129 58.0784 11.7959 58.0527 11.8813C58.0784 11.7959 58.1073 11.7129 58.1367 11.6313ZM58.2197 11.4146C58.204 11.4519 58.1887 11.4896 58.1738 11.5278C58.1887 11.4896 58.204 11.4519 58.2197 11.4146ZM58.3633 11.1089C58.3476 11.1388 58.3324 11.1691 58.3174 11.1997C58.3324 11.1691 58.3476 11.1388 58.3633 11.1089ZM58.9629 10.2827C58.8665 10.3786 58.7757 10.4823 58.6904 10.5933L58.7998 10.4595C58.8524 10.3981 58.9065 10.3388 58.9629 10.2827ZM59.5752 9.81885C59.3611 9.93932 59.164 10.0873 58.9854 10.2612C59.0426 10.2054 59.1021 10.1525 59.1631 10.1021C59.2493 10.0308 59.3391 9.96501 59.4326 9.90479C59.4793 9.87472 59.5268 9.84615 59.5752 9.81885ZM26.2725 9.32861C26.327 9.326 26.382 9.32471 26.4375 9.32471L26.5312 9.32666C26.5002 9.32581 26.4689 9.32373 26.4375 9.32373C26.382 9.32373 26.3269 9.32601 26.2725 9.32861Z" fill="black"/>
|
||||
<mask id="mask0_1119_6721" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="2" width="67" height="19">
|
||||
<path d="M26.4375 7.16162C28.1869 7.16171 29.6159 7.78044 30.6064 8.88428C31.5956 9.98684 32.1387 11.5637 32.1387 13.4653V14.4478H23.374V14.4917C23.3742 15.5488 23.6921 16.4168 24.249 17.019C24.8052 17.6203 25.6093 17.9673 26.6025 17.9673C28.0159 17.9672 29.0384 17.2571 29.3633 16.228L29.3877 16.1509H32.0156L31.9961 16.2778C31.6314 18.6096 29.4248 20.2515 26.543 20.2515C24.6949 20.2514 23.2009 19.6255 22.1699 18.4937C21.1401 17.3628 20.5822 15.7381 20.582 13.7603C20.582 11.7937 21.1488 10.1435 22.168 8.98389C23.1888 7.82281 24.6569 7.16165 26.4375 7.16162ZM36.1025 9.28955C36.8971 7.97245 38.2815 7.18506 39.9678 7.18506C41.5623 7.18515 42.8859 7.81845 43.8086 8.95557C44.7294 10.0905 45.2441 11.7187 45.2441 13.7007C45.2441 15.6882 44.7324 17.3191 43.8115 18.4556C42.8887 19.5944 41.5621 20.2279 39.9561 20.228C38.2187 20.228 36.8448 19.4336 36.0312 18.1226V20.0151H33.334V2.77295H36.1025V9.28955ZM67 20.0151H64.3027V18.1284C63.5158 19.417 62.1356 20.228 60.4141 20.228C58.8134 20.228 57.4778 19.588 56.5449 18.4468C55.614 17.3077 55.0899 15.677 55.0898 13.7007C55.0898 11.7241 55.6138 10.0956 56.542 8.95947C57.4719 7.82125 58.8017 7.18506 60.3906 7.18506C62.0846 7.18514 63.4609 7.98063 64.2207 9.2251V2.77295H67V20.0151ZM2.99902 2.85791L6.0918 15.9497L9.62793 2.85498L9.64941 2.77295H12.2715L12.2939 2.85498L15.8398 15.9507L18.9336 2.85791L18.9541 2.77295H21.9336L21.8955 2.91162L17.2949 19.9341L17.2734 20.0151H14.6172L14.5938 19.9351L10.9609 7.26416L7.33887 19.9351L7.31641 20.0151H4.64746L4.62598 19.9341L0.0371094 2.91162L0 2.77295H2.97949L2.99902 2.85791ZM53.9062 20.0151H51.1377V2.77295H53.9062V20.0151ZM49.0352 19.9839H46.2666V7.354H49.0352V19.9839ZM60.9092 17.9536C60.9674 17.9566 61.0263 17.9585 61.0859 17.9585V17.9575L60.9092 17.9536ZM60.6924 17.9351C60.7503 17.9417 60.8088 17.947 60.8682 17.9507C60.8088 17.947 60.7503 17.9417 60.6924 17.9351ZM60.4434 17.8979C60.5109 17.911 60.5797 17.9212 60.6494 17.9302C60.5797 17.9212 60.5109 17.911 60.4434 17.8979ZM39.2715 9.55225C38.3397 9.55229 37.5483 9.96027 36.9873 10.6792C36.4249 11.4002 36.0908 12.4394 36.0908 13.7007C36.0909 14.9739 36.425 16.0131 36.9873 16.731C37.548 17.4465 38.3393 17.8491 39.2715 17.8491C40.2407 17.8491 41.0189 17.4568 41.5576 16.7495C42.0989 16.0387 42.4052 15 42.4053 13.7007C42.4053 12.4132 42.0991 11.3737 41.5576 10.6597C41.0186 9.94925 40.2402 9.5523 39.2715 9.55225ZM61.0869 9.55225C60.1117 9.55225 59.3237 9.94972 58.7773 10.6606C58.2287 11.3747 57.917 12.4136 57.917 13.7007C57.917 14.9937 58.2288 16.0322 58.7773 16.7446C59.3236 17.454 60.1115 17.8491 61.0869 17.8491C62.0312 17.849 62.8158 17.449 63.3672 16.7358C63.9205 16.0199 64.2441 14.9807 64.2441 13.7007C64.2441 12.4325 63.9207 11.3935 63.3672 10.6743C62.8155 9.95775 62.0308 9.55241 61.0869 9.55225ZM41.2676 17.2271C41.3209 17.1788 41.3736 17.1293 41.4238 17.0767L41.4307 17.0688C41.3782 17.1242 41.3235 17.1766 41.2676 17.2271ZM57.8643 14.6216C57.8776 14.7252 57.8932 14.8268 57.9111 14.9263C57.8933 14.8268 57.8775 14.7252 57.8643 14.6216ZM57.8066 13.7007C57.8067 14.0028 57.8244 14.2919 57.8574 14.5669C57.8424 14.4409 57.8305 14.312 57.8223 14.1802L57.8076 13.7007C57.8076 13.6621 57.809 13.6237 57.8096 13.5854C57.809 13.6237 57.8066 13.6621 57.8066 13.7007ZM45.1035 14.5063C45.1121 14.3932 45.1182 14.2786 45.123 14.1626C45.1182 14.2786 45.112 14.3932 45.1035 14.5063ZM45.1338 13.7007C45.1338 13.8433 45.1302 13.9839 45.125 14.1226C45.1257 14.1037 45.1283 14.0849 45.1289 14.0659L45.1348 13.7007C45.1348 13.5487 45.1312 13.399 45.125 13.2515L45.1338 13.7007ZM57.8242 13.2104C57.8213 13.2551 57.8185 13.3 57.8164 13.3452C57.8185 13.3 57.8213 13.2551 57.8242 13.2104ZM57.8555 12.8501C57.8514 12.8844 57.8483 12.9189 57.8447 12.9536C57.8483 12.9189 57.8514 12.8844 57.8555 12.8501ZM57.9346 12.3618C57.9223 12.423 57.909 12.4846 57.8984 12.5474C57.909 12.4846 57.9223 12.423 57.9346 12.3618ZM26.4375 9.43506C24.7618 9.43512 23.5679 10.68 23.3984 12.4468H29.3623C29.3129 11.5693 29.0132 10.8288 28.5244 10.3032C28.0155 9.75613 27.2989 9.43515 26.4375 9.43506ZM57.999 12.0737C57.9868 12.1214 57.975 12.1695 57.9639 12.2183C57.975 12.1695 57.9868 12.1214 57.999 12.0737ZM58.1367 11.6313C58.1073 11.7129 58.0784 11.7959 58.0527 11.8813C58.0784 11.7959 58.1073 11.7129 58.1367 11.6313ZM58.2197 11.4146C58.204 11.4519 58.1887 11.4896 58.1738 11.5278C58.1887 11.4896 58.204 11.4519 58.2197 11.4146ZM58.3633 11.1089C58.3476 11.1388 58.3324 11.1691 58.3174 11.1997C58.3324 11.1691 58.3476 11.1388 58.3633 11.1089ZM58.9629 10.2827C58.8665 10.3786 58.7757 10.4823 58.6904 10.5933L58.7998 10.4595C58.8524 10.3981 58.9065 10.3388 58.9629 10.2827ZM59.5752 9.81885C59.3611 9.93932 59.164 10.0873 58.9854 10.2612C59.0426 10.2054 59.1021 10.1525 59.1631 10.1021C59.2493 10.0308 59.3391 9.96501 59.4326 9.90479C59.4793 9.87472 59.5268 9.84615 59.5752 9.81885ZM26.2725 9.32861C26.327 9.326 26.382 9.32471 26.4375 9.32471L26.5312 9.32666C26.5002 9.32581 26.4689 9.32373 26.4375 9.32373C26.382 9.32373 26.3269 9.32601 26.2725 9.32861Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1119_6721)">
|
||||
<g opacity="0.17" filter="url(#filter0_f_1119_6721)">
|
||||
<path d="M36.1377 2.6198H32.5596L42.4684 -6.4082L54.7993 -5.08703L62.8915 -1.34371L55.57 3.39048C54.469 3.18864 52.1459 2.90606 51.6615 3.39048C51.056 3.99602 51.056 5.64749 50.8908 6.91361C50.7587 7.92651 49.8082 7.84944 49.3495 7.68429L46.4319 8.12468L45.5511 12.4185C45.3125 12.2533 44.6813 11.5707 44.0648 10.1615C43.2941 8.39993 42.3032 7.84944 40.7068 7.68429C39.4297 7.55217 36.78 8.47332 36.1377 8.78527V2.6198Z" fill="#0597FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_f_1119_6721)">
|
||||
<path d="M36.1377 2.6198H32.5596L42.4684 -6.4082L54.7993 -5.08703L62.8915 -1.34371L55.57 3.39048C54.469 3.18864 52.1459 2.90606 51.6615 3.39048C51.056 3.99602 51.056 5.64749 50.8908 6.91361C50.7587 7.92651 49.8082 7.84944 49.3495 7.68429L46.4319 8.12468L45.5511 12.4185C45.3125 12.2533 44.6813 11.5707 44.0648 10.1615C43.2941 8.39993 42.3032 7.84944 40.7068 7.68429C39.4297 7.55217 36.78 8.47332 36.1377 8.78527V2.6198Z" fill="#0597FF"/>
|
||||
</g>
|
||||
</g>
|
||||
<g filter="url(#filter2_f_1119_6721)">
|
||||
<ellipse cx="47.5933" cy="3.27857" rx="1.8861" ry="1.90568" transform="rotate(25.4025 47.5933 3.27857)" fill="url(#paint0_linear_1119_6721)"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_f_1119_6721)">
|
||||
<circle cx="47.6964" cy="3.60892" r="1.65146" transform="rotate(25.4025 47.6964 3.60892)" fill="url(#paint1_linear_1119_6721)"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_f_1119_6721)">
|
||||
<circle cx="47.6963" cy="3.60874" r="1.65146" transform="rotate(-169.598 47.6963 3.60874)" fill="url(#paint2_linear_1119_6721)"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_f_1119_6721)">
|
||||
<circle cx="47.7491" cy="3.66147" r="1.65146" transform="rotate(-169.598 47.7491 3.66147)" fill="url(#paint3_linear_1119_6721)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_1119_6721" x="31.2494" y="-7.71836" width="32.9524" height="21.447" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.655081" result="effect1_foregroundBlur_1119_6721"/>
|
||||
</filter>
|
||||
<filter id="filter1_f_1119_6721" x="27.1648" y="-11.803" width="41.1216" height="29.6162" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="2.69739" result="effect1_foregroundBlur_1119_6721"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_1119_6721" x="45.2517" y="0.924576" width="4.68307" height="4.70798" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0" result="effect1_foregroundBlur_1119_6721"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_1119_6721" x="44.5691" y="0.481723" width="6.25433" height="6.25433" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0" result="effect1_foregroundBlur_1119_6721"/>
|
||||
</filter>
|
||||
<filter id="filter4_f_1119_6721" x="45.1637" y="1.07625" width="5.06527" height="5.06478" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.0330293" result="effect1_foregroundBlur_1119_6721"/>
|
||||
</filter>
|
||||
<filter id="filter5_f_1119_6721" x="44.6879" y="0.600516" width="6.12221" height="6.12172" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.704625" result="effect1_foregroundBlur_1119_6721"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1119_6721" x1="47.681" y1="3.60445" x2="49.0894" y2="5.12444" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0F3DA6"/>
|
||||
<stop offset="0.951923" stop-color="#3A9AFF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1119_6721" x1="46.5675" y1="3.53753" x2="47.9124" y2="5.21461" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.00961538" stop-color="#0D50E8"/>
|
||||
<stop offset="0.951923" stop-color="#3A9AFF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1119_6721" x1="49.3804" y1="4.86499" x2="46.7652" y2="1.59517" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0F3DA6" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#59ABFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_1119_6721" x1="49.4331" y1="4.91773" x2="48.3405" y2="2.31986" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0F3DA6" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#59ABFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
58
public/brand/logowhite.svg
Normal file
@@ -0,0 +1,58 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 67 21" width="67" height="21">
|
||||
<defs>
|
||||
<clipPath clipPathUnits="userSpaceOnUse" id="cp1">
|
||||
<path d="m26.44 7.16c1.75 0 3.18 0.62 4.17 1.72 0.99 1.11 1.53 2.68 1.53 4.59v0.98h-8.77v0.04c0 1.06 0.32 1.93 0.88 2.53 0.56 0.6 1.36 0.95 2.35 0.95 1.42 0 2.44-0.71 2.76-1.74l0.03-0.08h2.63l-0.02 0.13c-0.37 2.33-2.58 3.97-5.46 3.97-1.85 0-3.34-0.62-4.37-1.76-1.03-1.13-1.59-2.75-1.59-4.73 0-1.97 0.57-3.62 1.59-4.78 1.02-1.16 2.49-1.82 4.27-1.82zm9.66 2.13c0.8-1.32 2.18-2.1 3.87-2.1 1.59 0 2.92 0.63 3.84 1.77 0.92 1.13 1.43 2.76 1.43 4.74 0 1.99-0.51 3.62-1.43 4.76-0.92 1.13-2.25 1.77-3.85 1.77-1.74 0-3.12-0.8-3.93-2.11v1.9h-2.7v-17.25h2.77zm30.9 10.73h-2.7v-1.89c-0.78 1.29-2.16 2.1-3.89 2.1-1.6 0-2.93-0.64-3.87-1.78-0.93-1.14-1.45-2.77-1.45-4.75 0-1.97 0.52-3.6 1.45-4.74 0.93-1.14 2.26-1.77 3.85-1.77 1.69 0 3.07 0.79 3.83 2.04v-6.45h2.78zm-64-17.16l3.09 13.09 3.54-13.09 0.02-0.09h2.62l0.02 0.09 3.55 13.09 3.09-13.09 0.02-0.09h2.98l-0.03 0.14-4.61 17.03-0.02 0.08h-2.65l-0.03-0.08-3.63-12.67-3.62 12.67-0.02 0.08h-2.67l-0.02-0.08-4.59-17.03-0.04-0.14h2.98zm50.91 17.16h-2.77v-17.24h2.77zm-4.87-0.03h-2.77v-12.63h2.77zm11.87-2.03q0.09 0 0.18 0zm-21.64-8.4c-0.93 0-1.72 0.41-2.29 1.13-0.56 0.72-0.89 1.76-0.89 3.02 0 1.27 0.33 2.31 0.89 3.03 0.57 0.71 1.36 1.12 2.29 1.12 0.97 0 1.75-0.4 2.28-1.1 0.55-0.71 0.85-1.75 0.85-3.05 0-1.29-0.3-2.33-0.85-3.04-0.53-0.71-1.31-1.11-2.28-1.11zm21.81 0c-0.97 0-1.76 0.4-2.31 1.11-0.54 0.71-0.86 1.75-0.86 3.04 0 1.29 0.32 2.33 0.86 3.04 0.55 0.71 1.34 1.11 2.31 1.11 0.95 0 1.73-0.4 2.28-1.12 0.56-0.71 0.88-1.75 0.88-3.03 0-1.27-0.32-2.31-0.88-3.03-0.55-0.71-1.33-1.12-2.28-1.12zm-19.79 7.65q-0.01 0.01-0.03 0.03 0.02-0.02 0.03-0.03zm0.13-0.13h0.01q-0.07 0.07-0.14 0.13 0.07-0.06 0.13-0.13zm16.38-3.37q0 0.45 0.05 0.87-0.02-0.19-0.03-0.39l-0.02-0.48q0.01-0.06 0.01-0.12-0.01 0.06-0.01 0.12zm-12.67 0q0 0.04 0 0.09v-0.09q0-0.23-0.01-0.45zm0 0.36v-0.15q0 0.11-0.01 0.21 0.01-0.03 0.01-0.06zm0-0.15q0-0.06 0-0.12zm-18.69-4.47c-1.68 0-2.87 1.24-3.04 3.01h5.96c-0.05-0.88-0.35-1.62-0.84-2.15-0.5-0.54-1.22-0.86-2.08-0.86zm32.25 1.15l0.11-0.13q0.08-0.09 0.16-0.17-0.14 0.14-0.27 0.3zm0.89-0.77q-0.31 0.17-0.56 0.41 0.07-0.07 0.14-0.13 0.13-0.1 0.27-0.19 0.07-0.05 0.15-0.09zm-0.56 0.41q-0.02 0.02-0.03 0.03 0.01-0.01 0.03-0.03zm-32.75-0.9q0 0 0 0 0 0 0 0zm0.17 0h0.09q-0.05 0-0.09-0.01-0.09 0.01-0.17 0.01 0.08 0 0.17 0zm34.25 8.61q0.09 0.01 0.18 0.01-0.09 0-0.18-0.01zm-0.25-0.04q0.11 0.02 0.21 0.03-0.1-0.01-0.21-0.03zm-2.58-3.28q0.02 0.16 0.05 0.31-0.03-0.15-0.05-0.31zm-12.76-0.11q0.02-0.17 0.02-0.35 0 0.18-0.02 0.35zm12.72-1.3q0 0.07 0 0.14 0-0.07 0-0.14zm0.04-0.36q-0.01 0.05-0.02 0.1 0.01-0.05 0.02-0.1zm0.07-0.49q-0.01 0.09-0.03 0.19 0.02-0.1 0.03-0.19zm0.07-0.29q-0.02 0.08-0.04 0.15 0.02-0.07 0.04-0.15zm0.14-0.44q-0.05 0.12-0.09 0.25 0.04-0.13 0.09-0.25zm0.08-0.21q-0.02 0.05-0.05 0.11 0.03-0.06 0.05-0.11zm0.14-0.31q-0.02 0.04-0.04 0.09 0.02-0.05 0.04-0.09z"/>
|
||||
</clipPath>
|
||||
<filter x="-50%" y="-50%" width="200%" height="200%" id="f1"> <feGaussianBlur stdDeviation=".7"/> </filter>
|
||||
<filter x="-50%" y="-50%" width="200%" height="200%" id="f2"> <feGaussianBlur stdDeviation="2.7"/> </filter>
|
||||
<linearGradient id="g1" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.62,1.977,-1.976,.62,47.533,3.611)">
|
||||
<stop offset="0" stop-color="#0f3da6" stop-opacity="1"/>
|
||||
<stop offset=".952" stop-color="#3a9aff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="g2" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.495,2.092,-2.092,.495,46.707,3.06)">
|
||||
<stop offset=".01" stop-color="#0d50e8" stop-opacity="1"/>
|
||||
<stop offset=".952" stop-color="#3a9aff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<filter x="-50%" y="-50%" width="200%" height="200%" id="f3"> <feGaussianBlur stdDeviation="0"/> </filter>
|
||||
<linearGradient id="g3" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.982,3.688,-3.688,1.982,46.267,2.069)">
|
||||
<stop offset="0" stop-color="#0f3da6" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#59abff" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="g4" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.606,2.752,-2.752,.606,46.32,2.122)">
|
||||
<stop offset="0" stop-color="#0f3da6" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#59abff" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<style>
|
||||
.s0 { fill: #ffffff }
|
||||
.s1 { filter: url(#f1);fill: #0597ff }
|
||||
.s2 { filter: url(#f2);fill: #0597ff }
|
||||
.s3 { fill: url(#g1) }
|
||||
.s4 { fill: url(#g2) }
|
||||
.s5 { filter: url(#f3);fill: url(#g3) }
|
||||
.s6 { filter: url(#f1);fill: url(#g4) }
|
||||
</style>
|
||||
<path class="s0" d="m26.44 7.16c1.75 0 3.18 0.62 4.17 1.72 0.99 1.11 1.53 2.68 1.53 4.59v0.98h-8.77v0.04c0 1.06 0.32 1.93 0.88 2.53 0.56 0.6 1.36 0.95 2.35 0.95 1.42 0 2.44-0.71 2.76-1.74l0.03-0.08h2.63l-0.02 0.13c-0.37 2.33-2.58 3.97-5.46 3.97-1.85 0-3.34-0.62-4.37-1.76-1.03-1.13-1.59-2.75-1.59-4.73 0-1.97 0.57-3.62 1.59-4.78 1.02-1.16 2.49-1.82 4.27-1.82zm9.66 2.13c0.8-1.32 2.18-2.1 3.87-2.1 1.59 0 2.92 0.63 3.84 1.77 0.92 1.13 1.43 2.76 1.43 4.74 0 1.99-0.51 3.62-1.43 4.76-0.92 1.13-2.25 1.77-3.85 1.77-1.74 0-3.12-0.8-3.93-2.11v1.9h-2.7v-17.25h2.77zm30.9 10.73h-2.7v-1.89c-0.78 1.29-2.16 2.1-3.89 2.1-1.6 0-2.93-0.64-3.87-1.78-0.93-1.14-1.45-2.77-1.45-4.75 0-1.97 0.52-3.6 1.45-4.74 0.93-1.14 2.26-1.77 3.85-1.77 1.69 0 3.07 0.79 3.83 2.04v-6.45h2.78zm-64-17.16l3.09 13.09 3.54-13.09 0.02-0.09h2.62l0.02 0.09 3.55 13.09 3.09-13.09 0.02-0.09h2.98l-0.03 0.14-4.61 17.03-0.02 0.08h-2.65l-0.03-0.08-3.63-12.67-3.62 12.67-0.02 0.08h-2.67l-0.02-0.08-4.59-17.03-0.04-0.14h2.98zm50.91 17.16h-2.77v-17.24h2.77zm-4.87-0.03h-2.77v-12.63h2.77zm11.87-2.03q0.09 0 0.18 0zm-0.22-0.02q0.09 0.01 0.18 0.02-0.09-0.01-0.18-0.02zm-0.25-0.03q0.1 0.02 0.21 0.03-0.11-0.01-0.21-0.03zm-21.17-8.35c-0.93 0-1.72 0.41-2.29 1.13-0.56 0.72-0.89 1.76-0.89 3.02 0 1.27 0.33 2.31 0.89 3.03 0.57 0.71 1.36 1.12 2.29 1.12 0.97 0 1.75-0.4 2.28-1.1 0.55-0.71 0.85-1.75 0.85-3.05 0-1.29-0.3-2.33-0.85-3.04-0.53-0.71-1.31-1.11-2.28-1.11zm21.81 0c-0.97 0-1.76 0.4-2.31 1.11-0.54 0.71-0.86 1.75-0.86 3.04 0 1.29 0.32 2.33 0.86 3.04 0.55 0.71 1.34 1.11 2.31 1.11 0.95 0 1.73-0.4 2.28-1.12 0.56-0.71 0.88-1.75 0.88-3.03 0-1.27-0.32-2.31-0.88-3.03-0.55-0.71-1.33-1.12-2.28-1.12zm-19.82 7.68q0.09-0.08 0.16-0.16h0.01q-0.08 0.08-0.17 0.16zm16.6-2.61q0.02 0.16 0.05 0.3-0.03-0.14-0.05-0.3zm-0.06-0.92q0 0.45 0.05 0.87-0.02-0.19-0.03-0.39l-0.02-0.48q0.01-0.06 0.01-0.12-0.01 0.06-0.01 0.12zm-12.7 0.8q0.01-0.17 0.02-0.34-0.01 0.17-0.02 0.34zm0.03-0.8q0 0.21-0.01 0.42 0-0.03 0.01-0.06v-0.36q0-0.23-0.01-0.45zm12.69-0.49q0 0.07-0.01 0.13 0.01-0.06 0.01-0.13zm0.03-0.36q0 0.05-0.01 0.1 0.01-0.05 0.01-0.1zm0.08-0.49q-0.02 0.09-0.03 0.19 0.01-0.1 0.03-0.19zm-31.49-2.93c-1.68 0-2.87 1.25-3.04 3.02h5.96c-0.05-0.88-0.35-1.62-0.84-2.15-0.51-0.54-1.22-0.87-2.08-0.87zm31.56 2.64q-0.02 0.07-0.04 0.15 0.02-0.08 0.04-0.15zm0.14-0.44q-0.05 0.12-0.09 0.25 0.04-0.13 0.09-0.25zm0.08-0.22q-0.03 0.06-0.05 0.12 0.02-0.06 0.05-0.12zm0.14-0.3q-0.02 0.04-0.04 0.09 0.02-0.05 0.04-0.09zm0.6-0.83q-0.14 0.15-0.27 0.31l0.11-0.13q0.08-0.09 0.16-0.18zm0.61-0.46q-0.32 0.18-0.59 0.44 0.09-0.08 0.18-0.16 0.13-0.11 0.27-0.2 0.07-0.04 0.14-0.08zm-33.3-0.49q0.08-0.01 0.17-0.01l0.09 0.01q-0.05-0.01-0.09-0.01-0.09 0-0.17 0.01z"/>
|
||||
<g id="Clip-Path" clip-path="url(#cp1)">
|
||||
<g>
|
||||
<g style="opacity: .17">
|
||||
<path class="s1" d="m36.14 2.62h-3.58l9.91-9.03 12.33 1.32 8.09 3.75-7.32 4.73c-1.1-0.2-3.42-0.48-3.91 0-0.6 0.61-0.6 2.26-0.77 3.52-0.13 1.02-1.08 0.94-1.54 0.77l-2.92 0.44-0.88 4.3c-0.24-0.17-0.87-0.85-1.49-2.26-0.77-1.76-1.76-2.31-3.35-2.48-1.28-0.13-3.93 0.79-4.57 1.11z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="s2" d="m36.14 2.62h-3.58l9.91-9.03 12.33 1.32 8.09 3.75-7.32 4.73c-1.1-0.2-3.42-0.48-3.91 0-0.6 0.61-0.6 2.26-0.77 3.52-0.13 1.02-1.08 0.94-1.54 0.77l-2.92 0.44-0.88 4.3c-0.24-0.17-0.87-0.85-1.49-2.26-0.77-1.76-1.76-2.31-3.35-2.48-1.28-0.13-3.93 0.79-4.57 1.11z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path fill-rule="evenodd" class="s3" d="m46.78 5c-0.95-0.45-1.34-1.58-0.89-2.53 0.45-0.95 1.58-1.36 2.52-0.91 0.94 0.44 1.34 1.58 0.89 2.53-0.46 0.95-1.58 1.36-2.52 0.91z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill-rule="evenodd" class="s4" d="m46.99 5.1c-0.83-0.39-1.18-1.37-0.79-2.2 0.4-0.82 1.38-1.17 2.2-0.78 0.83 0.39 1.18 1.37 0.79 2.2-0.39 0.82-1.38 1.17-2.2 0.78z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill-rule="evenodd" class="s5" d="m47.99 1.98c0.9 0.17 1.5 1.03 1.33 1.93-0.16 0.9-1.02 1.49-1.92 1.32-0.9-0.16-1.49-1.02-1.33-1.92 0.17-0.9 1.03-1.49 1.92-1.33z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill-rule="evenodd" class="s6" d="m48.05 2.04c0.9 0.16 1.49 1.02 1.32 1.92-0.16 0.9-1.02 1.49-1.92 1.33-0.9-0.17-1.49-1.03-1.33-1.93 0.17-0.89 1.03-1.49 1.93-1.32z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/images/noise.webp
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
public/placeholders/iphone.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
19
public/placeholders/placeholder-logo.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54.424461 14.339843" fill="#fa0c00">
|
||||
<g>
|
||||
<path d="M 5.996094,0 H 0 v 14.339843 z m 0,0" />
|
||||
<path d="m 10.214844,0 h 5.988281 v 14.339843 z m 0,0" />
|
||||
<path d="m 8.105469,5.285156 3.816406,9.054687 H 9.417969 L 8.277344,11.457031 H 5.484375 Z m 0,0" />
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
d="m 25.985303,9.11922 0.707031,2.027344 c 0.02344,0.05078 0.05859,0.07422 0.121094,0.07422 h 1.359375 c 0.07422,0 0.08594,-0.03516 0.07422,-0.109375 L 25.438428,3.310627 c -0.01172,-0.0625 -0.02344,-0.074219 -0.08594,-0.074219 h -1.6875 c -0.04687,0 -0.07422,0.035156 -0.07422,0.085937 -0.02344,0.410157 -0.05859,0.535157 -0.109375,0.65625 l -2.503906,7.121094 c -0.01172,0.08594 0.01563,0.121094 0.08594,0.121094 h 1.214843 c 0.07422,0 0.109375,-0.02344 0.136719,-0.09766 L 23.082959,9.11922 Z M 23.469678,7.795002 c 0.367187,-1.109375 0.851562,-2.53125 1.046875,-3.34375 h 0.01172 c 0.242188,0.851562 0.8125,2.539062 1.070313,3.34375 z m 0,0" />
|
||||
<path
|
||||
d="m 31.857666,11.341877 c 0.730469,0 1.507813,-0.132813 2.296875,-0.472657 0.0625,-0.02344 0.07422,-0.05078 0.07422,-0.109375 -0.02344,-0.21875 -0.05078,-0.535156 -0.05078,-0.777343 v -7.34375 c 0,-0.046875 0,-0.070313 -0.05859,-0.070313 h -1.324219 c -0.05078,0 -0.07422,0.023438 -0.07422,0.085938 V 5.142658 C 32.513916,5.11922 32.369385,5.107502 32.19751,5.107502 c -2.136719,0 -3.449219,1.410156 -3.449219,3.171875 0,2.042968 1.347656,3.0625 3.109375,3.0625 z m 0.863281,-1.359375 c -0.21875,0.070312 -0.460937,0.097656 -0.707031,0.097656 -0.96875,0 -1.761719,-0.546875 -1.761719,-1.875 0,-1.175781 0.816407,-1.871094 1.898438,-1.871094 0.21875,0 0.410156,0.023438 0.570312,0.085938 z m 0,0" />
|
||||
<path
|
||||
d="m 38.453285,5.107502 c -1.824219,0 -2.953125,1.398437 -2.953125,3.125 0,1.542968 0.898438,3.109375 2.925781,3.109375 1.714844,0 2.917969,-1.261719 2.917969,-3.148438 0,-1.664062 -1.019531,-3.085937 -2.890625,-3.085937 z m -0.07422,1.226562 c 1.03125,0 1.46875,0.886719 1.46875,1.898438 0,1.25 -0.644531,1.871093 -1.394531,1.871093 -0.925781,0 -1.472656,-0.777343 -1.472656,-1.898437 0,-1.152344 0.582031,-1.871094 1.398437,-1.871094 z m 0,0" />
|
||||
<path
|
||||
d="m 42.712968,2.568439 c -0.05078,0 -0.08594,0.023438 -0.08594,0.085938 v 8.3125 c 0,0.03516 0.03516,0.09766 0.08594,0.109375 0.582031,0.179687 1.191406,0.265625 1.820312,0.265625 1.800781,0 3.550781,-1.117188 3.550781,-3.367188 0,-1.628906 -1.117187,-2.867187 -2.867187,-2.867187 -0.402344,0 -0.777344,0.0625 -1.105469,0.171875 L 44.09578,2.666095 c 0,-0.085937 -0.02344,-0.097656 -0.109375,-0.097656 z m 3.875,5.554688 c 0,1.347656 -0.921875,1.980468 -1.917969,1.980468 -0.207031,0 -0.390625,-0.01172 -0.558594,-0.0625 V 6.49422 c 0.191406,-0.074218 0.421875,-0.136718 0.847656,-0.136718 0.960938,0 1.628907,0.609375 1.628907,1.765625 z m 0,0" />
|
||||
<path
|
||||
d="m 53.026024,8.560627 c 0.59375,0 1.082031,-0.011719 1.25,-0.050782 0.0625,-0.011718 0.08594,-0.035156 0.09766,-0.085937 0.03516,-0.132813 0.05078,-0.410156 0.05078,-0.75 0,-1.15625 -0.695312,-2.566406 -2.492187,-2.566406 -1.835938,0 -2.855469,1.496093 -2.855469,3.183593 0,1.496094 0.789063,3.050782 3,3.050782 0.828125,0 1.363281,-0.132813 1.824219,-0.351563 0.04687,-0.02344 0.07031,-0.0625 0.07031,-0.132812 V 9.845783 c 0,-0.058594 -0.03516,-0.070313 -0.07031,-0.046875 -0.460938,0.195312 -0.960938,0.292969 -1.507813,0.292969 -1.238281,0 -1.800781,-0.683594 -1.835937,-1.53125 z m -2.46875,-1.046875 c 0.09766,-0.59375 0.472656,-1.238282 1.3125,-1.238282 0.925781,0 1.167969,0.777344 1.167969,1.128907 0,0.011718 0,0.0625 0,0.097656 -0.05078,0.011719 -0.207032,0.011719 -0.667969,0.011719 z m 0,0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/placeholders/placeholder1.webp
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/placeholders/placeholder2.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
public/placeholders/placeholder3.avif
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/placeholders/placeholder4.webp
Normal file
|
After Width: | Height: | Size: 417 KiB |
BIN
public/placeholders/placeholder5.jpg
Normal file
|
After Width: | Height: | Size: 4.8 MiB |
9769
registry.json
Normal file
162
src/app/blog/page.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactLenis from "lenis/react";
|
||||
import BlogCardOne from "@/components/sections/blog/BlogCardOne";
|
||||
import FooterBaseCard from "@/components/sections/footer/FooterBaseCard";
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleMinimal from "@/components/navbar/NavbarStyleMinimal";
|
||||
|
||||
type BlogPost = {
|
||||
id: string;
|
||||
category: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
imageSrc: string;
|
||||
imageAlt?: string;
|
||||
authorName: string;
|
||||
authorAvatar: string;
|
||||
date: string;
|
||||
onBlogClick?: () => void;
|
||||
};
|
||||
|
||||
const defaultPosts: BlogPost[] = [
|
||||
{
|
||||
id: "1", category: "Design", title: "UX review presentations", excerpt: "How do you create compelling presentations that wow your colleagues and impress your managers?", imageSrc: "/placeholders/placeholder3.avif", imageAlt: "Abstract design with purple and silver tones", authorName: "Olivia Rhye", authorAvatar: "/placeholders/placeholder3.avif", date: "20 Jan 2025", onBlogClick: () => console.log("Blog 1 clicked"),
|
||||
},
|
||||
{
|
||||
id: "2", category: "Development", title: "Building scalable applications", excerpt: "Learn the best practices for building applications that can handle millions of users.", imageSrc: "/placeholders/placeholder4.webp", imageAlt: "Development workspace", authorName: "John Smith", authorAvatar: "/placeholders/placeholder4.webp", date: "18 Jan 2025", onBlogClick: () => console.log("Blog 2 clicked"),
|
||||
},
|
||||
{
|
||||
id: "3", category: "Marketing", title: "Content strategy essentials", excerpt: "Discover how to create a content strategy that drives engagement and conversions.", imageSrc: "/placeholders/placeholder3.avif", imageAlt: "Marketing strategy board", authorName: "Sarah Johnson", authorAvatar: "/placeholders/placeholder3.avif", date: "15 Jan 2025", onBlogClick: () => console.log("Blog 3 clicked"),
|
||||
},
|
||||
{
|
||||
id: "4", category: "Product", title: "Product management 101", excerpt: "Everything you need to know to become an effective product manager in 2025.", imageSrc: "/placeholders/placeholder4.webp", imageAlt: "Product planning session", authorName: "Mike Davis", authorAvatar: "/placeholders/placeholder4.webp", date: "12 Jan 2025", onBlogClick: () => console.log("Blog 4 clicked"),
|
||||
},
|
||||
];
|
||||
|
||||
export default function BlogPage() {
|
||||
const [posts, setPosts] = useState<BlogPost[]>(defaultPosts);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPosts = async () => {
|
||||
try {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID;
|
||||
|
||||
if (!apiUrl || !projectId) {
|
||||
console.warn("NEXT_PUBLIC_API_URL or NEXT_PUBLIC_PROJECT_ID not configured, using default posts");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${apiUrl}/posts/${projectId}?status=published`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET", headers: {
|
||||
"Content-Type": "application/json"},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const resp = await response.json();
|
||||
const data = resp.data;
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
const mappedPosts = data.map((post: any) => ({
|
||||
id: post.id || String(Math.random()),
|
||||
category: post.category || "General", title: post.title || "Untitled", excerpt: post.excerpt || post.content.slice(0, 30) || "", imageSrc: post.imageUrl || "/placeholders/placeholder3.avif", imageAlt: post.imageAlt || post.title || "", authorName: post.author?.name || "Anonymous", authorAvatar: post.author?.avatar || "/placeholders/placeholder3.avif", date: post.date || post.createdAt || new Date().toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }),
|
||||
onBlogClick: () => console.log(`Blog ${post.id} clicked`),
|
||||
}));
|
||||
setPosts(mappedPosts);
|
||||
}
|
||||
} else {
|
||||
console.warn(`API request failed with status ${response.status}, using default posts`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching posts:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPosts();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="elastic-effect"
|
||||
defaultTextAnimation="background-highlight"
|
||||
borderRadius="rounded"
|
||||
contentWidth="mediumLarge"
|
||||
sizing="mediumLargeSizeLargeTitles"
|
||||
background="aurora"
|
||||
cardStyle="solid"
|
||||
primaryButtonStyle="flat"
|
||||
secondaryButtonStyle="layered"
|
||||
headingFontWeight="extrabold"
|
||||
>
|
||||
<ReactLenis root>
|
||||
<div className="min-h-screen bg-background">
|
||||
<NavbarStyleMinimal
|
||||
brandName="SpeedReview"
|
||||
button={{ text: "Explore", href: "#products" }}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="w-content-width mx-auto py-20 text-center">
|
||||
<p className="text-foreground">Loading posts...</p>
|
||||
</div>
|
||||
) : (
|
||||
<BlogCardOne
|
||||
blogs={posts}
|
||||
title="Latest Automotive Reviews"
|
||||
description="Discover expert insights and analysis on the latest vehicles in our comprehensive automotive reviews"
|
||||
textboxLayout="default"
|
||||
useInvertedBackground="noInvert"
|
||||
animationType="slide-up"
|
||||
carouselMode="buttons"
|
||||
/>
|
||||
)}
|
||||
|
||||
<FooterBaseCard
|
||||
logoText="SpeedReview"
|
||||
copyrightText="© 2025 SpeedReview. All rights reserved. Premium automotive reviews and analysis."
|
||||
columns={[
|
||||
{
|
||||
title: "Reviews", items: [
|
||||
{ label: "All Reviews", href: "#products" },
|
||||
{ label: "Sports Cars", href: "#products" },
|
||||
{ label: "Luxury Vehicles", href: "#products" },
|
||||
{ label: "Performance Analysis", href: "#features" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Company", items: [
|
||||
{ label: "About Us", href: "#about" },
|
||||
{ label: "Our Process", href: "#features" },
|
||||
{ label: "Contact", href: "#contact" },
|
||||
{ label: "FAQ", href: "#faq" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Resources", items: [
|
||||
{ label: "Buying Guide", href: "#" },
|
||||
{ label: "Maintenance Tips", href: "#" },
|
||||
{ label: "Industry News", href: "#" },
|
||||
{ label: "Newsletter", href: "#" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Legal", items: [
|
||||
{ label: "Privacy Policy", href: "#" },
|
||||
{ label: "Terms of Service", href: "#" },
|
||||
{ label: "Disclaimer", href: "#" },
|
||||
{ label: "Cookies", href: "#" },
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</ReactLenis>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
BIN
src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
595
src/app/globals.css
Normal file
@@ -0,0 +1,595 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
/* Base units */
|
||||
/* --vw is set by ThemeProvider */
|
||||
|
||||
/* --background: #fafffb;;
|
||||
--card: #f7fffa;;
|
||||
--foreground: #001a0a;;
|
||||
--primary-cta: #0a7039;;
|
||||
--secondary-cta: #ffffff;;
|
||||
--accent: #a8d9be;;
|
||||
--background-accent: #6bbf8e;; */
|
||||
|
||||
--background: #fafffb;;
|
||||
--card: #f7fffa;;
|
||||
--foreground: #001a0a;;
|
||||
--primary-cta: #0a7039;;
|
||||
--secondary-cta: #ffffff;;
|
||||
--accent: #a8d9be;;
|
||||
--background-accent: #6bbf8e;;
|
||||
|
||||
/* text sizing - set by ThemeProvider */
|
||||
/* --text-2xs: clamp(0.465rem, 0.62vw, 0.62rem);
|
||||
--text-xs: clamp(0.54rem, 0.72vw, 0.72rem);
|
||||
--text-sm: clamp(0.615rem, 0.82vw, 0.82rem);
|
||||
--text-base: clamp(0.69rem, 0.92vw, 0.92rem);
|
||||
--text-lg: clamp(0.75rem, 1vw, 1rem);
|
||||
--text-xl: clamp(0.825rem, 1.1vw, 1.1rem);
|
||||
--text-2xl: clamp(0.975rem, 1.3vw, 1.3rem);
|
||||
--text-3xl: clamp(1.2rem, 1.6vw, 1.6rem);
|
||||
--text-4xl: clamp(1.5rem, 2vw, 2rem);
|
||||
--text-5xl: clamp(2.025rem, 2.75vw, 2.75rem);
|
||||
--text-6xl: clamp(2.475rem, 3.3vw, 3.3rem);
|
||||
--text-7xl: clamp(3rem, 4vw, 4rem);
|
||||
--text-8xl: clamp(3.5rem, 4.5vw, 4.5rem);
|
||||
--text-9xl: clamp(5.25rem, 7vw, 7rem); */
|
||||
|
||||
/* Base spacing units */
|
||||
--vw-0_25: calc(var(--vw) * 0.25);
|
||||
--vw-0_5: calc(var(--vw) * 0.5);
|
||||
--vw-0_625: calc(var(--vw) * 0.625);
|
||||
--vw-0_75: calc(var(--vw) * 0.75);
|
||||
--vw-1: calc(var(--vw) * 1);
|
||||
--vw-1_25: calc(var(--vw) * 1.25);
|
||||
--vw-1_5: calc(var(--vw) * 1.5);
|
||||
--vw-1_75: calc(var(--vw) * 1.75);
|
||||
--vw-2: calc(var(--vw) * 2);
|
||||
--vw-2_25: calc(var(--vw) * 2.25);
|
||||
--vw-2_5: calc(var(--vw) * 2.5);
|
||||
--vw-2_75: calc(var(--vw) * 2.75);
|
||||
--vw-3: calc(var(--vw) * 3);
|
||||
|
||||
/* width */
|
||||
--width-5: clamp(4rem, 5vw, 6rem);
|
||||
--width-7_5: clamp(5.625rem, 7.5vw, 7.5rem);
|
||||
--width-10: clamp(7.5rem, 10vw, 10rem);
|
||||
--width-12_5: clamp(9.375rem, 12.5vw, 12.5rem);
|
||||
--width-15: clamp(11.25rem, 15vw, 15rem);
|
||||
--width-17: clamp(12.75rem, 17vw, 17rem);
|
||||
--width-17_5: clamp(13.125rem, 17.5vw, 17.5rem);
|
||||
--width-20: clamp(15rem, 20vw, 20rem);
|
||||
--width-21: clamp(15.75rem, 21vw, 21rem);
|
||||
--width-22_5: clamp(16.875rem, 22.5vw, 22.5rem);
|
||||
--width-25: clamp(18.75rem, 25vw, 25rem);
|
||||
--width-26: clamp(19.5rem, 26vw, 26rem);
|
||||
--width-27_5: clamp(20.625rem, 27.5vw, 27.5rem);
|
||||
--width-30: clamp(22.5rem, 30vw, 30rem);
|
||||
--width-32_5: clamp(24.375rem, 32.5vw, 32.5rem);
|
||||
--width-35: clamp(26.25rem, 35vw, 35rem);
|
||||
--width-37_5: clamp(28.125rem, 37.5vw, 37.5rem);
|
||||
--width-40: clamp(30rem, 40vw, 40rem);
|
||||
--width-42_5: clamp(31.875rem, 42.5vw, 42.5rem);
|
||||
--width-45: clamp(33.75rem, 45vw, 45rem);
|
||||
--width-47_5: clamp(35.625rem, 47.5vw, 47.5rem);
|
||||
--width-50: clamp(37.5rem, 50vw, 50rem);
|
||||
--width-52_5: clamp(39.375rem, 52.5vw, 52.5rem);
|
||||
--width-55: clamp(41.25rem, 55vw, 55rem);
|
||||
--width-57_5: clamp(43.125rem, 57.5vw, 57.5rem);
|
||||
--width-60: clamp(45rem, 60vw, 60rem);
|
||||
--width-62_5: clamp(46.875rem, 62.5vw, 62.5rem);
|
||||
--width-65: clamp(48.75rem, 65vw, 65rem);
|
||||
--width-67_5: clamp(50.625rem, 67.5vw, 67.5rem);
|
||||
--width-70: clamp(52.5rem, 70vw, 70rem);
|
||||
--width-72_5: clamp(54.375rem, 72.5vw, 72.5rem);
|
||||
--width-75: clamp(56.25rem, 75vw, 75rem);
|
||||
--width-77_5: clamp(58.125rem, 77.5vw, 77.5rem);
|
||||
--width-80: clamp(60rem, 80vw, 80rem);
|
||||
--width-82_5: clamp(61.875rem, 82.5vw, 82.5rem);
|
||||
--width-85: clamp(63.75rem, 85vw, 85rem);
|
||||
--width-87_5: clamp(65.625rem, 87.5vw, 87.5rem);
|
||||
--width-90: clamp(67.5rem, 90vw, 90rem);
|
||||
--width-92_5: clamp(69.375rem, 92.5vw, 92.5rem);
|
||||
--width-95: clamp(71.25rem, 95vw, 95rem);
|
||||
--width-97_5: clamp(73.125rem, 97.5vw, 97.5rem);
|
||||
--width-100: clamp(75rem, 100vw, 100rem);
|
||||
/* --width-content-width and --width-content-width-expanded are set by ThemeProvider */
|
||||
--width-carousel-padding: calc((100vw - var(--width-content-width)) / 2 + 1px - var(--vw-1_5));
|
||||
--width-carousel-padding-controls: calc((100vw - var(--width-content-width)) / 2 + 1px);
|
||||
--width-carousel-padding-expanded: calc((var(--width-content-width-expanded) - var(--width-content-width)) / 2 + 1px - var(--vw-1_5));
|
||||
--width-carousel-padding-controls-expanded: calc((var(--width-content-width-expanded) - var(--width-content-width)) / 2 + 1px);
|
||||
--width-carousel-item-3: calc(var(--width-content-width) / 3 - var(--vw-1_5) / 3 * 2);
|
||||
--width-carousel-item-4: calc(var(--width-content-width) / 4 - var(--vw-1_5) / 4 * 3);
|
||||
--width-x-padding-mask-fade: clamp(1.5rem, 4vw, 4rem);
|
||||
|
||||
--height-4: 1rem;
|
||||
--height-5: 1.25rem;
|
||||
--height-6: 1.5rem;
|
||||
--height-7: 1.75rem;
|
||||
--height-8: 2rem;
|
||||
--height-9: 2.25rem;
|
||||
--height-10: 2.5rem;
|
||||
--height-11: 2.75rem;
|
||||
--height-12: 3rem;
|
||||
--height-30: 7.5rem;
|
||||
--height-90: 22.5rem;
|
||||
--height-100: 25rem;
|
||||
--height-110: 27.5rem;
|
||||
--height-120: 30rem;
|
||||
--height-130: 32.5rem;
|
||||
--height-140: 35rem;
|
||||
--height-150: 37.5rem;
|
||||
|
||||
/* hero page padding */
|
||||
--padding-hero-page-padding-half: calc((var(--height-10) + var(--vw-1_5) + var(--vw-1_5) + var(--height-10)) / 2);
|
||||
--padding-hero-page-padding: calc(var(--height-10) + var(--vw-1_5) + var(--vw-1_5) + var(--height-10));
|
||||
--padding-hero-page-padding-1_5: calc(1.5 * (var(--height-10) + var(--vw-1_5) + var(--vw-1_5) + var(--height-10)));
|
||||
--padding-hero-page-padding-double: calc(2 * (var(--height-10) + var(--vw-1_5) + var(--vw-1_5) + var(--height-10)));
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
:root {
|
||||
/* --vw and text sizing are set by ThemeProvider */
|
||||
/* --vw: 3vw;
|
||||
|
||||
--text-2xs: 2.5vw;
|
||||
--text-xs: 2.75vw;
|
||||
--text-sm: 3vw;
|
||||
--text-base: 3.25vw;
|
||||
--text-lg: 3.5vw;
|
||||
--text-xl: 4.25vw;
|
||||
--text-2xl: 5vw;
|
||||
--text-3xl: 6vw;
|
||||
--text-4xl: 7vw;
|
||||
--text-5xl: 7.5vw;
|
||||
--text-6xl: 8.5vw;
|
||||
--text-7xl: 10vw;
|
||||
--text-8xl: 12vw;
|
||||
--text-9xl: 14vw; */
|
||||
|
||||
--width-5: 5vw;
|
||||
--width-7_5: 7.5vw;
|
||||
--width-10: 10vw;
|
||||
--width-12_5: 12.5vw;
|
||||
--width-15: 15vw;
|
||||
--width-17_5: 17.5vw;
|
||||
--width-20: 20vw;
|
||||
--width-22_5: 22.5vw;
|
||||
--width-25: 25vw;
|
||||
--width-27_5: 27.5vw;
|
||||
--width-30: 30vw;
|
||||
--width-32_5: 32.5vw;
|
||||
--width-35: 35vw;
|
||||
--width-37_5: 37.5vw;
|
||||
--width-40: 40vw;
|
||||
--width-42_5: 42.5vw;
|
||||
--width-45: 45vw;
|
||||
--width-47_5: 47.5vw;
|
||||
--width-50: 50vw;
|
||||
--width-52_5: 52.5vw;
|
||||
--width-55: 55vw;
|
||||
--width-57_5: 57.5vw;
|
||||
--width-60: 60vw;
|
||||
--width-62_5: 62.5vw;
|
||||
--width-65: 65vw;
|
||||
--width-67_5: 67.5vw;
|
||||
--width-70: 70vw;
|
||||
--width-72_5: 72.5vw;
|
||||
--width-75: 75vw;
|
||||
--width-77_5: 77.5vw;
|
||||
--width-80: 80vw;
|
||||
--width-82_5: 82.5vw;
|
||||
--width-85: 85vw;
|
||||
--width-87_5: 87.5vw;
|
||||
--width-90: 90vw;
|
||||
--width-92_5: 92.5vw;
|
||||
--width-95: 95vw;
|
||||
--width-97_5: 97.5vw;
|
||||
--width-100: 100vw;
|
||||
/* --width-content-width and --width-content-width-expanded are set by ThemeProvider */
|
||||
--width-carousel-padding: calc((100vw - var(--width-content-width)) / 2 + 1px - var(--vw-1_5));
|
||||
--width-carousel-padding-controls: calc((100vw - var(--width-content-width)) / 2 + 1px);
|
||||
--width-carousel-padding-expanded: calc((var(--width-content-width-expanded) - var(--width-content-width)) / 2 + 1px - var(--vw-1_5));
|
||||
--width-carousel-padding-controls-expanded: calc((var(--width-content-width-expanded) - var(--width-content-width)) / 2 + 1px);
|
||||
--width-carousel-item-3: var(--width-content-width);
|
||||
--width-carousel-item-4: var(--width-content-width);
|
||||
--width-x-padding-mask-fade: 10vw;
|
||||
|
||||
--height-4: 3.5vw;
|
||||
--height-5: 4.5vw;
|
||||
--height-6: 5.5vw;
|
||||
--height-7: 6.5vw;
|
||||
--height-8: 7.5vw;
|
||||
--height-9: 8.5vw;
|
||||
--height-10: 9vw;
|
||||
--height-11: 10vw;
|
||||
--height-12: 11vw;
|
||||
--height-30: 25vw;
|
||||
--height-90: 81vw;
|
||||
--height-100: 90vw;
|
||||
--height-110: 99vw;
|
||||
--height-120: 108vw;
|
||||
--height-130: 117vw;
|
||||
--height-140: 126vw;
|
||||
--height-150: 135vw;
|
||||
}
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-primary-cta: var(--primary-cta);
|
||||
--color-secondary-cta: var(--secondary-cta);
|
||||
--color-accent: var(--accent);
|
||||
--color-background-accent: var(--background-accent);
|
||||
|
||||
/* theme border radius */
|
||||
--radius-theme: var(--theme-border-radius);
|
||||
--radius-theme-capped: var(--theme-border-radius-capped);
|
||||
|
||||
/* text */
|
||||
--text-2xs: var(--text-2xs);
|
||||
--text-xs: var(--text-xs);
|
||||
--text-sm: var(--text-sm);
|
||||
--text-base: var(--text-base);
|
||||
--text-lg: var(--text-lg);
|
||||
--text-xl: var(--text-xl);
|
||||
--text-2xl: var(--text-2xl);
|
||||
--text-3xl: var(--text-3xl);
|
||||
--text-4xl: var(--text-4xl);
|
||||
--text-5xl: var(--text-5xl);
|
||||
--text-6xl: var(--text-6xl);
|
||||
--text-7xl: var(--text-7xl);
|
||||
--text-8xl: var(--text-8xl);
|
||||
--text-9xl: var(--text-9xl);
|
||||
|
||||
/* height */
|
||||
--height-4: var(--height-4);
|
||||
--height-5: var(--height-5);
|
||||
--height-6: var(--height-6);
|
||||
--height-7: var(--height-7);
|
||||
--height-8: var(--height-8);
|
||||
--height-9: var(--height-9);
|
||||
--height-11: var(--height-11);
|
||||
--height-12: var(--height-12);
|
||||
|
||||
--height-10: var(--height-10);
|
||||
--height-30: var(--height-30);
|
||||
--height-90: var(--height-90);
|
||||
--height-100: var(--height-100);
|
||||
--height-110: var(--height-110);
|
||||
--height-120: var(--height-120);
|
||||
--height-130: var(--height-130);
|
||||
--height-140: var(--height-140);
|
||||
--height-150: var(--height-150);
|
||||
|
||||
--height-page-padding: calc(2.25rem+var(--vw-1_5)+var(--vw-1_5));
|
||||
|
||||
/* width */
|
||||
--width-5: var(--width-5);
|
||||
--width-7_5: var(--width-7_5);
|
||||
--width-10: var(--width-10);
|
||||
--width-12_5: var(--width-12_5);
|
||||
--width-15: var(--width-15);
|
||||
--width-17: var(--width-17);
|
||||
--width-17_5: var(--width-17_5);
|
||||
--width-20: var(--width-20);
|
||||
--width-21: var(--width-21);
|
||||
--width-22_5: var(--width-22_5);
|
||||
--width-25: var(--width-25);
|
||||
--width-26: var(--width-26);
|
||||
--width-27_5: var(--width-27_5);
|
||||
--width-30: var(--width-30);
|
||||
--width-32_5: var(--width-32_5);
|
||||
--width-35: var(--width-35);
|
||||
--width-37_5: var(--width-37_5);
|
||||
--width-40: var(--width-40);
|
||||
--width-42_5: var(--width-42_5);
|
||||
--width-45: var(--width-45);
|
||||
--width-47_5: var(--width-47_5);
|
||||
--width-50: var(--width-50);
|
||||
--width-52_5: var(--width-52_5);
|
||||
--width-55: var(--width-55);
|
||||
--width-57_5: var(--width-57_5);
|
||||
--width-60: var(--width-60);
|
||||
--width-62_5: var(--width-62_5);
|
||||
--width-65: var(--width-65);
|
||||
--width-67_5: var(--width-67_5);
|
||||
--width-70: var(--width-70);
|
||||
--width-72_5: var(--width-72_5);
|
||||
--width-75: var(--width-75);
|
||||
--width-77_5: var(--width-77_5);
|
||||
--width-80: var(--width-80);
|
||||
--width-82_5: var(--width-82_5);
|
||||
--width-85: var(--width-85);
|
||||
--width-87_5: var(--width-87_5);
|
||||
--width-90: var(--width-90);
|
||||
--width-92_5: var(--width-92_5);
|
||||
--width-95: var(--width-95);
|
||||
--width-97_5: var(--width-97_5);
|
||||
--width-100: var(--width-100);
|
||||
--width-content-width: var(--width-content-width);
|
||||
--width-carousel-padding: var(--width-carousel-padding);
|
||||
--width-carousel-padding-controls: var(--width-carousel-padding-controls);
|
||||
--width-carousel-padding-expanded: var(--width-carousel-padding-expanded);
|
||||
--width-carousel-padding-controls-expanded: var(--width-carousel-padding-controls-expanded);
|
||||
--width-carousel-item-3: var(--width-carousel-item-3);
|
||||
--width-carousel-item-4: var(--width-carousel-item-4);
|
||||
--width-x-padding-mask-fade: var(--width-x-padding-mask-fade);
|
||||
--width-content-width-expanded: var(--width-content-width-expanded);
|
||||
|
||||
/* gap */
|
||||
--spacing-1: var(--vw-0_25);
|
||||
--spacing-2: var(--vw-0_5);
|
||||
--spacing-3: var(--vw-0_75);
|
||||
--spacing-4: var(--vw-1);
|
||||
--spacing-5: var(--vw-1_25);
|
||||
--spacing-6: var(--vw-1_5);
|
||||
--spacing-7: var(--vw-1_75);
|
||||
--spacing-8: var(--vw-2);
|
||||
|
||||
--spacing-x-1: var(--vw-0_25);
|
||||
--spacing-x-2: var(--vw-0_5);
|
||||
--spacing-x-3: var(--vw-0_75);
|
||||
--spacing-x-4: var(--vw-1);
|
||||
--spacing-x-5: var(--vw-1_25);
|
||||
--spacing-x-6: var(--vw-1_5);
|
||||
|
||||
/* border radius */
|
||||
--radius-none: 0;
|
||||
--radius-sm: var(--vw-0_5);
|
||||
--radius: var(--vw-0_75);
|
||||
--radius-md: var(--vw-1);
|
||||
--radius-lg: var(--vw-1_25);
|
||||
--radius-xl: var(--vw-1_75);
|
||||
--radius-full: 999px;
|
||||
|
||||
/* padding */
|
||||
--padding-1: var(--vw-0_25);
|
||||
--padding-2: var(--vw-0_5);
|
||||
--padding-2.5: var(--vw-0_625);
|
||||
--padding-3: var(--vw-0_75);
|
||||
--padding-4: var(--vw-1);
|
||||
--padding-5: var(--vw-1_25);
|
||||
--padding-6: var(--vw-1_5);
|
||||
--padding-7: var(--vw-1_75);
|
||||
--padding-8: var(--vw-2);
|
||||
|
||||
--padding-x-1: var(--vw-0_25);
|
||||
--padding-x-2: var(--vw-0_5);
|
||||
--padding-x-3: var(--vw-0_75);
|
||||
--padding-x-4: var(--vw-1);
|
||||
--padding-x-5: var(--vw-1_25);
|
||||
--padding-x-6: var(--vw-1_5);
|
||||
--padding-x-7: var(--vw-1_75);
|
||||
--padding-x-8: var(--vw-2);
|
||||
|
||||
--padding-hero-page-padding-half: var(--padding-hero-page-padding-half);
|
||||
--padding-hero-page-padding: var(--padding-hero-page-padding);
|
||||
--padding-hero-page-padding-1_5: var(--padding-hero-page-padding-1_5);
|
||||
--padding-hero-page-padding-double: var(--padding-hero-page-padding-double);
|
||||
|
||||
/* margin */
|
||||
--margin-1: var(--vw-0_25);
|
||||
--margin-2: var(--vw-0_5);
|
||||
--margin-3: var(--vw-0_75);
|
||||
--margin-4: var(--vw-1);
|
||||
--margin-5: var(--vw-1_25);
|
||||
--margin-6: var(--vw-1_5);
|
||||
--margin-7: var(--vw-1_75);
|
||||
--margin-8: var(--vw-2);
|
||||
|
||||
--margin-x-1: var(--vw-0_25);
|
||||
--margin-x-2: var(--vw-0_5);
|
||||
--margin-x-3: var(--vw-0_75);
|
||||
--margin-x-4: var(--vw-1);
|
||||
--margin-x-5: var(--vw-1_25);
|
||||
--margin-x-6: var(--vw-1_5);
|
||||
--margin-x-7: var(--vw-1_75);
|
||||
--margin-x-8: var(--vw-2);
|
||||
}
|
||||
|
||||
@layer components {}
|
||||
|
||||
@layer utilities {
|
||||
|
||||
/* Card, primary-button, and secondary-button styles are now dynamically injected via ThemeProvider */
|
||||
|
||||
/* .card {
|
||||
@apply backdrop-blur-sm bg-gradient-to-br from-card/80 to-card/40 shadow-sm border border-card;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
@apply bg-gradient-to-b from-primary-cta/83 to-primary-cta;
|
||||
box-shadow:
|
||||
color-mix(in srgb, var(--color-background) 25%, transparent) 0px 1px 1px 0px inset,
|
||||
color-mix(in srgb, var(--color-primary-cta) 15%, transparent) 3px 3px 3px 0px;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
@apply backdrop-blur-sm bg-gradient-to-br from-secondary-cta/80 to-secondary-cta shadow-sm border border-secondary-cta;
|
||||
} */
|
||||
|
||||
.tag-card {
|
||||
@apply backdrop-blur-sm bg-gradient-to-br from-card/80 to-card/40 shadow-sm border border-card;
|
||||
}
|
||||
|
||||
.mask-padding-x {
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0%, black var(--width-x-padding-mask-fade), black calc(100% - var(--width-x-padding-mask-fade)), transparent 100%);
|
||||
mask-image: linear-gradient(to right, transparent 0%, black var(--width-x-padding-mask-fade), black calc(100% - var(--width-x-padding-mask-fade)), transparent 100%);
|
||||
}
|
||||
|
||||
.mask-fade-bottom {
|
||||
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 100%);
|
||||
}
|
||||
|
||||
.mask-fade-y {
|
||||
mask-image: linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
black var(--vw-1_5),
|
||||
black calc(100% - var(--vw-1_5)),
|
||||
transparent 100%);
|
||||
}
|
||||
|
||||
.mask-fade-bottom-large {
|
||||
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 75%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 75%, transparent 100%);
|
||||
}
|
||||
|
||||
.mask-fade-bottom-long {
|
||||
-webkit-mask-image: linear-gradient(to bottom, black 0%, black 5%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 5%, transparent 100%);
|
||||
}
|
||||
|
||||
.mask-fade-top-long {
|
||||
-webkit-mask-image: linear-gradient(to top, black 0%, black 5%, transparent 100%);
|
||||
mask-image: linear-gradient(to top, black 0%, black 5%, transparent 100%);
|
||||
}
|
||||
|
||||
.mask-fade-xy {
|
||||
-webkit-mask-image:
|
||||
linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%),
|
||||
linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
|
||||
mask-image:
|
||||
linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%),
|
||||
linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);
|
||||
-webkit-mask-composite: source-in;
|
||||
mask-composite: intersect;
|
||||
}
|
||||
|
||||
/* ANIMATION */
|
||||
|
||||
.animation-container {
|
||||
animation:
|
||||
fadeInOpacity 0.8s ease-in-out forwards,
|
||||
fadeInTranslate 0.6s forwards;
|
||||
}
|
||||
|
||||
.animation-container-fade {
|
||||
animation: fadeInOpacity 0.8s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInOpacity {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInTranslate {
|
||||
from {
|
||||
transform: translateY(0.75vh);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0vh);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aurora {
|
||||
from {
|
||||
background-position: 50% 50%, 50% 50%;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 350% 50%, 350% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin-slow {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin-reverse {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin-slow {
|
||||
animation: spin-slow 15s linear infinite;
|
||||
}
|
||||
|
||||
.animate-spin-reverse {
|
||||
animation: spin-reverse 10s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes marquee-vertical {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-marquee-vertical {
|
||||
animation: marquee-vertical 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes orbit {
|
||||
from {
|
||||
transform: rotate(var(--initial-position, 0deg)) translateX(var(--translate-position, 120px)) rotate(calc(-1 * var(--initial-position, 0deg)));
|
||||
}
|
||||
to {
|
||||
transform: rotate(calc(var(--initial-position, 0deg) + 360deg)) translateX(var(--translate-position, 120px)) rotate(calc(-1 * (var(--initial-position, 0deg) + 360deg)));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes map-dot-pulse {
|
||||
0%, 100% {
|
||||
transform: scale(0.4);
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.4);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 1) rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
html {
|
||||
overscroll-behavior: none;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-mulish), sans-serif;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overscroll-behavior: none;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--font-mulish), sans-serif;
|
||||
}
|
||||
1267
src/app/layout.tsx
Normal file
330
src/app/page.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
"use client"
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleMinimal from '@/components/navbar/NavbarStyleMinimal';
|
||||
import HeroBillboardScroll from '@/components/sections/hero/HeroBillboardScroll';
|
||||
import ProductCardFour from '@/components/sections/product/ProductCardFour';
|
||||
import MetricCardOne from '@/components/sections/metrics/MetricCardOne';
|
||||
import TestimonialCardThirteen from '@/components/sections/testimonial/TestimonialCardThirteen';
|
||||
import FeatureProcessSteps from '@/components/sections/feature/FeatureProcessSteps';
|
||||
import MetricSplitMediaAbout from '@/components/sections/about/MetricSplitMediaAbout';
|
||||
import FaqSplitText from '@/components/sections/faq/FaqSplitText';
|
||||
import ContactText from '@/components/sections/contact/ContactText';
|
||||
import FooterBaseCard from '@/components/sections/footer/FooterBaseCard';
|
||||
import { Zap, Sparkles, Flame, Gauge, Star, Quote, CheckCircle, Trophy } from 'lucide-react';
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="elastic-effect"
|
||||
defaultTextAnimation="background-highlight"
|
||||
borderRadius="rounded"
|
||||
contentWidth="mediumLarge"
|
||||
sizing="mediumLargeSizeLargeTitles"
|
||||
background="aurora"
|
||||
cardStyle="solid"
|
||||
primaryButtonStyle="flat"
|
||||
secondaryButtonStyle="layered"
|
||||
headingFontWeight="extrabold"
|
||||
>
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleMinimal
|
||||
brandName="SpeedReview"
|
||||
button={{
|
||||
text: "Explore", href: "#products"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="hero" data-section="hero">
|
||||
<HeroBillboardScroll
|
||||
title="Experience Ultimate Performance"
|
||||
description="Explore six of the world's most legendary sports cars and luxury vehicles. Detailed reviews, specifications, and expert analysis of automotive excellence."
|
||||
tag="Premium Car Reviews"
|
||||
tagIcon={Zap}
|
||||
background={{
|
||||
variant: "aurora"
|
||||
}}
|
||||
imageSrc="https://img.b2bpic.net/free-photo/car-beautiful-sitting-embrace-carlo_1304-2608.jpg"
|
||||
imageAlt="Lamborghini supercar hero showcase"
|
||||
buttons={[
|
||||
{
|
||||
text: "View All Cars", href: "#products"
|
||||
},
|
||||
{
|
||||
text: "Read Reviews", href: "#reviews"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="products" data-section="products">
|
||||
<ProductCardFour
|
||||
title="Featured Sports Cars"
|
||||
description="Discover our curated collection of six extraordinary vehicles, each a masterpiece of engineering and design."
|
||||
tag="Automotive Excellence"
|
||||
tagIcon={Sparkles}
|
||||
products={[
|
||||
{
|
||||
id: "1", name: "Lamborghini Huracán", price: "$238,000+", variant: "V10 Supercar Multiple Configurations", imageSrc: "https://img.b2bpic.net/free-photo/young-curly-sexy-woman-leather-jacket-against-red-muscle-car-street_627829-6957.jpg", imageAlt: "Lamborghini Huracán sports car", isFavorited: false
|
||||
},
|
||||
{
|
||||
id: "2", name: "Lamborghini Huracán STO SVJ", price: "$353,000+", variant: "Track-Focused V10 Limited Edition", imageSrc: "https://img.b2bpic.net/free-photo/sport-cars-parade-race-highway_114579-4052.jpg", imageAlt: "Lamborghini SVJ supercar", isFavorited: false
|
||||
},
|
||||
{
|
||||
id: "3", name: "Ferrari 812 Superfast", price: "$335,000+", variant: "V12 Grand Tourer Handcrafted", imageSrc: "https://img.b2bpic.net/free-photo/green-yellow-red-purple-violet-sedan-sport-cars-standing-dark-space_114579-1159.jpg", imageAlt: "Ferrari 812 Superfast luxury car", isFavorited: false
|
||||
},
|
||||
{
|
||||
id: "4", name: "Mercedes-Maybach S-Class", price: "$180,000+", variant: "Luxury Sedan Ultra-Premium", imageSrc: "https://img.b2bpic.net/free-photo/black-sedan-wet-highway-rain-rainy-drive_169016-69903.jpg", imageAlt: "Mercedes-Maybach luxury sedan", isFavorited: false
|
||||
},
|
||||
{
|
||||
id: "5", name: "Rolls-Royce Cullinan", price: "$330,000+", variant: "Ultra-Luxury SUV Bespoke", imageSrc: "https://img.b2bpic.net/free-photo/white-car-turning-city-street-modern-urban-motion_169016-69787.jpg", imageAlt: "Rolls-Royce Cullinan luxury SUV", isFavorited: false
|
||||
},
|
||||
{
|
||||
id: "6", name: "Ferrari 458 Italia", price: "$220,000+", variant: "Mid-Engine Supercar Modern Classic", imageSrc: "https://img.b2bpic.net/free-photo/well-dressed-beautiful-woman-brutal-male-suit-near-luxury-car-outdoors-against-cityscape_613910-5973.jpg", imageAlt: "Ferrari 458 Italia sports car", isFavorited: false
|
||||
}
|
||||
]}
|
||||
gridVariant="bento-grid"
|
||||
animationType="scale-rotate"
|
||||
textboxLayout="default"
|
||||
useInvertedBackground="noInvert"
|
||||
buttons={[
|
||||
{
|
||||
text: "View Detailed Reviews", href: "#reviews"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="metrics" data-section="metrics">
|
||||
<MetricCardOne
|
||||
title="Performance Excellence"
|
||||
description="Key statistics showcasing the incredible capabilities of our featured vehicles"
|
||||
metrics={[
|
||||
{
|
||||
id: "1", value: "350+", title: "mph top speed", description: "Maximum velocity achieved by our supercars", icon: Zap
|
||||
},
|
||||
{
|
||||
id: "2", value: "0-60", title: "in 2.5 seconds", description: "Blazing acceleration on our fastest models", icon: Flame
|
||||
},
|
||||
{
|
||||
id: "3", value: "1000+", title: "combined horsepower", description: "Total power across all featured vehicles", icon: Gauge
|
||||
},
|
||||
{
|
||||
id: "4", value: "6", title: "legendary cars", description: "Handpicked automotive masterpieces", icon: Star
|
||||
}
|
||||
]}
|
||||
gridVariant="uniform-all-items-equal"
|
||||
animationType="depth-3d"
|
||||
textboxLayout="default"
|
||||
useInvertedBackground="invertDefault"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="reviews" data-section="reviews">
|
||||
<TestimonialCardThirteen
|
||||
title="Expert Reviews & Ratings"
|
||||
description="Hear from professional drivers, automotive journalists, and car enthusiasts about their experience with these machines."
|
||||
tag="Customer Testimonials"
|
||||
tagIcon={Quote}
|
||||
showRating={true}
|
||||
textboxLayout="default"
|
||||
useInvertedBackground="noInvert"
|
||||
animationType="slide-up"
|
||||
testimonials={[
|
||||
{
|
||||
id: "1", name: "Marco Rossini", handle: "@marco_racing", testimonial: "The Lamborghini Huracán delivers raw, unfiltered performance. Absolutely incredible acceleration and handling precision on the track.", rating: 5,
|
||||
imageSrc: "https://img.b2bpic.net/free-photo/portrait-mechanic-workshop_329181-11869.jpg"
|
||||
},
|
||||
{
|
||||
id: "2", name: "Sarah Chen", handle: "@automotive_expert", testimonial: "The Ferrari 812 Superfast is a masterpiece of Italian engineering. Every detail screams craftsmanship and pure automotive passion.", rating: 5,
|
||||
imageSrc: "https://img.b2bpic.net/free-photo/he-is-confident-small-business-owner_637285-9324.jpg"
|
||||
},
|
||||
{
|
||||
id: "3", name: "James Mitchell", handle: "@luxury_collector", testimonial: "The Rolls-Royce Cullinan redefines ultra-luxury. It's not just a vehicle; it's a statement of elegance and prestige.", rating: 5,
|
||||
imageSrc: "https://img.b2bpic.net/free-photo/portrait-middle-age-biker-sports-jacket-holds-motorcycle-helmet-grey-background_613910-12160.jpg"
|
||||
},
|
||||
{
|
||||
id: "4", name: "Elena Rodriguez", handle: "@performance_journalist", testimonial: "The Mercedes-Maybach offers unparalleled comfort without compromising on performance. A true luxury sedan benchmark.", rating: 5,
|
||||
imageSrc: "https://img.b2bpic.net/free-photo/salesman-car-showroom_1303-13625.jpg"
|
||||
},
|
||||
{
|
||||
id: "5", name: "David Park", handle: "@track_day_champion", testimonial: "The Ferrari 458 Italia is a balanced masterpiece. Perfect weight distribution and responsive steering make it a driver's dream.", rating: 5,
|
||||
imageSrc: "https://img.b2bpic.net/free-photo/young-man-choosing-car-car-showroom_1303-21751.jpg"
|
||||
},
|
||||
{
|
||||
id: "6", name: "Victoria Costa", handle: "@luxury_lifestyle", testimonial: "The Lamborghini SVJ takes track performance to another level. Aggressive aerodynamics meet practical superbly.", rating: 5,
|
||||
imageSrc: "https://img.b2bpic.net/free-photo/mechanic-standing-with-arms-crossed_1170-1294.jpg"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="features" data-section="features">
|
||||
<FeatureProcessSteps
|
||||
title="Our Review Process"
|
||||
description="We employ rigorous testing methodologies and expert analysis to deliver authentic, comprehensive vehicle reviews."
|
||||
tag="How We Review"
|
||||
tagIcon={CheckCircle}
|
||||
useInvertedBackground="invertDefault"
|
||||
steps={[
|
||||
{
|
||||
number: "01", title: "Performance Testing", tag: "Track & Road", description: "Every vehicle undergoes rigorous acceleration, braking, and handling tests on both track and public roads."
|
||||
},
|
||||
{
|
||||
number: "02", title: "Design Analysis", tag: "Aesthetics & Function", description: "We examine aerodynamics, interior craftsmanship, materials quality, and visual appeal in detail."
|
||||
},
|
||||
{
|
||||
number: "03", title: "Technology Review", tag: "Innovation & Safety", description: "Comprehensive assessment of advanced features, infotainment systems, and modern safety technologies."
|
||||
},
|
||||
{
|
||||
number: "04", title: "Value Assessment", tag: "Investment Analysis", description: "We analyze pricing, resale value, maintenance costs, and long-term ownership experience."
|
||||
}
|
||||
]}
|
||||
buttons={[
|
||||
{
|
||||
text: "Read Full Methodology", href: "#contact"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="about" data-section="about">
|
||||
<MetricSplitMediaAbout
|
||||
title="Expert Automotive Analysis"
|
||||
description="SpeedReview combines professional racing experience, automotive journalism, and technical expertise to deliver unbiased, detailed reviews of the world's finest vehicles. We test cars like professionals, analyze them like engineers, and present our findings with complete transparency."
|
||||
tag="About Our Reviews"
|
||||
tagIcon={Trophy}
|
||||
useInvertedBackground="noInvert"
|
||||
metrics={[
|
||||
{
|
||||
value: "15+", title: "Years of testing expertise"
|
||||
},
|
||||
{
|
||||
value: "500+", title: "Professional test drives"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="faq" data-section="faq">
|
||||
<FaqSplitText
|
||||
sideTitle="Frequently Asked Questions"
|
||||
sideDescription="Everything you need to know about our reviews and vehicle recommendations."
|
||||
useInvertedBackground="invertDefault"
|
||||
animationType="smooth"
|
||||
textPosition="left"
|
||||
faqs={[
|
||||
{
|
||||
id: "1", title: "How are vehicles selected for review?", content: "We select vehicles based on their significance in the automotive world, market demand, performance capabilities, and manufacturer importance. Our selection committee ensures we review the most relevant and exciting cars currently available."
|
||||
},
|
||||
{
|
||||
id: "2", title: "Are your reviews sponsored by manufacturers?", content: "No. SpeedReview maintains complete editorial independence. We purchase or arrange test vehicles independently and accept no payment for favorable reviews. Our reviews reflect unbiased professional opinions only."
|
||||
},
|
||||
{
|
||||
id: "3", title: "How long does a full vehicle review take?", content: "Each comprehensive review takes 4-6 weeks of intensive testing, including 200+ miles of driving, track testing, technology evaluation, and detailed analysis. We don't rush the process to ensure accuracy."
|
||||
},
|
||||
{
|
||||
id: "4", title: "Can I get personalized car recommendations?", content: "Yes! Our expert consultants can provide tailored recommendations based on your budget, lifestyle, and performance preferences. Contact us for a personalized consultation."
|
||||
},
|
||||
{
|
||||
id: "5", title: "Do you test electric and hybrid vehicles?", content: "Absolutely. While our current showcase features performance vehicles, we test all powertrains including electric, hybrid, and alternative fuel vehicles using the same rigorous methodologies."
|
||||
},
|
||||
{
|
||||
id: "6", title: "How do I stay updated with new reviews?", content: "Subscribe to our newsletter for monthly updates on new vehicle tests, industry news, and exclusive content. You'll be first to see reviews of the latest models hitting the market."
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="contact" data-section="contact">
|
||||
<ContactText
|
||||
text="Ready to explore the world of premium automotive excellence? Get in touch with our team for personalized reviews, consultations, and exclusive automotive insights."
|
||||
animationType="background-highlight"
|
||||
useInvertedBackground="noInvert"
|
||||
buttons={[
|
||||
{
|
||||
text: "Start a Consultation", href: "#contact"
|
||||
},
|
||||
{
|
||||
text: "Subscribe to Updates", href: "#newsletter"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="footer" data-section="footer">
|
||||
<FooterBaseCard
|
||||
logoText="SpeedReview"
|
||||
copyrightText="© 2025 SpeedReview. All rights reserved. Premium automotive reviews and analysis."
|
||||
columns={[
|
||||
{
|
||||
title: "Reviews", items: [
|
||||
{
|
||||
label: "All Reviews", href: "#products"
|
||||
},
|
||||
{
|
||||
label: "Sports Cars", href: "#products"
|
||||
},
|
||||
{
|
||||
label: "Luxury Vehicles", href: "#products"
|
||||
},
|
||||
{
|
||||
label: "Performance Analysis", href: "#features"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Company", items: [
|
||||
{
|
||||
label: "About Us", href: "#about"
|
||||
},
|
||||
{
|
||||
label: "Our Process", href: "#features"
|
||||
},
|
||||
{
|
||||
label: "Contact", href: "#contact"
|
||||
},
|
||||
{
|
||||
label: "FAQ", href: "#faq"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Resources", items: [
|
||||
{
|
||||
label: "Buying Guide", href: "#"
|
||||
},
|
||||
{
|
||||
label: "Maintenance Tips", href: "#"
|
||||
},
|
||||
{
|
||||
label: "Industry News", href: "#"
|
||||
},
|
||||
{
|
||||
label: "Newsletter", href: "#"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Legal", items: [
|
||||
{
|
||||
label: "Privacy Policy", href: "#"
|
||||
},
|
||||
{
|
||||
label: "Terms of Service", href: "#"
|
||||
},
|
||||
{
|
||||
label: "Disclaimer", href: "#"
|
||||
},
|
||||
{
|
||||
label: "Cookies", href: "#"
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
146
src/components/Accordion.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, useCallback, memo } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
|
||||
interface AccordionProps {
|
||||
index: number;
|
||||
isActive?: boolean;
|
||||
onToggle?: (index: number) => void;
|
||||
title: string;
|
||||
content: string;
|
||||
animationType?: "smooth" | "instant";
|
||||
showCard?: boolean;
|
||||
useInvertedBackground?: "noInvert" | "invertDefault";
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
iconContainerClassName?: string;
|
||||
iconClassName?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
const Accordion = ({
|
||||
index,
|
||||
isActive: controlledIsActive,
|
||||
onToggle,
|
||||
title,
|
||||
content,
|
||||
animationType = "smooth",
|
||||
showCard = true,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
titleClassName = "",
|
||||
iconContainerClassName = "",
|
||||
iconClassName = "",
|
||||
contentClassName = "",
|
||||
}: AccordionProps) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [height, setHeight] = useState("0px");
|
||||
const [internalIsActive, setInternalIsActive] = useState(false);
|
||||
|
||||
const isActive = controlledIsActive !== undefined ? controlledIsActive : internalIsActive;
|
||||
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = showCard
|
||||
? shouldUseInvertedText(useInvertedBackground, theme.cardStyle)
|
||||
: useInvertedBackground;
|
||||
|
||||
useEffect(() => {
|
||||
if (animationType === "smooth") {
|
||||
setHeight(isActive ? `${contentRef.current?.scrollHeight}px` : "0px");
|
||||
}
|
||||
}, [isActive, animationType]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (controlledIsActive === undefined) {
|
||||
setInternalIsActive(!internalIsActive);
|
||||
}
|
||||
if (onToggle) {
|
||||
onToggle(index);
|
||||
}
|
||||
}, [controlledIsActive, internalIsActive, onToggle, index]);
|
||||
|
||||
const headerContent = (
|
||||
<div className="flex flex-row items-center justify-between w-full">
|
||||
<h2
|
||||
className={cls(
|
||||
"text-base md:text-xl font-medium",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
animationType === "instant" && "text-left",
|
||||
titleClassName
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<div
|
||||
className={cls(
|
||||
"h-8 aspect-square flex items-center justify-center rounded-theme primary-button transition-all duration-300",
|
||||
iconContainerClassName
|
||||
)}
|
||||
>
|
||||
<Plus
|
||||
className={cls(
|
||||
"w-4/10 aspect-square text-background",
|
||||
animationType === "smooth" ? "transition-transform duration-500" : "transition-transform duration-300",
|
||||
isActive && "rotate-45",
|
||||
iconClassName
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const contentElement = (
|
||||
<div
|
||||
className={cls(
|
||||
"text-base",
|
||||
shouldUseLightText ? "text-background" : "text-foreground",
|
||||
animationType === "smooth" && "pt-2",
|
||||
contentClassName
|
||||
)}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
);
|
||||
|
||||
if (animationType === "instant") {
|
||||
return (
|
||||
<div className={cls(showCard && "card rounded-theme", className)}>
|
||||
<button
|
||||
className={cls("cursor-pointer flex flex-row items-center justify-between w-full transition-all duration-300 group", showCard && "p-4")}
|
||||
onClick={handleClick}
|
||||
aria-expanded={isActive}
|
||||
>
|
||||
{headerContent}
|
||||
</button>
|
||||
{isActive && <div className={cls(showCard && "px-4 pb-4")}>{contentElement}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
showCard ? "card p-4 rounded-theme-capped" : "",
|
||||
"cursor-pointer flex flex-col items-center justify-between transition-all duration-500 group",
|
||||
className
|
||||
)}
|
||||
onClick={handleClick}
|
||||
aria-expanded={isActive}
|
||||
>
|
||||
{headerContent}
|
||||
<div
|
||||
ref={contentRef}
|
||||
style={{ maxHeight: height }}
|
||||
className="overflow-hidden transition-[max-height] duration-500 w-full flex flex-col"
|
||||
>
|
||||
{contentElement}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Accordion.displayName = "Accordion";
|
||||
|
||||
export default memo(Accordion);
|
||||
22
src/components/ServiceWrapper.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import Script from 'next/script';
|
||||
|
||||
export function ServiceWrapper({ children }: { children: React.ReactNode }) {
|
||||
const websiteId = process.env.NEXT_PUBLIC_WEBSITE_ANALYTICS_ID;
|
||||
|
||||
return (
|
||||
<>
|
||||
{websiteId && (
|
||||
<Script
|
||||
async
|
||||
defer
|
||||
data-website-id={websiteId}
|
||||
src="https://analytics.webild.io/script.js"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
290
src/components/Textbox.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import Image from "next/image";
|
||||
import TextAnimation from "./text/TextAnimation";
|
||||
import Button from "./button/Button";
|
||||
import Tag from "./shared/Tag";
|
||||
import AvatarGroup from "./shared/AvatarGroup";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { getButtonProps } from "@/lib/buttonUtils";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import type { AnimationType } from "./text/types";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { ButtonConfig } from "@/types/button";
|
||||
import type { Avatar } from "./shared/AvatarGroup";
|
||||
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type TitleSegment =
|
||||
| { type: "text"; content: string }
|
||||
| { type: "image"; src: string; alt?: string };
|
||||
|
||||
interface TextBoxProps {
|
||||
title: string;
|
||||
titleSegments?: TitleSegment[];
|
||||
description: string;
|
||||
type?: AnimationType;
|
||||
textboxLayout?: TextboxLayout;
|
||||
useInvertedBackground?: InvertedBackground;
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
titleImageWrapperClassName?: string;
|
||||
titleImageClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
duration?: number;
|
||||
start?: string;
|
||||
end?: string;
|
||||
gradientColors?: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
children?: React.ReactNode;
|
||||
center?: boolean;
|
||||
tag?: string;
|
||||
tagIcon?: LucideIcon;
|
||||
tagClassName?: string;
|
||||
buttons?: ButtonConfig[];
|
||||
buttonContainerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonTextClassName?: string;
|
||||
avatars?: Avatar[];
|
||||
avatarText?: string;
|
||||
avatarGroupClassName?: string;
|
||||
}
|
||||
|
||||
const TextBox = ({
|
||||
title,
|
||||
titleSegments,
|
||||
description,
|
||||
type,
|
||||
textboxLayout = "default",
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
titleClassName = "",
|
||||
titleImageWrapperClassName = "",
|
||||
titleImageClassName = "",
|
||||
descriptionClassName = "",
|
||||
duration = 1,
|
||||
start = "top 80%",
|
||||
end = "top 20%",
|
||||
gradientColors,
|
||||
children,
|
||||
center = false,
|
||||
tag,
|
||||
tagIcon: TagIcon,
|
||||
tagClassName = "",
|
||||
buttons,
|
||||
buttonContainerClassName = "",
|
||||
buttonClassName = "",
|
||||
buttonTextClassName = "",
|
||||
avatars,
|
||||
avatarText,
|
||||
avatarGroupClassName = "",
|
||||
}: TextBoxProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// Shared tag component
|
||||
const tagElement = useMemo(() => tag && (
|
||||
<Tag
|
||||
text={tag}
|
||||
icon={TagIcon}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
className={cls(textboxLayout === "default" && "mb-3", tagClassName)}
|
||||
/>
|
||||
), [tag, TagIcon, useInvertedBackground, textboxLayout, tagClassName]);
|
||||
|
||||
// Shared title component
|
||||
const titleElement = useMemo(() => (
|
||||
<TextAnimation
|
||||
type={type || theme.defaultTextAnimation}
|
||||
text={title}
|
||||
variant="trigger"
|
||||
as="h2"
|
||||
className={cls(
|
||||
textboxLayout === "split" || textboxLayout === "split-actions" || textboxLayout === "split-description" ? "text-7xl font-medium text-balance" : "text-6xl font-medium",
|
||||
center && textboxLayout === "default" && "text-center",
|
||||
useInvertedBackground === "invertDefault" && "text-background",
|
||||
titleClassName
|
||||
)}
|
||||
duration={duration}
|
||||
start={start}
|
||||
end={end}
|
||||
gradientColors={gradientColors}
|
||||
/>
|
||||
), [type, theme.defaultTextAnimation, title, textboxLayout, center, useInvertedBackground, titleClassName, duration, start, end, gradientColors]);
|
||||
|
||||
// Inline image title component (used when textboxLayout === "inline-image")
|
||||
const inlineImageTitleElement = useMemo(() => titleSegments && titleSegments.length > 0 ? (
|
||||
<h2
|
||||
className={cls(
|
||||
"text-4xl md:text-5xl font-medium text-center leading-[1.15] text-balance",
|
||||
useInvertedBackground === "invertDefault" && "text-background",
|
||||
titleClassName
|
||||
)}
|
||||
>
|
||||
{titleSegments.map((segment, index) => {
|
||||
const imageIndex = titleSegments
|
||||
.slice(0, index + 1)
|
||||
.filter(s => s.type === "image").length - 1;
|
||||
|
||||
const element = segment.type === "text" ? (
|
||||
<span key={index}>{segment.content}</span>
|
||||
) : (
|
||||
<span
|
||||
key={index}
|
||||
className={cls(
|
||||
"inline-block relative primary-button -mt-[0.2em] h-[1.1em] w-auto aspect-square align-middle mx-1 p-0.5 rounded-theme",
|
||||
imageIndex % 2 === 0 ? "-rotate-12" : "rotate-12",
|
||||
titleImageWrapperClassName
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full h-full">
|
||||
<Image
|
||||
src={segment.src}
|
||||
alt={segment.alt || ""}
|
||||
width={24}
|
||||
height={24}
|
||||
className={cls(
|
||||
"absolute inset-0 m-auto h-full w-full rounded-theme",
|
||||
titleImageClassName
|
||||
)}
|
||||
unoptimized={segment.src.startsWith("http") || segment.src.startsWith("//")}
|
||||
aria-hidden={!segment.alt || segment.alt === ""}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<span key={index}>
|
||||
{index > 0 && " "}
|
||||
{element}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</h2>
|
||||
) : null, [titleSegments, useInvertedBackground, titleClassName, titleImageWrapperClassName, titleImageClassName]);
|
||||
|
||||
// Shared description component
|
||||
const descriptionElement = useMemo(() => (
|
||||
<TextAnimation
|
||||
type={type || theme.defaultTextAnimation}
|
||||
text={description}
|
||||
variant="words-trigger"
|
||||
as="p"
|
||||
className={cls(
|
||||
"text-lg leading-[1.2]",
|
||||
center && textboxLayout === "default" && "text-center",
|
||||
(textboxLayout === "split" || textboxLayout === "split-description") && "text-balance",
|
||||
useInvertedBackground === "invertDefault" && "text-background",
|
||||
descriptionClassName
|
||||
)}
|
||||
duration={duration}
|
||||
start={start}
|
||||
end={end}
|
||||
gradientColors={gradientColors}
|
||||
/>
|
||||
), [type, theme.defaultTextAnimation, description, center, textboxLayout, useInvertedBackground, descriptionClassName, duration, start, end, gradientColors]);
|
||||
|
||||
// Shared avatars component
|
||||
const avatarsElement = useMemo(() => avatars && avatars.length > 0 ? (
|
||||
<AvatarGroup
|
||||
avatars={avatars}
|
||||
text={avatarText}
|
||||
className={cls(
|
||||
textboxLayout === "default" && "mt-3",
|
||||
center && textboxLayout === "default" && "justify-center",
|
||||
avatarGroupClassName
|
||||
)}
|
||||
/>
|
||||
) : null, [avatars, avatarText, textboxLayout, center, avatarGroupClassName]);
|
||||
|
||||
// Shared buttons/children component
|
||||
const actionsElement = useMemo(() => buttons && buttons.length > 0 ? (
|
||||
<div className={cls(
|
||||
"flex gap-4",
|
||||
textboxLayout === "default" && "w-full mt-3",
|
||||
(textboxLayout === "split" || textboxLayout === "split-actions") && "w-fit",
|
||||
center && textboxLayout === "default" && "justify-center",
|
||||
buttonContainerClassName
|
||||
)}>
|
||||
{/* Limit to 2 buttons for optimal layout */}
|
||||
{buttons.slice(0, 2).map((button, index) => (
|
||||
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
), [buttons, textboxLayout, center, buttonContainerClassName, theme.defaultButtonVariant, buttonClassName, buttonTextClassName, children]);
|
||||
|
||||
// Split layout
|
||||
if (textboxLayout === "split") {
|
||||
return (
|
||||
<div className={cls("flex flex-col md:flex-row gap-3 md:gap-15 md:items-end", className)}>
|
||||
<div className="w-full md:w-6/10 flex flex-col gap-3">
|
||||
{tagElement}
|
||||
{titleElement}
|
||||
{descriptionElement}
|
||||
</div>
|
||||
<div className="w-full md:w-4/10 flex flex-col gap-3 md:items-end">
|
||||
{actionsElement}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Split actions layout - tag and buttons required, no description
|
||||
if (textboxLayout === "split-actions") {
|
||||
return (
|
||||
<div className={cls("flex flex-col md:flex-row gap-3 md:gap-15 md:items-end", className)}>
|
||||
<div className="w-full md:w-6/10 flex flex-col gap-3">
|
||||
{tagElement}
|
||||
{titleElement}
|
||||
</div>
|
||||
<div className="w-full md:w-4/10 flex flex-col gap-3 md:items-end">
|
||||
{actionsElement}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Split description layout - tag + title left, description only right (no buttons)
|
||||
if (textboxLayout === "split-description") {
|
||||
return (
|
||||
<div className={cls("flex flex-col md:flex-row gap-3 md:gap-15 md:items-end", className)}>
|
||||
<div className="w-full md:w-6/10 flex flex-col gap-3">
|
||||
{tagElement}
|
||||
{titleElement}
|
||||
</div>
|
||||
<div className="w-full md:w-4/10 flex flex-col gap-3 md:items-end">
|
||||
{descriptionElement}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Inline image layout - centered heading with inline images and optional buttons
|
||||
if (textboxLayout === "inline-image") {
|
||||
return (
|
||||
<div className={cls("flex flex-col gap-3 md:gap-1", center && "items-center text-center", className)}>
|
||||
{inlineImageTitleElement}
|
||||
{actionsElement}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default layout
|
||||
return (
|
||||
<div className={cls("flex flex-col gap-3 md:gap-1", center && "items-center text-center", className)}>
|
||||
{tagElement}
|
||||
{titleElement}
|
||||
{descriptionElement}
|
||||
{actionsElement}
|
||||
{avatarsElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TextBox.displayName = "TextBox";
|
||||
|
||||
export default memo(TextBox);
|
||||
44
src/components/background/AnimatedAuroraBackground.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
import React, { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface AnimatedAuroraBackgroundProps {
|
||||
className?: string;
|
||||
showRadialGradient?: boolean;
|
||||
/**
|
||||
* Inverts the aurora colors for better visibility.
|
||||
* Use `true` for light backgrounds (makes aurora darker/inverted)
|
||||
* Use `false` for dark backgrounds (keeps aurora colors vibrant)
|
||||
*/
|
||||
invertColors: boolean;
|
||||
}
|
||||
|
||||
const AnimatedAuroraBackground = ({
|
||||
className,
|
||||
showRadialGradient = true,
|
||||
invertColors,
|
||||
}: AnimatedAuroraBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"fixed inset-0 -z-10 bg-background",
|
||||
className
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute inset-0 overflow-hidden opacity-30">
|
||||
<div
|
||||
className={cls(
|
||||
"[--base-gradient:repeating-linear-gradient(100deg,var(--background)_0%,var(--background)_7%,transparent_10%,transparent_12%,var(--background)_16%)] [--aurora:repeating-linear-gradient(100deg,var(--color-primary-cta)_10%,var(--color-accent)_15%,var(--color-secondary-cta)_20%,var(--color-accent)_25%,var(--color-primary-cta)_30%)] [background-image:var(--base-gradient),var(--aurora)] [background-size:300%,_200%] [background-position:50%_50%,50%_50%] filter blur-[10px] after:content-[''] after:absolute after:inset-0 after:[background-image:var(--base-gradient),var(--aurora)] after:[background-size:200%,_100%] after:[animation:aurora_60s_linear_infinite] after:[background-attachment:fixed] after:mix-blend-difference pointer-events-none absolute -inset-[10px] opacity-30 will-change-transform",
|
||||
invertColors && "invert",
|
||||
showRadialGradient && "[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]"
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AnimatedAuroraBackground.displayName = "AnimatedAuroraBackground";
|
||||
|
||||
export default memo(AnimatedAuroraBackground);
|
||||
112
src/components/background/AnimatedGridBackground.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useEffect, useId, useRef, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface AnimatedGridBackgroundProps {
|
||||
className?: string;
|
||||
squareSize?: number;
|
||||
numSquares?: number;
|
||||
maxOpacity?: number;
|
||||
}
|
||||
|
||||
const AnimatedGridBackground = ({
|
||||
className = "",
|
||||
squareSize = 100,
|
||||
numSquares = 50,
|
||||
maxOpacity = 0.15,
|
||||
}: AnimatedGridBackgroundProps) => {
|
||||
const id = useId();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [squares, setSquares] = useState<Array<{ id: number; pos: [number, number] }>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const { width, height } = containerRef.current.getBoundingClientRect();
|
||||
setDimensions({ width, height });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (dimensions.width && dimensions.height) {
|
||||
const cols = Math.ceil(dimensions.width / squareSize);
|
||||
const rows = Math.ceil(dimensions.height / squareSize);
|
||||
|
||||
const newSquares = Array.from({ length: numSquares }, (_, i) => ({
|
||||
id: i,
|
||||
pos: [
|
||||
Math.floor(Math.random() * cols),
|
||||
Math.floor(Math.random() * rows),
|
||||
] as [number, number],
|
||||
}));
|
||||
|
||||
setSquares(newSquares);
|
||||
}
|
||||
}, [dimensions, squareSize, numSquares]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cls(
|
||||
"absolute inset-0 z-0 pointer-events-none select-none overflow-hidden inset-x-0 inset-y-[-30%] h-[200%] skew-y-12",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
mask: 'radial-gradient(ellipse 100% 100% at 50% 0%, rgb(0, 0, 0) 0%, rgba(0, 0, 0, 0) 70%)',
|
||||
WebkitMask: 'radial-gradient(ellipse 100% 100% at 50% 0%, rgb(0, 0, 0) 0%, rgba(0, 0, 0, 0) 70%)',
|
||||
} as React.CSSProperties}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id={`grid-${id}`}
|
||||
width={squareSize}
|
||||
height={squareSize}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path
|
||||
d={`M ${squareSize} 0 L 0 0 0 ${squareSize}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
className="text-background-accent/50"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill={`url(#grid-${id})`} />
|
||||
{squares.map(({ id, pos: [x, y] }) => (
|
||||
<motion.rect
|
||||
key={id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: [0, maxOpacity, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: Math.random() * 2 + 2,
|
||||
repeat: Infinity,
|
||||
delay: Math.random() * 2,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
x={x * squareSize}
|
||||
y={y * squareSize}
|
||||
width={squareSize}
|
||||
height={squareSize}
|
||||
fill="var(--color-background-accent)"
|
||||
strokeWidth="0"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AnimatedGridBackground.displayName = "AnimatedGridBackground";
|
||||
|
||||
export default memo(AnimatedGridBackground);
|
||||
32
src/components/background/AuroraBackground.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface AuroraBackgroundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const AuroraBackground = ({
|
||||
className = "",
|
||||
}: AuroraBackgroundProps) => {
|
||||
return (
|
||||
<div className={cls("fixed inset-0 z-0 w-full h-full bg-background", className)}>
|
||||
<div className="absolute top-0 left-0 w-full h-full z-10 backdrop-blur-3xl" ></div>
|
||||
{/* top center */}
|
||||
<div className="absolute top-0 left-1/2 -translate-y-1/2 -translate-x-[120%] w-[9vw] h-[110vh] bg-background-accent/15 -rotate-[52.5deg] rounded-[100%]" />
|
||||
{/* top right */}
|
||||
<div className="absolute top-[-20vh] right-[2.5vw] -translate-x-[0%] w-[12.5vw] h-[100vh] bg-background-accent/15 -rotate-[60deg] rounded-[100%]" />
|
||||
{/* center left */}
|
||||
<div className="absolute top-[-20vh] left-[2vw] -translate-x-[0%] w-[15vw] h-[150vh] bg-background-accent/20 -rotate-[45deg] rounded-[100%]" />
|
||||
{/* top left */}
|
||||
<div className="absolute top-[-30vh] left-0 -translate-x-[0%] w-[10vw] h-[70vh] bg-background-accent/15 -rotate-[45deg] rounded-[100%]" />
|
||||
{/* bottom center */}
|
||||
<div className="absolute bottom-[-40vh] left-0 -translate-x-[0%] w-[120vw] h-[50vh] bg-background-accent/10 -rotate-[20deg] rounded-[100%]" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AuroraBackground.displayName = "AuroraBackground";
|
||||
|
||||
export default memo(AuroraBackground);
|
||||
58
src/components/background/BlurBottomBackground.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useState, useEffect, useCallback } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
|
||||
const BOTTOM_THRESHOLD = 50;
|
||||
const TOP_THRESHOLD = 50;
|
||||
|
||||
interface BlurBottomBackgroundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const BlurBottomBackground = ({
|
||||
className = ""
|
||||
}: BlurBottomBackgroundProps) => {
|
||||
const [isAtBottom, setIsAtBottom] = useState(false);
|
||||
const [isAtTop, setIsAtTop] = useState(true);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||
const windowHeight = window.innerHeight;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
|
||||
const distanceFromBottom = documentHeight - (scrollTop + windowHeight);
|
||||
|
||||
setIsAtTop(scrollTop <= TOP_THRESHOLD);
|
||||
setIsAtBottom(distanceFromBottom <= BOTTOM_THRESHOLD);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleScroll();
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
window.addEventListener("resize", handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
window.removeEventListener("resize", handleScroll);
|
||||
};
|
||||
}, [handleScroll]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"fixed pointer-events-none backdrop-blur-xl w-full h-50 left-0 bottom-0 z-[500] transition-opacity duration-500 ease-out",
|
||||
isAtTop || isAtBottom ? "opacity-0" : "opacity-100",
|
||||
className
|
||||
)}
|
||||
style={{ maskImage: MASK_GRADIENT }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
BlurBottomBackground.displayName = "BlurBottomBackground";
|
||||
|
||||
export default memo(BlurBottomBackground);
|
||||
74
src/components/background/CanvasRevealBackground.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import { memo, useState, useEffect } from 'react';
|
||||
import { cls } from '@/lib/utils';
|
||||
import CanvasRevealEffect from './CanvasRevealEffect';
|
||||
|
||||
interface CanvasRevealBackgroundProps {
|
||||
className?: string;
|
||||
animationSpeed?: number;
|
||||
dotSize?: number;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
const hexToRgb = (hex: string): number[] => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]
|
||||
: [0, 255, 255];
|
||||
};
|
||||
|
||||
const CanvasRevealBackground = ({
|
||||
className = "",
|
||||
animationSpeed = 5,
|
||||
dotSize = 3,
|
||||
height = "30%",
|
||||
}: CanvasRevealBackgroundProps) => {
|
||||
const [colors, setColors] = useState<number[][]>([[0, 255, 255]]);
|
||||
|
||||
useEffect(() => {
|
||||
const primaryCta = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--color-background-accent')
|
||||
.trim();
|
||||
|
||||
if (primaryCta) {
|
||||
setColors([hexToRgb(primaryCta)]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls("absolute inset-0 z-0 overflow-hidden pointer-events-none select-none", className)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 w-full"
|
||||
style={{
|
||||
height: height,
|
||||
mask: `
|
||||
radial-gradient(ellipse 60% 120% at 50% 0%, rgb(0, 0, 0) 0%, rgba(0, 0, 0, 0) 80%),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 10%, rgb(0, 0, 0) 25%, rgb(0, 0, 0) 75%, rgba(0, 0, 0, 0) 90%, rgba(0, 0, 0, 0) 100%)
|
||||
`,
|
||||
maskComposite: 'intersect',
|
||||
WebkitMask: `
|
||||
radial-gradient(ellipse 60% 120% at 50% 0%, rgb(0, 0, 0) 0%, rgba(0, 0, 0, 0) 80%),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 10%, rgb(0, 0, 0) 25%, rgb(0, 0, 0) 75%, rgba(0, 0, 0, 0) 90%, rgba(0, 0, 0, 0) 100%)
|
||||
`,
|
||||
WebkitMaskComposite: 'source-in',
|
||||
}}
|
||||
>
|
||||
<CanvasRevealEffect
|
||||
animationSpeed={animationSpeed}
|
||||
colors={colors}
|
||||
dotSize={dotSize}
|
||||
showGradient={false}
|
||||
containerClassName="bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CanvasRevealBackground.displayName = 'CanvasRevealBackground';
|
||||
|
||||
export default memo(CanvasRevealBackground);
|
||||
304
src/components/background/CanvasRevealEffect.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
'use client';
|
||||
|
||||
import { cls } from '@/lib/utils';
|
||||
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||
import { useMemo, useRef, useCallback, memo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
interface CanvasRevealEffectProps {
|
||||
animationSpeed?: number;
|
||||
opacities?: number[];
|
||||
colors?: number[][];
|
||||
containerClassName?: string;
|
||||
dotSize?: number;
|
||||
showGradient?: boolean;
|
||||
}
|
||||
|
||||
const CanvasRevealEffect = ({
|
||||
animationSpeed = 0.4,
|
||||
opacities = [0.2, 0.2, 0.2, 0.4, 0.4, 0.4, 0.7, 0.6, 0.6, 0.9],
|
||||
colors = [[0, 255, 255]],
|
||||
containerClassName = "",
|
||||
dotSize = 3,
|
||||
showGradient = true,
|
||||
}: CanvasRevealEffectProps) => {
|
||||
return (
|
||||
<div className={cls('h-full relative bg-white w-full', containerClassName)}>
|
||||
<div className="h-full w-full">
|
||||
<DotMatrix
|
||||
colors={colors}
|
||||
dotSize={dotSize}
|
||||
opacities={opacities}
|
||||
shader={`
|
||||
float animation_speed_factor = ${animationSpeed.toFixed(1)};
|
||||
float intro_offset = distance(u_resolution / 2.0 / u_total_size, st2) * 0.01 + (random(st2) * 0.15);
|
||||
opacity *= step(intro_offset, u_time * animation_speed_factor);
|
||||
opacity *= clamp((1.0 - step(intro_offset + 0.1, u_time * animation_speed_factor)) * 1.25, 1.0, 1.25);
|
||||
`}
|
||||
center={['x', 'y']}
|
||||
/>
|
||||
</div>
|
||||
{showGradient && (
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-gray-950 to-[84%]" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface DotMatrixProps {
|
||||
colors?: number[][];
|
||||
opacities?: number[];
|
||||
totalSize?: number;
|
||||
dotSize?: number;
|
||||
shader?: string;
|
||||
center?: ('x' | 'y')[];
|
||||
}
|
||||
|
||||
const DotMatrix = ({
|
||||
colors = [[0, 0, 0]],
|
||||
opacities = [0.04, 0.04, 0.04, 0.04, 0.04, 0.08, 0.08, 0.08, 0.08, 0.14],
|
||||
totalSize = 4,
|
||||
dotSize = 2,
|
||||
shader = '',
|
||||
center = ['x', 'y'],
|
||||
}: DotMatrixProps) => {
|
||||
const uniforms = useMemo(() => {
|
||||
let colorsArray = [
|
||||
colors[0],
|
||||
colors[0],
|
||||
colors[0],
|
||||
colors[0],
|
||||
colors[0],
|
||||
colors[0],
|
||||
];
|
||||
if (colors.length === 2) {
|
||||
colorsArray = [
|
||||
colors[0],
|
||||
colors[0],
|
||||
colors[0],
|
||||
colors[1],
|
||||
colors[1],
|
||||
colors[1],
|
||||
];
|
||||
} else if (colors.length === 3) {
|
||||
colorsArray = [
|
||||
colors[0],
|
||||
colors[0],
|
||||
colors[1],
|
||||
colors[1],
|
||||
colors[2],
|
||||
colors[2],
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
u_colors: {
|
||||
value: colorsArray.map((color) => [
|
||||
color[0] / 255,
|
||||
color[1] / 255,
|
||||
color[2] / 255,
|
||||
]),
|
||||
type: 'uniform3fv',
|
||||
},
|
||||
u_opacities: {
|
||||
value: opacities,
|
||||
type: 'uniform1fv',
|
||||
},
|
||||
u_total_size: {
|
||||
value: totalSize,
|
||||
type: 'uniform1f',
|
||||
},
|
||||
u_dot_size: {
|
||||
value: dotSize,
|
||||
type: 'uniform1f',
|
||||
},
|
||||
};
|
||||
}, [colors, opacities, totalSize, dotSize]);
|
||||
|
||||
return (
|
||||
<Shader
|
||||
source={`
|
||||
precision mediump float;
|
||||
in vec2 fragCoord;
|
||||
|
||||
uniform float u_time;
|
||||
uniform float u_opacities[10];
|
||||
uniform vec3 u_colors[6];
|
||||
uniform float u_total_size;
|
||||
uniform float u_dot_size;
|
||||
uniform vec2 u_resolution;
|
||||
out vec4 fragColor;
|
||||
float PHI = 1.61803398874989484820459;
|
||||
float random(vec2 xy) {
|
||||
return fract(tan(distance(xy * PHI, xy) * 0.5) * xy.x);
|
||||
}
|
||||
float map(float value, float min1, float max1, float min2, float max2) {
|
||||
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
|
||||
}
|
||||
void main() {
|
||||
vec2 st = fragCoord.xy;
|
||||
${
|
||||
center.includes('x')
|
||||
? 'st.x -= abs(floor((mod(u_resolution.x, u_total_size) - u_dot_size) * 0.5));'
|
||||
: ''
|
||||
}
|
||||
${
|
||||
center.includes('y')
|
||||
? 'st.y -= abs(floor((mod(u_resolution.y, u_total_size) - u_dot_size) * 0.5));'
|
||||
: ''
|
||||
}
|
||||
float opacity = step(0.0, st.x);
|
||||
opacity *= step(0.0, st.y);
|
||||
|
||||
vec2 st2 = vec2(int(st.x / u_total_size), int(st.y / u_total_size));
|
||||
|
||||
float frequency = 5.0;
|
||||
float show_offset = random(st2);
|
||||
float rand = random(st2 * floor((u_time / frequency) + show_offset + frequency) + 1.0);
|
||||
opacity *= u_opacities[int(rand * 10.0)];
|
||||
opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.x / u_total_size));
|
||||
opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.y / u_total_size));
|
||||
|
||||
vec3 color = u_colors[int(show_offset * 6.0)];
|
||||
|
||||
${shader}
|
||||
|
||||
fragColor = vec4(color, opacity);
|
||||
fragColor.rgb *= fragColor.a;
|
||||
}`}
|
||||
uniforms={uniforms}
|
||||
maxFps={60}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type Uniforms = {
|
||||
[key: string]: {
|
||||
value: number[] | number[][] | number;
|
||||
type: string;
|
||||
};
|
||||
};
|
||||
|
||||
const ShaderMaterial = ({
|
||||
source,
|
||||
uniforms,
|
||||
maxFps = 60,
|
||||
}: {
|
||||
source: string;
|
||||
maxFps?: number;
|
||||
uniforms: Uniforms;
|
||||
}) => {
|
||||
const { size } = useThree();
|
||||
const ref = useRef<THREE.Mesh>(null);
|
||||
let lastFrameTime = 0;
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!ref.current) return;
|
||||
const timestamp = clock.getElapsedTime();
|
||||
if (timestamp - lastFrameTime < 1 / maxFps) {
|
||||
return;
|
||||
}
|
||||
lastFrameTime = timestamp;
|
||||
|
||||
const material = ref.current.material as THREE.ShaderMaterial;
|
||||
const timeLocation = material.uniforms.u_time;
|
||||
timeLocation.value = timestamp;
|
||||
});
|
||||
|
||||
const getUniforms = useCallback(() => {
|
||||
const preparedUniforms: Record<string, { value: unknown; type?: string }> = {};
|
||||
|
||||
for (const uniformName in uniforms) {
|
||||
const uniform = uniforms[uniformName] as { type: string; value: number | number[] | number[][] };
|
||||
|
||||
switch (uniform.type) {
|
||||
case 'uniform1f':
|
||||
preparedUniforms[uniformName] = { value: uniform.value, type: '1f' };
|
||||
break;
|
||||
case 'uniform3f':
|
||||
preparedUniforms[uniformName] = {
|
||||
value: new THREE.Vector3().fromArray(uniform.value as number[]),
|
||||
type: '3f',
|
||||
};
|
||||
break;
|
||||
case 'uniform1fv':
|
||||
preparedUniforms[uniformName] = { value: uniform.value, type: '1fv' };
|
||||
break;
|
||||
case 'uniform3fv':
|
||||
preparedUniforms[uniformName] = {
|
||||
value: (uniform.value as number[][]).map((v: number[]) =>
|
||||
new THREE.Vector3().fromArray(v)
|
||||
),
|
||||
type: '3fv',
|
||||
};
|
||||
break;
|
||||
case 'uniform2f':
|
||||
preparedUniforms[uniformName] = {
|
||||
value: new THREE.Vector2().fromArray(uniform.value as number[]),
|
||||
type: '2f',
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.error(`Invalid uniform type for '${uniformName}'.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
preparedUniforms['u_time'] = { value: 0, type: '1f' };
|
||||
preparedUniforms['u_resolution'] = {
|
||||
value: new THREE.Vector2(size.width * 2, size.height * 2),
|
||||
};
|
||||
return preparedUniforms;
|
||||
}, [uniforms, size.width, size.height]);
|
||||
|
||||
const material = useMemo(() => {
|
||||
const materialObject = new THREE.ShaderMaterial({
|
||||
vertexShader: `
|
||||
precision mediump float;
|
||||
in vec2 coordinates;
|
||||
uniform vec2 u_resolution;
|
||||
out vec2 fragCoord;
|
||||
void main(){
|
||||
float x = position.x;
|
||||
float y = position.y;
|
||||
gl_Position = vec4(x, y, 0.0, 1.0);
|
||||
fragCoord = (position.xy + vec2(1.0)) * 0.5 * u_resolution;
|
||||
fragCoord.y = u_resolution.y - fragCoord.y;
|
||||
}
|
||||
`,
|
||||
fragmentShader: source,
|
||||
uniforms: getUniforms(),
|
||||
glslVersion: THREE.GLSL3,
|
||||
blending: THREE.CustomBlending,
|
||||
blendSrc: THREE.SrcAlphaFactor,
|
||||
blendDst: THREE.OneFactor,
|
||||
});
|
||||
|
||||
return materialObject;
|
||||
}, [source, getUniforms]);
|
||||
|
||||
return (
|
||||
<mesh ref={ref as React.Ref<THREE.Mesh>}>
|
||||
<planeGeometry args={[2, 2]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
|
||||
interface ShaderProps {
|
||||
source: string;
|
||||
uniforms: Uniforms;
|
||||
maxFps?: number;
|
||||
}
|
||||
|
||||
const Shader = ({ source, uniforms, maxFps = 60 }: ShaderProps) => {
|
||||
return (
|
||||
<Canvas className="absolute inset-0 h-full w-full">
|
||||
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
|
||||
</Canvas>
|
||||
);
|
||||
};
|
||||
|
||||
CanvasRevealEffect.displayName = 'CanvasRevealEffect';
|
||||
|
||||
export default memo(CanvasRevealEffect);
|
||||
56
src/components/background/CardPattern.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import { motion, useMotionTemplate, type MotionValue } from "framer-motion";
|
||||
|
||||
const GRADIENT_SIZE = 250;
|
||||
|
||||
interface CardPatternProps {
|
||||
mouseX: MotionValue<number>;
|
||||
mouseY: MotionValue<number>;
|
||||
randomString: string;
|
||||
isActive?: boolean;
|
||||
gradientClassName?: string;
|
||||
}
|
||||
|
||||
function CardPatternComponent({
|
||||
mouseX,
|
||||
mouseY,
|
||||
randomString,
|
||||
isActive = false,
|
||||
gradientClassName,
|
||||
}: CardPatternProps) {
|
||||
const maskImage = useMotionTemplate`radial-gradient(${GRADIENT_SIZE}px at ${mouseX}px ${mouseY}px, white, transparent)`;
|
||||
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
maskImage,
|
||||
WebkitMaskImage: maskImage,
|
||||
}),
|
||||
[maskImage]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none">
|
||||
<div
|
||||
className={`absolute inset-0 rounded-theme-capped [mask-image:linear-gradient(white,transparent)] ${isActive ? "opacity-50" : "group-hover/primary-button:opacity-50"}`}
|
||||
/>
|
||||
<motion.div
|
||||
className={`absolute inset-0 rounded-theme-capped ${gradientClassName} backdrop-blur-xl transition duration-500 ${isActive ? "opacity-100" : "opacity-0 group-hover/primary-button:opacity-100"}`}
|
||||
style={style}
|
||||
/>
|
||||
<motion.div
|
||||
className={`absolute inset-0 rounded-theme-capped mix-blend-overlay ${isActive ? "opacity-100" : "opacity-0 group-hover/primary-button:opacity-100"}`}
|
||||
style={style}
|
||||
>
|
||||
<p className="absolute inset-x-0 text-xs h-full break-words whitespace-pre-wrap text-white font-mono font-bold transition duration-500">
|
||||
{randomString}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CardPatternComponent.displayName = "CardPattern";
|
||||
|
||||
export const CardPattern = memo(CardPatternComponent);
|
||||
103
src/components/background/CellWaveBackground.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import { cls } from '@/lib/utils';
|
||||
|
||||
interface CellWaveBackgroundProps {
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
cellColor?: string;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CellWaveBackground = ({
|
||||
columns = 5,
|
||||
rows = 24,
|
||||
cellColor = 'var(--color-background-accent)',
|
||||
duration = 0.25,
|
||||
delay = 1.25,
|
||||
className = ''
|
||||
}: CellWaveBackgroundProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const cellRefs = useRef<(HTMLDivElement | null)[][]>([]);
|
||||
const timelinesRef = useRef<gsap.core.Timeline[]>([]);
|
||||
|
||||
const setCellRef = (colIndex: number, cellIndex: number) => (el: HTMLDivElement | null) => {
|
||||
if (!cellRefs.current[colIndex]) {
|
||||
cellRefs.current[colIndex] = [];
|
||||
}
|
||||
cellRefs.current[colIndex][cellIndex] = el;
|
||||
};
|
||||
|
||||
const cellStyles = {
|
||||
backgroundColor: cellColor,
|
||||
boxShadow: `0px 0px 50px 16px color-mix(in srgb, ${cellColor} 12%, transparent), 0px 0px 7px 1px color-mix(in srgb, ${cellColor} 31%, transparent)`
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
timelinesRef.current.forEach(tl => tl.kill());
|
||||
timelinesRef.current = [];
|
||||
|
||||
cellRefs.current.forEach((column, colIndex) => {
|
||||
const cells = [...column].filter(Boolean).reverse();
|
||||
const timeline = gsap.timeline({
|
||||
delay: delay * colIndex,
|
||||
repeat: -1,
|
||||
repeatDelay: 2
|
||||
});
|
||||
|
||||
cells.forEach((cell, cellIndex) => {
|
||||
if (cell) {
|
||||
timeline.to(cell, {
|
||||
keyframes: [
|
||||
{ opacity: 0, duration: 0 },
|
||||
{ opacity: 0.05, duration: duration },
|
||||
{ opacity: 0.15, duration: duration },
|
||||
{ opacity: 0.25, duration: duration },
|
||||
{ opacity: 0.5, duration: duration },
|
||||
{ opacity: 0.25, duration: duration },
|
||||
{ opacity: 0.15, duration: duration },
|
||||
{ opacity: 0.05, duration: duration },
|
||||
{ opacity: 0, duration: duration }
|
||||
],
|
||||
ease: 'none'
|
||||
}, cellIndex * duration);
|
||||
}
|
||||
});
|
||||
|
||||
timelinesRef.current.push(timeline);
|
||||
});
|
||||
|
||||
return () => {
|
||||
timelinesRef.current.forEach(tl => tl.kill());
|
||||
};
|
||||
}, [duration, delay, columns, rows]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cls("absolute inset-0 z-0 flex items-end justify-between pointer-events-none select-none", className)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{Array.from({ length: columns }).map((_, colIndex) => (
|
||||
<div className="relative flex flex-col gap-1 h-full" key={colIndex}>
|
||||
{Array.from({ length: rows }).map((_, cellIndex) => (
|
||||
<div
|
||||
ref={setCellRef(colIndex, cellIndex)}
|
||||
className="opacity-0 h-8 w-2"
|
||||
key={cellIndex}
|
||||
style={cellStyles}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CellWaveBackground.displayName = 'CellWaveBackground';
|
||||
|
||||
export default memo(CellWaveBackground);
|
||||
48
src/components/background/CircleGradientBackground.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type DiagonalVariant = "primary" | "secondary";
|
||||
|
||||
interface CircleGradientBackgroundProps {
|
||||
className?: string;
|
||||
diagonal?: DiagonalVariant;
|
||||
}
|
||||
|
||||
const CircleGradientBackground = ({
|
||||
className = "",
|
||||
diagonal = "primary",
|
||||
}: CircleGradientBackgroundProps) => {
|
||||
const isPrimary = diagonal === "primary";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls("fixed top-0 left-0 right-0 bottom-0 h-screen w-full -z-10 overflow-hidden", className)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"fixed w-100 md:w-70 h-auto aspect-square rounded-full opacity-10",
|
||||
isPrimary ? "top-0 right-0 translate-x-1/2 -translate-y-1/2" : "top-0 left-0 -translate-x-1/2 -translate-y-1/2"
|
||||
)}
|
||||
style={{
|
||||
background: `radial-gradient(circle at center, var(--color-background-accent) 35%, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cls(
|
||||
"fixed w-100 md:w-70 h-auto aspect-square rounded-full opacity-10",
|
||||
isPrimary ? "bottom-0 left-0 -translate-x-1/2 translate-y-1/2" : "bottom-0 right-0 translate-x-1/2 translate-y-1/2"
|
||||
)}
|
||||
style={{
|
||||
background: `radial-gradient(circle at center, var(--color-background-accent) 35%, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CircleGradientBackground.displayName = "CircleGradientBackground";
|
||||
|
||||
export default memo(CircleGradientBackground);
|
||||
45
src/components/background/DotGridBackground.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type GridSize = "small" | "medium" | "large";
|
||||
|
||||
interface DotGridBackgroundProps {
|
||||
size?: GridSize;
|
||||
className?: string;
|
||||
perspectiveThreeD?: boolean;
|
||||
}
|
||||
|
||||
const GRID_SIZES: Record<GridSize, string> = {
|
||||
small: "1vw 1vw",
|
||||
medium: "2vw 2vw",
|
||||
large: "4vw 4vw",
|
||||
};
|
||||
|
||||
const DotGridBackground = ({
|
||||
size = "medium",
|
||||
className = "",
|
||||
perspectiveThreeD = false
|
||||
}: DotGridBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"fixed inset-0 -z-10 bg-background [mask-image:radial-gradient(circle_at_center,white_0%,transparent_90%)]",
|
||||
perspectiveThreeD && "inset-x-0 inset-y-[-30%] h-[200%] skew-y-12",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(circle, color-mix(in srgb, var(--background-accent) 30%, transparent) 1px, transparent 1px)",
|
||||
backgroundSize: GRID_SIZES[size],
|
||||
backgroundRepeat: "repeat",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
DotGridBackground.displayName = "DotGridBackground";
|
||||
|
||||
export default memo(DotGridBackground);
|
||||
130
src/components/background/DownwardRaysBackground.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface RayConfig {
|
||||
width: number;
|
||||
opacity: number;
|
||||
rotation: number;
|
||||
scale?: number;
|
||||
animationDuration: number;
|
||||
animationDelay: number;
|
||||
}
|
||||
|
||||
interface LightSourceConfig {
|
||||
width: number;
|
||||
height?: number;
|
||||
opacity: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
interface DownwardRaysBackgroundProps {
|
||||
animated: boolean;
|
||||
showGrid: boolean;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
const rays: RayConfig[] = [
|
||||
{ width: 35, opacity: 1, rotation: -20, animationDuration: 4, animationDelay: 0 },
|
||||
{ width: 35, opacity: 0.6, rotation: -12, animationDuration: 3.5, animationDelay: 0.5 },
|
||||
{ width: 20, opacity: 0.45, rotation: -5, scale: 0.90, animationDuration: 5, animationDelay: 1.2 },
|
||||
{ width: 15, opacity: 0.625, rotation: -3, animationDuration: 3, animationDelay: 0.3 },
|
||||
{ width: 40, opacity: 0.1, rotation: 0, scale: 0.79, animationDuration: 4.5, animationDelay: 0.8 },
|
||||
{ width: 20, opacity: 0.525, rotation: 3, animationDuration: 3.2, animationDelay: 1.5 },
|
||||
{ width: 15, opacity: 0.725, rotation: 5, scale: 0.90, animationDuration: 4.2, animationDelay: 0.2 },
|
||||
{ width: 35, opacity: 0.6, rotation: 12, animationDuration: 3.8, animationDelay: 1 },
|
||||
{ width: 35, opacity: 1, rotation: 20, animationDuration: 4, animationDelay: 0.7 },
|
||||
];
|
||||
|
||||
const lightSources: LightSourceConfig[] = [
|
||||
{ width: 1198, opacity: 0.025, top: -352 },
|
||||
{ width: 865, height: 929, opacity: 0.1, top: -252 },
|
||||
{ width: 865, height: 929, opacity: 0.1, top: -252 },
|
||||
];
|
||||
|
||||
const DownwardRaysBackground = ({
|
||||
animated,
|
||||
showGrid,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
}: DownwardRaysBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls("absolute inset-0 z-0 overflow-hidden pointer-events-none select-none", className)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{animated && (
|
||||
<style>
|
||||
{`
|
||||
@keyframes rayPulse {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: var(--target-opacity); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
)}
|
||||
|
||||
{showGrid && (
|
||||
<div
|
||||
className="absolute inset-0 -z-10 bg-background [mask-image:radial-gradient(50%_50%_at_50%_0%,white_0%,transparent_100%)]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to right, color-mix(in srgb, var(--color-background-accent) 20%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in srgb, var(--color-background-accent) 10%, transparent) 1px, transparent 1px)",
|
||||
backgroundSize: "10vw 10vw",
|
||||
backgroundRepeat: "repeat",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cls(
|
||||
"absolute overflow-hidden w-[1142px] h-[129vh] -top-[400px] left-1/2 -translate-x-1/2",
|
||||
"blur-[16px]",
|
||||
"[mask:radial-gradient(50%_109%,#000_0%,#000000f6_0%,transparent_96%)]",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
{rays.map((ray, index) => (
|
||||
<div
|
||||
key={`ray-${index}`}
|
||||
className="absolute overflow-hidden origin-top -top-[352px] -bottom-[920px] [background:radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
|
||||
style={{
|
||||
width: `${ray.width}px`,
|
||||
left: `calc(50% - ${ray.width / 2}px)`,
|
||||
transform: `${ray.scale ? `scale(${ray.scale})` : ""} rotate(${ray.rotation}deg)`,
|
||||
...(animated
|
||||
? {
|
||||
"--target-opacity": ray.opacity,
|
||||
animation: `rayPulse ${ray.animationDuration}s ease-in-out ${ray.animationDelay}s infinite both`,
|
||||
}
|
||||
: {
|
||||
opacity: ray.opacity,
|
||||
}),
|
||||
} as React.CSSProperties}
|
||||
/>
|
||||
))}
|
||||
|
||||
{lightSources.map((source, index) => (
|
||||
<div
|
||||
key={`light-source-${index}`}
|
||||
className="absolute overflow-hidden [background:radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
|
||||
style={{
|
||||
width: `${source.width}px`,
|
||||
height: source.height ? `${source.height}px` : undefined,
|
||||
top: `${source.top}px`,
|
||||
bottom: source.height ? undefined : "-46px",
|
||||
left: `calc(50% - ${source.width / 2}px)`,
|
||||
opacity: source.opacity,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DownwardRaysBackground.displayName = "DownwardRaysBackground";
|
||||
|
||||
export default memo(DownwardRaysBackground);
|
||||
277
src/components/background/FluidBackground.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useMemo, memo, useEffect, useState } from 'react';
|
||||
import { Canvas, useFrame, extend, useThree } from '@react-three/fiber';
|
||||
import { shaderMaterial } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import { cls } from '@/lib/utils';
|
||||
|
||||
const getComputedColor = (varName: string): THREE.Color => {
|
||||
if (typeof window === 'undefined') return new THREE.Color(0x000000);
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const colorString = styles.getPropertyValue(varName).trim();
|
||||
return new THREE.Color(colorString || '#000000');
|
||||
};
|
||||
|
||||
const vertexShader = `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
uniform float iTime;
|
||||
uniform vec2 iResolution;
|
||||
uniform vec3 uBackgroundColor;
|
||||
uniform vec3 uPrimaryCta;
|
||||
uniform vec3 uAccent;
|
||||
uniform vec3 uSecondaryCta;
|
||||
varying vec2 vUv;
|
||||
|
||||
vec4 buf[8];
|
||||
|
||||
vec4 sigmoid(vec4 x) { return 1. / (1. + exp(-x)); }
|
||||
|
||||
vec4 cppn_fn(vec2 coordinate, float in0, float in1, float in2) {
|
||||
buf[6] = vec4(coordinate.x, coordinate.y, 0.3948333106474662 + in0, 0.36 + in1);
|
||||
buf[7] = vec4(0.14 + in2, sqrt(coordinate.x * coordinate.x + coordinate.y * coordinate.y), 0., 0.);
|
||||
|
||||
buf[0] = mat4(vec4(6.5404263, -3.6126034, 0.7590882, -1.13613), vec4(2.4582713, 3.1660357, 1.2219609, 0.06276096), vec4(-5.478085, -6.159632, 1.8701609, -4.7742867), vec4(6.039214, -5.542865, -0.90925294, 3.251348))
|
||||
* buf[6]
|
||||
+ mat4(vec4(0.8473259, -5.722911, 3.975766, 1.6522468), vec4(-0.24321538, 0.5839259, -1.7661959, -5.350116), vec4(0.0, 0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0))
|
||||
* buf[7]
|
||||
+ vec4(0.21808943, 1.1243913, -1.7969975, 5.0294676);
|
||||
|
||||
buf[1] = mat4(vec4(-3.3522482, -6.0612736, 0.55641043, -4.4719114), vec4(0.8631464, 1.7432913, 5.643898, 1.6106541), vec4(2.4941394, -3.5012043, 1.7184316, 6.357333), vec4(3.310376, 8.209261, 1.1355612, -1.165539))
|
||||
* buf[6]
|
||||
+ mat4(vec4(5.24046, -13.034365, 0.009859298, 15.870829), vec4(2.987511, 3.129433, -0.89023495, -1.6822904), vec4(0.0, 0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0))
|
||||
* buf[7]
|
||||
+ vec4(-5.9457836, -6.573602, -0.8812491, 1.5436668);
|
||||
|
||||
buf[0] = sigmoid(buf[0]);
|
||||
buf[1] = sigmoid(buf[1]);
|
||||
|
||||
buf[2] = mat4(vec4(-15.219568, 8.095543, -2.429353, -1.9381982), vec4(-5.951362, 4.3115187, 2.6393783, 1.274315), vec4(-7.3145227, 6.7297835, 5.2473326, 5.9411426), vec4(5.0796127, 8.979051, -1.7278991, -1.158976))
|
||||
* buf[6]
|
||||
+ mat4(vec4(-11.967154, -11.608155, 6.1486754, 11.237008), vec4(2.124141, -6.263192, -1.7050359, -0.7021966), vec4(0.0, 0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0))
|
||||
* buf[7]
|
||||
+ vec4(-4.17164, -3.2281182, -4.576417, -3.6401186);
|
||||
|
||||
buf[3] = mat4(vec4(3.1832156, -13.738922, 1.879223, 3.233465), vec4(0.64300746, 12.768129, 1.9141049, 0.50990224), vec4(-0.049295485, 4.4807224, 1.4733979, 1.801449), vec4(5.0039253, 13.000481, 3.3991797, -4.5561905))
|
||||
* buf[6]
|
||||
+ mat4(vec4(-0.1285731, 7.720628, -3.1425676, 4.742367), vec4(0.6393625, 3.714393, -0.8108378, -0.39174938), vec4(0.0, 0.0, 0.0, 0.0), vec4(0.0, 0.0, 0.0, 0.0))
|
||||
* buf[7]
|
||||
+ vec4(-1.1811101, -21.621881, 0.7851888, 1.2329718);
|
||||
|
||||
buf[2] = sigmoid(buf[2]);
|
||||
buf[3] = sigmoid(buf[3]);
|
||||
|
||||
buf[4] = mat4(vec4(5.214916, -7.183024, 2.7228765, 2.6592617), vec4(-5.601878, -25.3591, 4.067988, 0.4602802), vec4(-10.57759, 24.286327, 21.102104, 37.546658), vec4(4.3024497, -1.9625226, 2.3458803, -1.372816))
|
||||
* buf[0]
|
||||
+ mat4(vec4(-17.6526, -10.507558, 2.2587414, 12.462782), vec4(6.265566, -502.75443, -12.642513, 0.9112289), vec4(-10.983244, 20.741234, -9.701768, -0.7635988), vec4(5.383626, 1.4819539, -4.1911616, -4.8444734))
|
||||
* buf[1]
|
||||
+ mat4(vec4(12.785233, -16.345072, -0.39901125, 1.7955981), vec4(-30.48365, -1.8345358, 1.4542528, -1.1118771), vec4(19.872723, -7.337935, -42.941723, -98.52709), vec4(8.337645, -2.7312303, -2.2927687, -36.142323))
|
||||
* buf[2]
|
||||
+ mat4(vec4(-16.298317, 3.5471997, -0.44300047, -9.444417), vec4(57.5077, -35.609753, 16.163465, -4.1534753), vec4(-0.07470326, -3.8656476, -7.0901804, 3.1523974), vec4(-12.559385, -7.077619, 1.490437, -0.8211543))
|
||||
* buf[3]
|
||||
+ vec4(-7.67914, 15.927437, 1.3207729, -1.6686112);
|
||||
|
||||
buf[5] = mat4(vec4(-1.4109162, -0.372762, -3.770383, -21.367174), vec4(-6.2103205, -9.35908, 0.92529047, 8.82561), vec4(11.460242, -22.348068, 13.625772, -18.693201), vec4(-0.3429052, -3.9905605, -2.4626114, -0.45033523))
|
||||
* buf[0]
|
||||
+ mat4(vec4(7.3481627, -4.3661838, -6.3037653, -3.868115), vec4(1.5462853, 6.5488915, 1.9701879, -0.58291394), vec4(6.5858274, -2.2180402, 3.7127688, -1.3730392), vec4(-5.7973905, 10.134961, -2.3395722, -5.965605))
|
||||
* buf[1]
|
||||
+ mat4(vec4(-2.5132585, -6.6685553, -1.4029363, -0.16285264), vec4(-0.37908727, 0.53738135, 4.389061, -1.3024765), vec4(-0.70647055, 2.0111287, -5.1659346, -3.728635), vec4(-13.562562, 10.487719, -0.9173751, -2.6487076))
|
||||
* buf[2]
|
||||
+ mat4(vec4(-8.645013, 6.5546675, -6.3944063, -5.5933375), vec4(-0.57783127, -1.077275, 36.91025, 5.736769), vec4(14.283112, 3.7146652, 7.1452246, -4.5958776), vec4(2.7192075, 3.6021907, -4.366337, -2.3653464))
|
||||
* buf[3]
|
||||
+ vec4(-5.9000807, -4.329569, 1.2427121, 8.59503);
|
||||
|
||||
buf[4] = sigmoid(buf[4]);
|
||||
buf[5] = sigmoid(buf[5]);
|
||||
|
||||
buf[6] = mat4(vec4(-1.61102, 0.7970257, 1.4675229, 0.20917463), vec4(-28.793737, -7.1390953, 1.5025433, 4.656581), vec4(-10.94861, 39.66238, 0.74318546, -10.095605), vec4(-0.7229728, -1.5483948, 0.7301322, 2.1687684))
|
||||
* buf[0]
|
||||
+ mat4(vec4(3.2547753, 21.489103, -1.0194173, -3.3100595), vec4(-3.7316632, -3.3792162, -7.223193, -0.23685838), vec4(13.1804495, 0.7916005, 5.338587, 5.687114), vec4(-4.167605, -17.798311, -6.815736, -1.6451967))
|
||||
* buf[1]
|
||||
+ mat4(vec4(0.604885, -7.800309, -7.213122, -2.741014), vec4(-3.522382, -0.12359311, -0.5258442, 0.43852118), vec4(9.6752825, -22.853785, 2.062431, 0.099892326), vec4(-4.3196306, -17.730087, 2.5184598, 5.30267))
|
||||
* buf[2]
|
||||
+ mat4(vec4(-6.545563, -15.790176, -6.0438633, -5.415399), vec4(-43.591583, 28.551912, -16.00161, 18.84728), vec4(4.212382, 8.394307, 3.0958717, 8.657522), vec4(-5.0237565, -4.450633, -4.4768, -5.5010443))
|
||||
* buf[3]
|
||||
+ mat4(vec4(1.6985557, -67.05806, 6.897715, 1.9004834), vec4(1.8680354, 2.3915145, 2.5231109, 4.081538), vec4(11.158006, 1.7294737, 2.0738268, 7.386411), vec4(-4.256034, -306.24686, 8.258898, -17.132736))
|
||||
* buf[4]
|
||||
+ mat4(vec4(1.6889864, -4.5852966, 3.8534803, -6.3482175), vec4(1.3543309, -1.2640043, 9.932754, 2.9079645), vec4(-5.2770967, 0.07150358, -0.13962056, 3.3269649), vec4(28.34703, -4.918278, 6.1044083, 4.085355))
|
||||
* buf[5]
|
||||
+ vec4(6.6818056, 12.522166, -3.7075126, -4.104386);
|
||||
|
||||
buf[7] = mat4(vec4(-8.265602, -4.7027016, 5.098234, 0.7509808), vec4(8.6507845, -17.15949, 16.51939, -8.884479), vec4(-4.036479, -2.3946867, -2.6055532, -1.9866527), vec4(-2.2167742, -1.8135649, -5.9759874, 4.8846445))
|
||||
* buf[0]
|
||||
+ mat4(vec4(6.7790847, 3.5076547, -2.8191125, -2.7028968), vec4(-5.743024, -0.27844876, 1.4958696, -5.0517144), vec4(13.122226, 15.735168, -2.9397483, -4.101023), vec4(-14.375265, -5.030483, -6.2599335, 2.9848232))
|
||||
* buf[1]
|
||||
+ mat4(vec4(4.0950394, -0.94011575, -5.674733, 4.755022), vec4(4.3809423, 4.8310084, 1.7425908, -3.437416), vec4(2.117492, 0.16342592, -104.56341, 16.949184), vec4(-5.22543, -2.994248, 3.8350096, -1.9364246))
|
||||
* buf[2]
|
||||
+ mat4(vec4(-5.900337, 1.7946124, -13.604192, -3.8060522), vec4(6.6583457, 31.911177, 25.164474, 91.81147), vec4(11.840538, 4.1503043, -0.7314397, 6.768467), vec4(-6.3967767, 4.034772, 6.1714606, -0.32874924))
|
||||
* buf[3]
|
||||
+ mat4(vec4(3.4992442, -196.91893, -8.923708, 2.8142626), vec4(3.4806502, -3.1846354, 5.1725626, 5.1804223), vec4(-2.4009497, 15.585794, 1.2863957, 2.0252278), vec4(-71.25271, -62.441242, -8.138444, 0.50670296))
|
||||
* buf[4]
|
||||
+ mat4(vec4(-12.291733, -11.176166, -7.3474145, 4.390294), vec4(10.805477, 5.6337385, -0.9385842, -4.7348723), vec4(-12.869276, -7.039391, 5.3029537, 7.5436664), vec4(1.4593618, 8.91898, 3.5101583, 5.840625))
|
||||
* buf[5]
|
||||
+ vec4(2.2415268, -6.705987, -0.98861027, -2.117676);
|
||||
|
||||
buf[6] = sigmoid(buf[6]);
|
||||
buf[7] = sigmoid(buf[7]);
|
||||
|
||||
buf[0] = mat4(vec4(1.6794263, 1.3817469, 2.9625452, 0.0), vec4(-1.8834411, -1.4806935, -3.5924516, 0.0), vec4(-1.3279216, -1.0918057, -2.3124623, 0.0), vec4(0.2662234, 0.23235129, 0.44178495, 0.0))
|
||||
* buf[0]
|
||||
+ mat4(vec4(-0.6299101, -0.5945583, -0.9125601, 0.0), vec4(0.17828953, 0.18300213, 0.18182953, 0.0), vec4(-2.96544, -2.5819945, -4.9001055, 0.0), vec4(1.4195864, 1.1868085, 2.5176322, 0.0))
|
||||
* buf[1]
|
||||
+ mat4(vec4(-1.2584374, -1.0552157, -2.1688404, 0.0), vec4(-0.7200217, -0.52666044, -1.438251, 0.0), vec4(0.15345335, 0.15196142, 0.272854, 0.0), vec4(0.945728, 0.8861938, 1.2766753, 0.0))
|
||||
* buf[2]
|
||||
+ mat4(vec4(-2.4218085, -1.968602, -4.35166, 0.0), vec4(-22.683098, -18.0544, -41.954372, 0.0), vec4(0.63792, 0.5470648, 1.1078634, 0.0), vec4(-1.5489894, -1.3075932, -2.6444845, 0.0))
|
||||
* buf[3]
|
||||
+ mat4(vec4(-0.49252132, -0.39877754, -0.91366625, 0.0), vec4(0.95609266, 0.7923952, 1.640221, 0.0), vec4(0.30616966, 0.15693925, 0.8639857, 0.0), vec4(1.1825981, 0.94504964, 2.176963, 0.0))
|
||||
* buf[4]
|
||||
+ mat4(vec4(0.35446745, 0.3293795, 0.59547555, 0.0), vec4(-0.58784515, -0.48177817, -1.0614829, 0.0), vec4(2.5271258, 1.9991658, 4.6846647, 0.0), vec4(0.13042648, 0.08864098, 0.30187556, 0.0))
|
||||
* buf[5]
|
||||
+ mat4(vec4(-1.7718065, -1.4033192, -3.3355875, 0.0), vec4(3.1664357, 2.638297, 5.378702, 0.0), vec4(-3.1724713, -2.6107926, -5.549295, 0.0), vec4(-2.851368, -2.249092, -5.3013067, 0.0))
|
||||
* buf[6]
|
||||
+ mat4(vec4(1.5203838, 1.2212278, 2.8404984, 0.0), vec4(1.5210563, 1.2651345, 2.683903, 0.0), vec4(2.9789467, 2.4364579, 5.2347264, 0.0), vec4(2.2270417, 1.8825914, 3.8028636, 0.0))
|
||||
* buf[7]
|
||||
+ vec4(-1.5468478, -3.6171484, 0.24762098, 0.0);
|
||||
|
||||
buf[0] = sigmoid(buf[0]);
|
||||
return vec4(buf[0].x , buf[0].y , buf[0].z, 1.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = vUv * 2.0 - 1.0; uv.y *= -1.0;
|
||||
vec4 pattern = cppn_fn(uv, 0.1 * sin(0.3 * iTime), 0.1 * sin(0.69 * iTime), 0.1 * sin(0.44 * iTime));
|
||||
|
||||
vec3 color1 = mix(uBackgroundColor, uPrimaryCta, pattern.x);
|
||||
vec3 color2 = mix(uBackgroundColor, uAccent, pattern.y);
|
||||
vec3 color3 = mix(uBackgroundColor, uSecondaryCta, pattern.z);
|
||||
|
||||
vec3 finalColor = (color1 + color2 + color3) / 3.0;
|
||||
|
||||
gl_FragColor = vec4(finalColor, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const CPPNShaderMaterial = shaderMaterial(
|
||||
{
|
||||
iTime: 0,
|
||||
iResolution: new THREE.Vector2(1, 1),
|
||||
uBackgroundColor: new THREE.Color(0x000000),
|
||||
uPrimaryCta: new THREE.Color(0xff0000),
|
||||
uAccent: new THREE.Color(0x00ff00),
|
||||
uSecondaryCta: new THREE.Color(0x0000ff),
|
||||
},
|
||||
vertexShader,
|
||||
fragmentShader
|
||||
);
|
||||
|
||||
extend({ CPPNShaderMaterial });
|
||||
|
||||
interface ShaderPlaneProps {
|
||||
backgroundColor: THREE.Color;
|
||||
primaryCta: THREE.Color;
|
||||
accent: THREE.Color;
|
||||
secondaryCta: THREE.Color;
|
||||
}
|
||||
|
||||
const ShaderPlane = memo(({ backgroundColor, primaryCta, accent, secondaryCta }: ShaderPlaneProps) => {
|
||||
const meshRef = useRef<THREE.Mesh>(null!);
|
||||
const materialRef = useRef<THREE.ShaderMaterial & {
|
||||
iTime: number;
|
||||
iResolution: THREE.Vector2;
|
||||
uBackgroundColor: THREE.Color;
|
||||
uPrimaryCta: THREE.Color;
|
||||
uAccent: THREE.Color;
|
||||
uSecondaryCta: THREE.Color;
|
||||
}>(null!);
|
||||
const { viewport } = useThree();
|
||||
|
||||
useFrame((state) => {
|
||||
if (!materialRef.current) return;
|
||||
materialRef.current.iTime = state.clock.elapsedTime;
|
||||
const { width, height } = state.size;
|
||||
materialRef.current.iResolution.set(width, height);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!materialRef.current) return;
|
||||
materialRef.current.uBackgroundColor = backgroundColor;
|
||||
materialRef.current.uPrimaryCta = primaryCta;
|
||||
materialRef.current.uAccent = accent;
|
||||
materialRef.current.uSecondaryCta = secondaryCta;
|
||||
}, [backgroundColor, primaryCta, accent, secondaryCta]);
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef} position={[0, 0, 0]}>
|
||||
<planeGeometry args={[viewport.width, viewport.height]} />
|
||||
<cPPNShaderMaterial ref={materialRef} side={THREE.DoubleSide} />
|
||||
</mesh>
|
||||
);
|
||||
});
|
||||
|
||||
ShaderPlane.displayName = 'ShaderPlane';
|
||||
|
||||
interface FluidBackgroundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const FluidBackground = ({ className = "" }: FluidBackgroundProps) => {
|
||||
const camera = useMemo(() => ({ position: [0, 0, 5] as [number, number, number], fov: 75, near: 0.1, far: 1000 }), []);
|
||||
|
||||
const [colors, setColors] = useState({
|
||||
background: new THREE.Color(0x000000),
|
||||
primaryCta: new THREE.Color(0xff0000),
|
||||
accent: new THREE.Color(0x00ff00),
|
||||
secondaryCta: new THREE.Color(0x0000ff),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const updateColors = () => {
|
||||
setColors({
|
||||
background: getComputedColor('--background'),
|
||||
primaryCta: getComputedColor('--color-primary-cta'),
|
||||
accent: getComputedColor('--color-accent'),
|
||||
secondaryCta: getComputedColor('--color-secondary-cta'),
|
||||
});
|
||||
};
|
||||
|
||||
updateColors();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cls("bg-background fixed inset-0 -z-10 w-full h-full", className)} aria-hidden="true">
|
||||
<Canvas
|
||||
camera={camera}
|
||||
gl={{ antialias: true, alpha: false }}
|
||||
dpr={[1, 2]}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<ShaderPlane
|
||||
backgroundColor={colors.background}
|
||||
primaryCta={colors.primaryCta}
|
||||
accent={colors.accent}
|
||||
secondaryCta={colors.secondaryCta}
|
||||
/>
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FluidBackground.displayName = 'FluidBackground';
|
||||
|
||||
export default memo(FluidBackground);
|
||||
|
||||
declare module '@react-three/fiber' {
|
||||
interface ThreeElements {
|
||||
cPPNShaderMaterial: unknown;
|
||||
}
|
||||
}
|
||||
272
src/components/background/GlowingEffect.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { animate } from "motion/react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
const INACTIVE_ZONE_MULTIPLIER = 0.5;
|
||||
const CENTER_MULTIPLIER = 0.5;
|
||||
const ANGLE_CONVERSION_FACTOR = 180 / Math.PI;
|
||||
const ANGLE_OFFSET = 90;
|
||||
const ANGLE_NORMALIZATION = 180;
|
||||
const FULL_CIRCLE = 360;
|
||||
const REPEATING_GRADIENT_TIMES = 5;
|
||||
const GRADIENT_DIVISION = 25;
|
||||
|
||||
const ANIMATION_EASING = [0.16, 1, 0.3, 1] as const;
|
||||
|
||||
interface GlowingEffectProps {
|
||||
blur?: number;
|
||||
inactiveZone?: number;
|
||||
proximity?: number;
|
||||
spread?: number;
|
||||
glow?: boolean;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
movementDuration?: number;
|
||||
borderWidth?: number;
|
||||
}
|
||||
|
||||
interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
type MouseEventLike = MouseEvent | Position;
|
||||
const getIsSSR = () => typeof window === "undefined";
|
||||
|
||||
const getViewportCenter = (): Position => {
|
||||
if (getIsSSR()) return { x: 0, y: 0 };
|
||||
return {
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
};
|
||||
};
|
||||
|
||||
const getIsMobileDevice = (): boolean => {
|
||||
if (getIsSSR()) return false;
|
||||
return window.innerWidth < MOBILE_BREAKPOINT;
|
||||
};
|
||||
|
||||
const calculateAngleDiff = (current: number, target: number): number => {
|
||||
return ((target - current + ANGLE_NORMALIZATION) % FULL_CIRCLE) - ANGLE_NORMALIZATION;
|
||||
};
|
||||
|
||||
const GlowingEffect = memo(
|
||||
({
|
||||
blur = 0,
|
||||
inactiveZone = 0.7,
|
||||
proximity = 0,
|
||||
spread = 20,
|
||||
glow = false,
|
||||
className,
|
||||
movementDuration = 2,
|
||||
borderWidth = 1,
|
||||
disabled = true,
|
||||
}: GlowingEffectProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const lastPosition = useRef<Position>({ x: 0, y: 0 });
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
const [isMobile, setIsMobile] = useState(() => getIsMobileDevice());
|
||||
|
||||
const updateElementStyles = useCallback(
|
||||
(element: HTMLElement, property: string, value: string) => {
|
||||
element.style.setProperty(property, value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const calculateMousePosition = useCallback(
|
||||
(e?: MouseEventLike): Position => {
|
||||
if (isMobile) {
|
||||
return getViewportCenter();
|
||||
}
|
||||
return {
|
||||
x: e?.x ?? lastPosition.current.x,
|
||||
y: e?.y ?? lastPosition.current.y,
|
||||
};
|
||||
},
|
||||
[isMobile]
|
||||
);
|
||||
|
||||
const animateAngleTransition = useCallback(
|
||||
(element: HTMLElement, currentAngle: number, targetAngle: number) => {
|
||||
const angleDiff = calculateAngleDiff(currentAngle, targetAngle);
|
||||
const newAngle = currentAngle + angleDiff;
|
||||
|
||||
animate(currentAngle, newAngle, {
|
||||
duration: movementDuration,
|
||||
ease: ANIMATION_EASING,
|
||||
onUpdate: (value) => {
|
||||
updateElementStyles(element, "--start", String(value));
|
||||
},
|
||||
});
|
||||
},
|
||||
[movementDuration, updateElementStyles]
|
||||
);
|
||||
|
||||
const handleMove = useCallback(
|
||||
(e?: MouseEventLike) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(() => {
|
||||
const element = containerRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const { left, top, width, height } = element.getBoundingClientRect();
|
||||
const mousePosition = calculateMousePosition(e);
|
||||
|
||||
if (e) {
|
||||
lastPosition.current = mousePosition;
|
||||
}
|
||||
|
||||
const centerX = left + width * CENTER_MULTIPLIER;
|
||||
const centerY = top + height * CENTER_MULTIPLIER;
|
||||
const distanceFromCenter = Math.hypot(
|
||||
mousePosition.x - centerX,
|
||||
mousePosition.y - centerY
|
||||
);
|
||||
const inactiveRadius = INACTIVE_ZONE_MULTIPLIER * Math.min(width, height) * inactiveZone;
|
||||
|
||||
if (distanceFromCenter < inactiveRadius) {
|
||||
updateElementStyles(element, "--active", "0");
|
||||
return;
|
||||
}
|
||||
|
||||
const isActive =
|
||||
mousePosition.x > left - proximity &&
|
||||
mousePosition.x < left + width + proximity &&
|
||||
mousePosition.y > top - proximity &&
|
||||
mousePosition.y < top + height + proximity;
|
||||
|
||||
updateElementStyles(element, "--active", isActive ? "1" : "0");
|
||||
|
||||
if (!isActive) return;
|
||||
|
||||
const currentAngle =
|
||||
parseFloat(element.style.getPropertyValue("--start")) || 0;
|
||||
const targetAngle =
|
||||
ANGLE_CONVERSION_FACTOR * Math.atan2(mousePosition.y - centerY, mousePosition.x - centerX) +
|
||||
ANGLE_OFFSET;
|
||||
|
||||
animateAngleTransition(element, currentAngle, targetAngle);
|
||||
});
|
||||
},
|
||||
[inactiveZone, proximity, calculateMousePosition, updateElementStyles, animateAngleTransition]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (getIsSSR()) return;
|
||||
|
||||
const checkMobile = () => {
|
||||
setIsMobile(getIsMobileDevice());
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", checkMobile);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled || getIsSSR()) return;
|
||||
|
||||
const handleScroll = () => handleMove();
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
if (!isMobile) {
|
||||
handleMove(e);
|
||||
}
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
handleMove();
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
document.body.addEventListener("pointermove", handlePointerMove, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
document.body.removeEventListener("pointermove", handlePointerMove);
|
||||
};
|
||||
}, [handleMove, disabled, isMobile]);
|
||||
|
||||
const gradient = useMemo(
|
||||
() => `radial-gradient(circle, var(--accent) 10%, transparent 20%),
|
||||
radial-gradient(circle at 40% 40%, var(--background-accent) 5%, transparent 15%),
|
||||
repeating-conic-gradient(
|
||||
from 236.84deg at 50% 50%,
|
||||
var(--accent) 0%,
|
||||
var(--background-accent) calc(${GRADIENT_DIVISION}% / var(--repeating-conic-gradient-times)),
|
||||
var(--accent) calc(${GRADIENT_DIVISION * 2}% / var(--repeating-conic-gradient-times))
|
||||
)`,
|
||||
[]
|
||||
);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => ({
|
||||
"--blur": `${blur}px`,
|
||||
"--spread": spread,
|
||||
"--start": "0",
|
||||
"--active": "0",
|
||||
"--glowingeffect-border-width": `${borderWidth}px`,
|
||||
"--repeating-conic-gradient-times": String(REPEATING_GRADIENT_TIMES),
|
||||
"--gradient": gradient,
|
||||
} as React.CSSProperties),
|
||||
[blur, spread, borderWidth, gradient]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cls(
|
||||
"pointer-events-none absolute inset-0 hidden rounded-[inherit] border opacity-0 transition-opacity",
|
||||
glow && "opacity-100",
|
||||
disabled && "!block"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={containerStyle}
|
||||
className={cls(
|
||||
"pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity",
|
||||
glow && "opacity-100",
|
||||
blur > 0 && "blur-[var(--blur)] ",
|
||||
className,
|
||||
disabled && "!hidden"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"glow",
|
||||
"rounded-[inherit]",
|
||||
'after:content-[""] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]',
|
||||
"after:[border:var(--glowingeffect-border-width)_solid_transparent]",
|
||||
"after:[background:var(--gradient)] after:[background-attachment:fixed]",
|
||||
"after:opacity-[var(--active)] after:transition-opacity after:duration-300",
|
||||
"after:[mask-clip:padding-box,border-box]",
|
||||
"after:[mask-composite:intersect]",
|
||||
"after:[mask-image:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
GlowingEffect.displayName = "GlowingEffect";
|
||||
|
||||
export { GlowingEffect };
|
||||
52
src/components/background/GlowingOrbBackground.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { cls } from '@/lib/utils';
|
||||
|
||||
interface GlowingOrbBackgroundProps {
|
||||
className?: string;
|
||||
blurAmount?: string;
|
||||
glowColor?: string;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
const GlowingOrbBackground = ({
|
||||
className = "",
|
||||
blurAmount = "57px",
|
||||
glowColor = "var(--color-primary-cta)",
|
||||
backgroundColor = "var(--background)",
|
||||
}: GlowingOrbBackgroundProps) => {
|
||||
return (
|
||||
<div className="absolute z-0 top-0 left-0 w-full h-screen overflow-hidden pointer-events-none select-none [mask-image:linear-gradient(180deg,rgb(0,0,0)_0%,rgb(0,0,0)_80%,rgba(0,0,0,0)_100%)]" aria-hidden="true">
|
||||
<div
|
||||
className={cls("absolute left-1/2 -translate-x-1/2 w-full h-[100vh] -bottom-[9vh] overflow-hidden z-0", className)}
|
||||
>
|
||||
<div
|
||||
className="absolute left-1/2 -translate-x-1/2 w-[49vw] h-[12vh] bottom-[25vh] overflow-hidden"
|
||||
style={{
|
||||
background: `radial-gradient(50% 50% at 50% 50%, color-mix(in srgb, ${glowColor} 25%, transparent), transparent)`,
|
||||
filter: `blur(${blurAmount})`,
|
||||
WebkitFilter: `blur(${blurAmount})`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-[61vh] -left-[33vw] -right-[33vw] h-[100vh] rounded-[100%]"
|
||||
style={{
|
||||
background: `linear-gradient(180deg, color-mix(in srgb, ${glowColor} 30%, transparent), transparent)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute -bottom-[62vh] -left-[36vw] -right-[36vw] h-[105vh] rounded-[100%]"
|
||||
style={{
|
||||
backgroundColor,
|
||||
boxShadow: `inset 0 2px 20px color-mix(in srgb, ${glowColor} 30%, transparent), 0 -10px 50px 1px color-mix(in srgb, ${glowColor} 25%, transparent)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GlowingOrbBackground.displayName = 'GlowingOrbBackground';
|
||||
|
||||
export default memo(GlowingOrbBackground);
|
||||
74
src/components/background/GradientBarsBackground.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { cls } from '@/lib/utils';
|
||||
|
||||
interface GradientBarsBackgroundProps {
|
||||
className?: string;
|
||||
numBarsPerSide?: number;
|
||||
gradientFrom?: string;
|
||||
gradientTo?: string;
|
||||
opacity?: number;
|
||||
sideWidth?: string;
|
||||
}
|
||||
|
||||
const GradientBarsBackground = ({
|
||||
className = "",
|
||||
numBarsPerSide = 8,
|
||||
gradientFrom = "var(--color-primary-cta)",
|
||||
gradientTo = "transparent",
|
||||
opacity = 0.075,
|
||||
sideWidth = "35%",
|
||||
}: GradientBarsBackgroundProps) => {
|
||||
const getBarStyle = (side: 'left' | 'right') => ({
|
||||
flex: '1 0 0',
|
||||
minWidth: '30px',
|
||||
maxWidth: '82px',
|
||||
background: `linear-gradient(${side === 'left' ? '90deg' : '270deg'}, ${gradientFrom}, ${gradientTo})`,
|
||||
opacity: opacity,
|
||||
});
|
||||
|
||||
const renderBars = (side: 'left' | 'right') =>
|
||||
Array.from({ length: numBarsPerSide }).map((_, index) => (
|
||||
<div key={`${side}-${index}`} className="h-full" style={getBarStyle(side)} />
|
||||
));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls("absolute inset-0 z-0 overflow-hidden pointer-events-none select-none", className)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className="flex h-8/10 w-full justify-between backface-hidden antialiased"
|
||||
style={{
|
||||
transform: 'translateZ(0)',
|
||||
mask: 'linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 100%)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex h-full overflow-hidden"
|
||||
style={{
|
||||
width: sideWidth,
|
||||
mask: 'linear-gradient(270deg, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 100%)',
|
||||
}}
|
||||
>
|
||||
{renderBars('left')}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex h-full justify-end overflow-hidden"
|
||||
style={{
|
||||
width: sideWidth,
|
||||
mask: 'linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 100%)',
|
||||
}}
|
||||
>
|
||||
{renderBars('right')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GradientBarsBackground.displayName = 'GradientBarsBackground';
|
||||
|
||||
export default memo(GradientBarsBackground);
|
||||
45
src/components/background/GridBackround.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type GridSize = "small" | "medium" | "large";
|
||||
|
||||
interface GridBackroundProps {
|
||||
size?: GridSize;
|
||||
className?: string;
|
||||
perspectiveThreeD?: boolean;
|
||||
}
|
||||
|
||||
const GRID_SIZES: Record<GridSize, string> = {
|
||||
small: "6.25vw 6.25vw",
|
||||
medium: "10vw 10vw",
|
||||
large: "20vw 20vw",
|
||||
};
|
||||
|
||||
const GridBackround = ({
|
||||
size = "medium",
|
||||
className = "",
|
||||
perspectiveThreeD = false
|
||||
}: GridBackroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"fixed inset-0 -z-10 bg-background [mask-image:radial-gradient(circle_at_center,white_0%,transparent_90%)]",
|
||||
perspectiveThreeD && "inset-x-0 inset-y-[-30%] h-[200%] skew-y-12",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to right, color-mix(in srgb, var(--background-accent) 10%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in srgb, var(--background-accent) 10%, transparent) 1px, transparent 1px)",
|
||||
backgroundSize: GRID_SIZES[size],
|
||||
backgroundRepeat: "repeat",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
GridBackround.displayName = "GridBackround";
|
||||
|
||||
export default memo(GridBackround);
|
||||
116
src/components/background/HeroBackgrounds.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import AnimatedGridBackground from "./AnimatedGridBackground";
|
||||
import CanvasRevealBackground from "./CanvasRevealBackground";
|
||||
import CellWaveBackground from "./CellWaveBackground";
|
||||
import DownwardRaysBackground from "./DownwardRaysBackground";
|
||||
import GlowingOrbBackground from "./GlowingOrbBackground";
|
||||
import GradientBarsBackground from "./GradientBarsBackground";
|
||||
import RadialGradientBackground from "./RadialGradientBackground";
|
||||
import RotatedRaysBackground from "./RotatedRaysBackground";
|
||||
import RotatingGradientBackground from "./RotatingGradientBackground";
|
||||
import SparklesGradientBackground from "./SparklesGradientBackground";
|
||||
|
||||
export type HeroBackgroundVariant =
|
||||
| "plain"
|
||||
| "animated-grid"
|
||||
| "canvas-reveal"
|
||||
| "cell-wave"
|
||||
| "downward-rays-animated"
|
||||
| "downward-rays-animated-grid"
|
||||
| "downward-rays-static"
|
||||
| "downward-rays-static-grid"
|
||||
| "glowing-orb"
|
||||
| "gradient-bars"
|
||||
| "radial-gradient"
|
||||
| "rotated-rays-animated"
|
||||
| "rotated-rays-animated-grid"
|
||||
| "rotated-rays-static"
|
||||
| "rotated-rays-static-grid"
|
||||
| "rotating-gradient"
|
||||
| "sparkles-gradient";
|
||||
|
||||
type AnimatedGridProps = React.ComponentProps<typeof AnimatedGridBackground>;
|
||||
type CanvasRevealProps = React.ComponentProps<typeof CanvasRevealBackground>;
|
||||
type CellWaveProps = React.ComponentProps<typeof CellWaveBackground>;
|
||||
type GlowingOrbProps = React.ComponentProps<typeof GlowingOrbBackground>;
|
||||
type GradientBarsProps = React.ComponentProps<typeof GradientBarsBackground>;
|
||||
type RadialGradientProps = React.ComponentProps<typeof RadialGradientBackground>;
|
||||
type RotatingGradientProps = React.ComponentProps<typeof RotatingGradientBackground>;
|
||||
type SparklesGradientProps = React.ComponentProps<typeof SparklesGradientBackground>;
|
||||
|
||||
export type HeroBackgroundVariantProps =
|
||||
| { variant: "plain" }
|
||||
| ({ variant: "animated-grid" } & AnimatedGridProps)
|
||||
| ({ variant: "canvas-reveal" } & CanvasRevealProps)
|
||||
| ({ variant: "cell-wave" } & CellWaveProps)
|
||||
| { variant: "downward-rays-animated" }
|
||||
| { variant: "downward-rays-animated-grid" }
|
||||
| { variant: "downward-rays-static" }
|
||||
| { variant: "downward-rays-static-grid" }
|
||||
| ({ variant: "glowing-orb" } & GlowingOrbProps)
|
||||
| ({ variant: "gradient-bars" } & GradientBarsProps)
|
||||
| ({ variant: "radial-gradient" } & RadialGradientProps)
|
||||
| { variant: "rotated-rays-animated" }
|
||||
| { variant: "rotated-rays-animated-grid" }
|
||||
| { variant: "rotated-rays-static" }
|
||||
| { variant: "rotated-rays-static-grid" }
|
||||
| ({ variant: "rotating-gradient" } & RotatingGradientProps)
|
||||
| ({ variant: "sparkles-gradient" } & SparklesGradientProps);
|
||||
|
||||
const heroBackgroundComponents = {
|
||||
"animated-grid": AnimatedGridBackground,
|
||||
"canvas-reveal": CanvasRevealBackground,
|
||||
"cell-wave": CellWaveBackground,
|
||||
"downward-rays": DownwardRaysBackground,
|
||||
"glowing-orb": GlowingOrbBackground,
|
||||
"gradient-bars": GradientBarsBackground,
|
||||
"radial-gradient": RadialGradientBackground,
|
||||
"rotated-rays": RotatedRaysBackground,
|
||||
"rotating-gradient": RotatingGradientBackground,
|
||||
"sparkles-gradient": SparklesGradientBackground,
|
||||
} as const;
|
||||
|
||||
const HeroBackgrounds = (props: HeroBackgroundVariantProps) => {
|
||||
if (props.variant === "plain") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { variant, ...restProps } = props;
|
||||
|
||||
// Handle rotated-rays preset variants
|
||||
if (variant === "rotated-rays-animated") {
|
||||
return <RotatedRaysBackground animated={true} showGrid={false} {...(restProps as any)} />;
|
||||
}
|
||||
if (variant === "rotated-rays-animated-grid") {
|
||||
return <RotatedRaysBackground animated={true} showGrid={true} {...(restProps as any)} />;
|
||||
}
|
||||
if (variant === "rotated-rays-static") {
|
||||
return <RotatedRaysBackground animated={false} showGrid={false} {...(restProps as any)} />;
|
||||
}
|
||||
if (variant === "rotated-rays-static-grid") {
|
||||
return <RotatedRaysBackground animated={false} showGrid={true} {...(restProps as any)} />;
|
||||
}
|
||||
|
||||
// Handle downward-rays preset variants
|
||||
if (variant === "downward-rays-animated") {
|
||||
return <DownwardRaysBackground animated={true} showGrid={false} {...(restProps as any)} />;
|
||||
}
|
||||
if (variant === "downward-rays-animated-grid") {
|
||||
return <DownwardRaysBackground animated={true} showGrid={true} {...(restProps as any)} />;
|
||||
}
|
||||
if (variant === "downward-rays-static") {
|
||||
return <DownwardRaysBackground animated={false} showGrid={false} {...(restProps as any)} />;
|
||||
}
|
||||
if (variant === "downward-rays-static-grid") {
|
||||
return <DownwardRaysBackground animated={false} showGrid={true} {...(restProps as any)} />;
|
||||
}
|
||||
|
||||
const BackgroundComponent = heroBackgroundComponents[variant];
|
||||
return <BackgroundComponent {...(restProps as any)} />;
|
||||
};
|
||||
|
||||
HeroBackgrounds.displayName = "HeroBackgrounds";
|
||||
|
||||
export default memo(HeroBackgrounds);
|
||||
31
src/components/background/NoiseBackground.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import React, { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface NoiseBackgroundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NoiseBackground = ({ className = "" }: NoiseBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls("fixed inset-0 -z-10 bg-accent/10",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-repeat mix-blend-overlay opacity-12"
|
||||
style={{
|
||||
backgroundImage: "url(/images/noise.webp)",
|
||||
backgroundSize: "512px"
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoiseBackground.displayName = "NoiseBackground";
|
||||
|
||||
export default memo(NoiseBackground);
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import React, { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface NoiseDiagonalGradientBackgroundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NoiseDiagonalGradientBackground = ({ className = "" }: NoiseDiagonalGradientBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls("fixed inset-0 -z-10 bg-accent/10",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden pointer-events-none opacity-100 bg-gradient-to-br from-background via-accent/20 to-primary-cta/20"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 bg-repeat mix-blend-overlay opacity-12"
|
||||
style={{
|
||||
backgroundImage: "url(/images/noise.webp)",
|
||||
backgroundSize: "512px"
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoiseDiagonalGradientBackground.displayName = "NoiseDiagonalGradientBackground";
|
||||
|
||||
export default memo(NoiseDiagonalGradientBackground);
|
||||
35
src/components/background/NoiseGradientBackground.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import React, { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface NoiseGradientBackgroundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NoiseGradientBackground = ({ className = "" }: NoiseGradientBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls("fixed inset-0 -z-10 bg-accent/10",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden pointer-events-none opacity-100 bg-gradient-to-r from-background via-accent/20 to-primary-cta/20"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 bg-repeat mix-blend-overlay opacity-12"
|
||||
style={{
|
||||
backgroundImage: "url(/images/noise.webp)",
|
||||
backgroundSize: "512px"
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoiseGradientBackground.displayName = "NoiseGradientBackground";
|
||||
|
||||
export default memo(NoiseGradientBackground);
|
||||
21
src/components/background/PlainBackground.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface PlainBackgroundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PlainBackground = ({ className = "" }: PlainBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls("fixed inset-0 -z-10 bg-background", className)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
PlainBackground.displayName = "PlainBackground";
|
||||
|
||||
export default memo(PlainBackground);
|
||||
40
src/components/background/RadialGradientBackground.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { cls } from '@/lib/utils';
|
||||
|
||||
interface RadialGradientBackgroundProps {
|
||||
className?: string;
|
||||
centerColor?: string;
|
||||
edgeColor?: string;
|
||||
size?: string;
|
||||
position?: string;
|
||||
}
|
||||
|
||||
const RadialGradientBackground = ({
|
||||
className = "",
|
||||
centerColor = "var(--background)",
|
||||
edgeColor = "var(--color-background-accent)",
|
||||
size = "130% 130%",
|
||||
position = "50% 15%",
|
||||
}: RadialGradientBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls("absolute inset-0 z-0 pointer-events-none select-none md:px-5 md:pb-5", className)}
|
||||
>
|
||||
<div
|
||||
className="relative w-full h-full rounded-b-theme-capped"
|
||||
style={{
|
||||
background: `radial-gradient(${size} at ${position}, ${centerColor} 40%, ${edgeColor} 100%)`,
|
||||
mask: 'linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 15%, rgb(0, 0, 0) 55%, rgb(0, 0, 0) 100%)',
|
||||
WebkitMask: 'linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 15%, rgb(0, 0, 0) 55%, rgb(0, 0, 0) 100%)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RadialGradientBackground.displayName = 'RadialGradientBackground';
|
||||
|
||||
export default memo(RadialGradientBackground);
|
||||
130
src/components/background/RotatedRaysBackground.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface RayConfig {
|
||||
width: number;
|
||||
opacity: number;
|
||||
rotation: number;
|
||||
scale?: number;
|
||||
left?: string;
|
||||
animationDuration: number;
|
||||
animationDelay: number;
|
||||
}
|
||||
|
||||
interface LightSourceConfig {
|
||||
width: number;
|
||||
height?: number;
|
||||
opacity: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
interface RotatedRaysBackgroundProps {
|
||||
animated: boolean;
|
||||
showGrid: boolean;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
const rays: RayConfig[] = [
|
||||
{ width: 35, opacity: 0.85, rotation: -18, animationDuration: 4, animationDelay: 0 },
|
||||
{ width: 35, opacity: 0.775, rotation: -12, animationDuration: 3.5, animationDelay: 0.5 },
|
||||
{ width: 20, opacity: 0.65, rotation: -5, scale: 0.90, animationDuration: 5, animationDelay: 1.2 },
|
||||
{ width: 15, opacity: 0.25, rotation: -3, animationDuration: 3, animationDelay: 0.3 },
|
||||
{ width: 40, opacity: 0.45, rotation: 0, scale: 0.79, animationDuration: 4.5, animationDelay: 0.8 },
|
||||
{ width: 20, opacity: 0.45, rotation: 6, animationDuration: 3.2, animationDelay: 1.5 },
|
||||
{ width: 35, opacity: 0.65, rotation: 9, scale: 0.83, animationDuration: 4.2, animationDelay: 0.2 },
|
||||
{ width: 35, opacity: 1, rotation: 14, scale: 0.9, animationDuration: 3.8, animationDelay: 1 },
|
||||
];
|
||||
|
||||
const lightSources: LightSourceConfig[] = [
|
||||
{ width: 1198, opacity: 0.05, top: -352 },
|
||||
{ width: 865, height: 929, opacity: 0.15, top: -252 },
|
||||
{ width: 865, height: 929, opacity: 0.15, top: -252 },
|
||||
];
|
||||
|
||||
const RotatedRaysBackground = ({
|
||||
animated,
|
||||
showGrid,
|
||||
className = "",
|
||||
containerClassName = "",
|
||||
}: RotatedRaysBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls("absolute inset-0 z-0 overflow-hidden pointer-events-none select-none", className)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{animated && (
|
||||
<style>
|
||||
{`
|
||||
@keyframes rotatedRayPulse {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: var(--target-opacity); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
)}
|
||||
|
||||
{showGrid && (
|
||||
<div
|
||||
className="absolute inset-0 -z-10 bg-background [mask-image:radial-gradient(50%_50%_at_50%_0%,white_0%,transparent_100%)]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to right, color-mix(in srgb, var(--color-background-accent) 20%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in srgb, var(--color-background-accent) 10%, transparent) 1px, transparent 1px)",
|
||||
backgroundSize: "10vw 10vw",
|
||||
backgroundRepeat: "repeat",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cls(
|
||||
"absolute overflow-hidden w-[1142px] h-[179vh] -top-[571px] -left-[373px]",
|
||||
"-rotate-[38deg] blur-[16px]",
|
||||
"[mask:radial-gradient(50%_109%,#000_0%,#000000f6_0%,transparent_96%)]",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
{rays.map((ray, index) => (
|
||||
<div
|
||||
key={`ray-${index}`}
|
||||
className="absolute overflow-hidden origin-top-right -top-[352px] -bottom-[920px] [background:radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
|
||||
style={{
|
||||
width: `${ray.width}px`,
|
||||
left: ray.left || `calc(50% - ${ray.width / 2}px)`,
|
||||
transform: `${ray.scale ? `scale(${ray.scale})` : ""} rotate(${ray.rotation}deg)`,
|
||||
...(animated
|
||||
? {
|
||||
"--target-opacity": ray.opacity,
|
||||
animation: `rotatedRayPulse ${ray.animationDuration}s ease-in-out ${ray.animationDelay}s infinite both`,
|
||||
}
|
||||
: {
|
||||
opacity: ray.opacity,
|
||||
}),
|
||||
} as React.CSSProperties}
|
||||
/>
|
||||
))}
|
||||
|
||||
{lightSources.map((source, index) => (
|
||||
<div
|
||||
key={`light-source-${index}`}
|
||||
className="absolute overflow-hidden [background:radial-gradient(50%_50%_at_50%_50%,var(--color-background-accent)_0%,transparent_100%)]"
|
||||
style={{
|
||||
width: `${source.width}px`,
|
||||
height: source.height ? `${source.height}px` : undefined,
|
||||
top: `${source.top}px`,
|
||||
bottom: source.height ? undefined : "-46px",
|
||||
left: `calc(50% - ${source.width / 2}px)`,
|
||||
opacity: source.opacity,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RotatedRaysBackground.displayName = "RotatedRaysBackground";
|
||||
|
||||
export default memo(RotatedRaysBackground);
|
||||
77
src/components/background/RotatingGradientBackground.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { cls } from '@/lib/utils';
|
||||
import { Sparkles } from './Sparkles';
|
||||
|
||||
interface RotatingGradientBackgroundProps {
|
||||
className?: string;
|
||||
gradientColorStart?: string;
|
||||
gradientColorEnd?: string;
|
||||
bigCircleSize?: string;
|
||||
smallCircleSize?: string;
|
||||
blurAmount?: string;
|
||||
opacity?: number;
|
||||
showSparkles?: boolean;
|
||||
}
|
||||
|
||||
const RotatingGradientBackground = ({
|
||||
className = "",
|
||||
gradientColorStart = "var(--color-background-accent)",
|
||||
gradientColorEnd = "var(--color-background-accent)",
|
||||
bigCircleSize = "28vw",
|
||||
smallCircleSize = "21vw",
|
||||
blurAmount = "10px",
|
||||
opacity = 0.6,
|
||||
showSparkles = true,
|
||||
}: RotatingGradientBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls("absolute inset-0 z-0 overflow-hidden pointer-events-none select-none", className)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
style={{
|
||||
filter: `blur(${blurAmount})`,
|
||||
WebkitFilter: `blur(${blurAmount})`,
|
||||
opacity,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full aspect-square animate-spin-slow opacity-75"
|
||||
style={{
|
||||
width: bigCircleSize,
|
||||
height: bigCircleSize,
|
||||
background: `linear-gradient(229deg, ${gradientColorStart} 10%, color-mix(in srgb, ${gradientColorStart} 0%, transparent) 40%, color-mix(in srgb, ${gradientColorEnd} 0%, transparent) 64%, ${gradientColorEnd} 88%)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full aspect-square animate-spin-reverse opacity-75"
|
||||
style={{
|
||||
width: smallCircleSize,
|
||||
height: smallCircleSize,
|
||||
background: `linear-gradient(141deg, ${gradientColorStart} 13%, color-mix(in srgb, ${gradientColorStart} 0%, transparent) 37.5%, color-mix(in srgb, ${gradientColorEnd} 0%, transparent) 64%, ${gradientColorEnd} 88%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{showSparkles && (
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
mask: 'radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 22%, rgb(0, 0, 0) 32%, rgb(0, 0, 0) 55%, rgba(0, 0, 0, 0) 75%, rgba(0, 0, 0, 0) 100%), linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 15%, rgb(0, 0, 0) 85%, rgba(0, 0, 0, 0) 100%)',
|
||||
maskComposite: 'intersect',
|
||||
WebkitMask: 'radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 22%, rgb(0, 0, 0) 32%, rgb(0, 0, 0) 55%, rgba(0, 0, 0, 0) 75%, rgba(0, 0, 0, 0) 100%), linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0) 15%, rgb(0, 0, 0) 85%, rgba(0, 0, 0, 0) 100%)',
|
||||
WebkitMaskComposite: 'source-in',
|
||||
}}
|
||||
>
|
||||
<Sparkles particleDensity={60} minSize={0.3} maxSize={0.8} speed={3} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RotatingGradientBackground.displayName = 'RotatingGradientBackground';
|
||||
|
||||
export default memo(RotatingGradientBackground);
|
||||
460
src/components/background/Sparkles.tsx
Normal file
@@ -0,0 +1,460 @@
|
||||
"use client";
|
||||
|
||||
import { useId, useEffect, useState } from "react";
|
||||
import Particles, { initParticlesEngine } from "@tsparticles/react";
|
||||
import type { Container, SingleOrMultiple } from "@tsparticles/engine";
|
||||
import { loadSlim } from "@tsparticles/slim";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { motion, useAnimation } from "framer-motion";
|
||||
|
||||
type SparklesProps = {
|
||||
id?: string;
|
||||
className?: string;
|
||||
background?: string;
|
||||
particleSize?: number;
|
||||
minSize?: number;
|
||||
maxSize?: number;
|
||||
speed?: number;
|
||||
particleColor?: string;
|
||||
particleDensity?: number;
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
minSize: 0.5,
|
||||
maxSize: 1,
|
||||
speed: 4,
|
||||
particleDensity: 100,
|
||||
particleColor: "var(--color-primary-cta)",
|
||||
background: "transparent",
|
||||
};
|
||||
|
||||
export const Sparkles = (props: SparklesProps) => {
|
||||
const {
|
||||
id,
|
||||
className,
|
||||
background = defaultProps.background,
|
||||
minSize = defaultProps.minSize,
|
||||
maxSize = defaultProps.maxSize,
|
||||
speed = defaultProps.speed,
|
||||
particleColor = defaultProps.particleColor,
|
||||
particleDensity = defaultProps.particleDensity,
|
||||
} = props;
|
||||
const [init, setInit] = useState(false);
|
||||
const [resolvedColor, setResolvedColor] = useState("#ffffff");
|
||||
|
||||
useEffect(() => {
|
||||
if (particleColor?.startsWith('var(')) {
|
||||
const varName = particleColor.match(/var\((.*?)\)/)?.[1];
|
||||
if (varName) {
|
||||
const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
|
||||
if (computed) {
|
||||
setResolvedColor(computed);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setResolvedColor(particleColor || "#ffffff");
|
||||
}
|
||||
}, [particleColor]);
|
||||
|
||||
useEffect(() => {
|
||||
initParticlesEngine(async (engine) => {
|
||||
await loadSlim(engine);
|
||||
}).then(() => {
|
||||
setInit(true);
|
||||
});
|
||||
}, []);
|
||||
const controls = useAnimation();
|
||||
|
||||
const particlesLoaded = async (container?: Container) => {
|
||||
if (container) {
|
||||
controls.start({
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const generatedId = useId();
|
||||
return (
|
||||
<motion.div animate={controls} className={cls("absolute inset-0 opacity-0", className)}>
|
||||
{init && (
|
||||
<Particles
|
||||
id={id || generatedId}
|
||||
className={cls("h-full w-full")}
|
||||
particlesLoaded={particlesLoaded}
|
||||
options={{
|
||||
background: {
|
||||
color: {
|
||||
value: background || "transparent",
|
||||
},
|
||||
},
|
||||
fullScreen: {
|
||||
enable: false,
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
fpsLimit: 120,
|
||||
interactivity: {
|
||||
events: {
|
||||
onClick: {
|
||||
enable: true,
|
||||
mode: "push",
|
||||
},
|
||||
onHover: {
|
||||
enable: false,
|
||||
mode: "repulse",
|
||||
},
|
||||
resize: true as any,
|
||||
},
|
||||
modes: {
|
||||
push: {
|
||||
quantity: 4,
|
||||
},
|
||||
repulse: {
|
||||
distance: 200,
|
||||
duration: 0.4,
|
||||
},
|
||||
},
|
||||
},
|
||||
particles: {
|
||||
bounce: {
|
||||
horizontal: {
|
||||
value: 1,
|
||||
},
|
||||
vertical: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
collisions: {
|
||||
absorb: {
|
||||
speed: 2,
|
||||
},
|
||||
bounce: {
|
||||
horizontal: {
|
||||
value: 1,
|
||||
},
|
||||
vertical: {
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
enable: false,
|
||||
maxSpeed: 50,
|
||||
mode: "bounce",
|
||||
overlap: {
|
||||
enable: true,
|
||||
retries: 0,
|
||||
},
|
||||
},
|
||||
color: {
|
||||
value: resolvedColor,
|
||||
animation: {
|
||||
h: {
|
||||
count: 0,
|
||||
enable: false,
|
||||
speed: 1,
|
||||
decay: 0,
|
||||
delay: 0,
|
||||
sync: true,
|
||||
offset: 0,
|
||||
},
|
||||
s: {
|
||||
count: 0,
|
||||
enable: false,
|
||||
speed: 1,
|
||||
decay: 0,
|
||||
delay: 0,
|
||||
sync: true,
|
||||
offset: 0,
|
||||
},
|
||||
l: {
|
||||
count: 0,
|
||||
enable: false,
|
||||
speed: 1,
|
||||
decay: 0,
|
||||
delay: 0,
|
||||
sync: true,
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
effect: {
|
||||
close: true,
|
||||
fill: true,
|
||||
options: {},
|
||||
type: {} as SingleOrMultiple<string> | undefined,
|
||||
},
|
||||
groups: {},
|
||||
move: {
|
||||
angle: {
|
||||
offset: 0,
|
||||
value: 90,
|
||||
},
|
||||
attract: {
|
||||
distance: 200,
|
||||
enable: false,
|
||||
rotate: {
|
||||
x: 3000,
|
||||
y: 3000,
|
||||
},
|
||||
},
|
||||
center: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
mode: "percent",
|
||||
radius: 0,
|
||||
},
|
||||
decay: 0,
|
||||
distance: {},
|
||||
direction: "none",
|
||||
drift: 0,
|
||||
enable: true,
|
||||
gravity: {
|
||||
acceleration: 9.81,
|
||||
enable: false,
|
||||
inverse: false,
|
||||
maxSpeed: 50,
|
||||
},
|
||||
path: {
|
||||
clamp: true,
|
||||
delay: {
|
||||
value: 0,
|
||||
},
|
||||
enable: false,
|
||||
options: {},
|
||||
},
|
||||
outModes: {
|
||||
default: "out",
|
||||
},
|
||||
random: false,
|
||||
size: false,
|
||||
speed: {
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
},
|
||||
spin: {
|
||||
acceleration: 0,
|
||||
enable: false,
|
||||
},
|
||||
straight: false,
|
||||
trail: {
|
||||
enable: false,
|
||||
length: 10,
|
||||
fill: {},
|
||||
},
|
||||
vibrate: false,
|
||||
warp: false,
|
||||
},
|
||||
number: {
|
||||
density: {
|
||||
enable: true,
|
||||
width: 400,
|
||||
height: 400,
|
||||
},
|
||||
limit: {
|
||||
mode: "delete",
|
||||
value: 0,
|
||||
},
|
||||
value: particleDensity || 120,
|
||||
},
|
||||
opacity: {
|
||||
value: {
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
},
|
||||
animation: {
|
||||
count: 0,
|
||||
enable: true,
|
||||
speed: speed || 4,
|
||||
decay: 0,
|
||||
delay: 0,
|
||||
sync: false,
|
||||
mode: "auto",
|
||||
startValue: "random",
|
||||
destroy: "none",
|
||||
},
|
||||
},
|
||||
reduceDuplicates: false,
|
||||
shadow: {
|
||||
blur: 0,
|
||||
color: {
|
||||
value: "#000",
|
||||
},
|
||||
enable: false,
|
||||
offset: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
close: true,
|
||||
fill: true,
|
||||
options: {},
|
||||
type: "circle",
|
||||
},
|
||||
size: {
|
||||
value: {
|
||||
min: minSize || 1,
|
||||
max: maxSize || 3,
|
||||
},
|
||||
animation: {
|
||||
count: 0,
|
||||
enable: false,
|
||||
speed: 5,
|
||||
decay: 0,
|
||||
delay: 0,
|
||||
sync: false,
|
||||
mode: "auto",
|
||||
startValue: "random",
|
||||
destroy: "none",
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 0,
|
||||
},
|
||||
zIndex: {
|
||||
value: 0,
|
||||
opacityRate: 1,
|
||||
sizeRate: 1,
|
||||
velocityRate: 1,
|
||||
},
|
||||
destroy: {
|
||||
bounds: {},
|
||||
mode: "none",
|
||||
split: {
|
||||
count: 1,
|
||||
factor: {
|
||||
value: 3,
|
||||
},
|
||||
rate: {
|
||||
value: {
|
||||
min: 4,
|
||||
max: 9,
|
||||
},
|
||||
},
|
||||
sizeOffset: true,
|
||||
},
|
||||
},
|
||||
roll: {
|
||||
darken: {
|
||||
enable: false,
|
||||
value: 0,
|
||||
},
|
||||
enable: false,
|
||||
enlighten: {
|
||||
enable: false,
|
||||
value: 0,
|
||||
},
|
||||
mode: "vertical",
|
||||
speed: 25,
|
||||
},
|
||||
tilt: {
|
||||
value: 0,
|
||||
animation: {
|
||||
enable: false,
|
||||
speed: 0,
|
||||
decay: 0,
|
||||
sync: false,
|
||||
},
|
||||
direction: "clockwise",
|
||||
enable: false,
|
||||
},
|
||||
twinkle: {
|
||||
lines: {
|
||||
enable: false,
|
||||
frequency: 0.05,
|
||||
opacity: 1,
|
||||
},
|
||||
particles: {
|
||||
enable: false,
|
||||
frequency: 0.05,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
wobble: {
|
||||
distance: 5,
|
||||
enable: false,
|
||||
speed: {
|
||||
angle: 50,
|
||||
move: 10,
|
||||
},
|
||||
},
|
||||
life: {
|
||||
count: 0,
|
||||
delay: {
|
||||
value: 0,
|
||||
sync: false,
|
||||
},
|
||||
duration: {
|
||||
value: 0,
|
||||
sync: false,
|
||||
},
|
||||
},
|
||||
rotate: {
|
||||
value: 0,
|
||||
animation: {
|
||||
enable: false,
|
||||
speed: 0,
|
||||
decay: 0,
|
||||
sync: false,
|
||||
},
|
||||
direction: "clockwise",
|
||||
path: false,
|
||||
},
|
||||
orbit: {
|
||||
animation: {
|
||||
count: 0,
|
||||
enable: false,
|
||||
speed: 1,
|
||||
decay: 0,
|
||||
delay: 0,
|
||||
sync: false,
|
||||
},
|
||||
enable: false,
|
||||
opacity: 1,
|
||||
rotation: {
|
||||
value: 45,
|
||||
},
|
||||
width: 1,
|
||||
},
|
||||
links: {
|
||||
blink: false,
|
||||
color: {
|
||||
value: "#fff",
|
||||
},
|
||||
consent: false,
|
||||
distance: 100,
|
||||
enable: false,
|
||||
frequency: 1,
|
||||
opacity: 1,
|
||||
shadow: {
|
||||
blur: 5,
|
||||
color: {
|
||||
value: "#000",
|
||||
},
|
||||
enable: false,
|
||||
},
|
||||
triangles: {
|
||||
enable: false,
|
||||
frequency: 1,
|
||||
},
|
||||
width: 1,
|
||||
warp: false,
|
||||
},
|
||||
repulse: {
|
||||
value: 0,
|
||||
enabled: false,
|
||||
distance: 1,
|
||||
duration: 1,
|
||||
factor: 1,
|
||||
speed: 1,
|
||||
},
|
||||
},
|
||||
detectRetina: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
56
src/components/background/SparklesGradientBackground.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { cls } from '@/lib/utils';
|
||||
import { Sparkles } from './Sparkles';
|
||||
|
||||
interface SparklesGradientBackgroundProps {
|
||||
className?: string;
|
||||
gradientColor?: string;
|
||||
accentColor?: string;
|
||||
blurAmount?: string;
|
||||
}
|
||||
|
||||
const SparklesGradientBackground = ({
|
||||
className = "",
|
||||
gradientColor = "var(--color-background-accent)",
|
||||
accentColor = "var(--color-background-accent)",
|
||||
blurAmount = "6vw",
|
||||
}: SparklesGradientBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls("absolute inset-0 z-0 overflow-hidden pointer-events-none select-none", className)}
|
||||
style={{
|
||||
mask: 'radial-gradient(ellipse 100% 100% at 50% 0%, rgb(0, 0, 0) 0%, rgba(0, 0, 0, 0) 70%)',
|
||||
WebkitMask: 'radial-gradient(ellipse 100% 100% at 50% 0%, rgb(0, 0, 0) 0%, rgba(0, 0, 0, 0) 70%)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className="absolute left-1/2 -translate-x-1/2 w-[65vw] h-[88vh] -top-[59vh] overflow-visible z-0"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 rounded-[100%] overflow-hidden"
|
||||
style={{
|
||||
background: `radial-gradient(50% 50% at 50% 50%, ${gradientColor}, color-mix(in srgb, ${gradientColor} 25%, transparent) 41%, color-mix(in srgb, ${gradientColor} 20%, transparent))`,
|
||||
filter: `blur(${blurAmount})`,
|
||||
WebkitFilter: `blur(${blurAmount})`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[33vw] h-[53vh] rounded-[100%] overflow-hidden"
|
||||
style={{
|
||||
background: `color-mix(in srgb, ${accentColor} 30%, transparent)`,
|
||||
filter: `blur(${blurAmount})`,
|
||||
WebkitFilter: `blur(${blurAmount})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Sparkles />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SparklesGradientBackground.displayName = 'SparklesGradientBackground';
|
||||
|
||||
export default memo(SparklesGradientBackground);
|
||||
@@ -0,0 +1,102 @@
|
||||
.floating-gradient-background-container {
|
||||
--circle-size: 80%;
|
||||
--circle-size-small: 60%;
|
||||
--blending: hard-light;
|
||||
}
|
||||
|
||||
.floating-gradient-background-circle-one {
|
||||
background: radial-gradient(circle at center, var(--color-background-accent) 0, rgba(255, 255, 255, 0) 50%) no-repeat;
|
||||
mix-blend-mode: var(--blending);
|
||||
width: var(--circle-size);
|
||||
height: var(--circle-size);
|
||||
top: calc(50% - var(--circle-size-small) / 2);
|
||||
left: calc(50% - var(--circle-size-small) / 2);
|
||||
transform-origin: center center;
|
||||
animation: moveVertical 20s ease infinite;
|
||||
}
|
||||
|
||||
.floating-gradient-background-circle-two {
|
||||
background: radial-gradient(circle at center, var(--color-accent) 0, rgba(255, 255, 255, 0) 50%) no-repeat;
|
||||
mix-blend-mode: var(--blending);
|
||||
width: var(--circle-size);
|
||||
height: var(--circle-size);
|
||||
top: calc(50% - var(--circle-size-small) / 2);
|
||||
left: calc(50% - var(--circle-size-small) / 2);
|
||||
transform-origin: calc(50% - 400px);
|
||||
animation: moveInCircle 20s reverse infinite;
|
||||
}
|
||||
|
||||
.floating-gradient-background-circle-three {
|
||||
background: radial-gradient(circle at center, var(--color-primary-cta) 0, rgba(255, 255, 255, 0) 50%) no-repeat;
|
||||
mix-blend-mode: var(--blending);
|
||||
width: var(--circle-size-small);
|
||||
height: var(--circle-size-small);
|
||||
top: calc(50% - var(--circle-size) / 2 + 200px);
|
||||
left: calc(50% - var(--circle-size) / 2 - 500px);
|
||||
transform-origin: calc(50% + 400px);
|
||||
animation: moveInCircle 30s linear infinite;
|
||||
}
|
||||
|
||||
.floating-gradient-background-circle-four {
|
||||
background: radial-gradient(circle at center, var(--color-background-accent) 0, rgba(255, 255, 255, 0) 50%) no-repeat;
|
||||
mix-blend-mode: var(--blending);
|
||||
width: var(--circle-size-small);
|
||||
height: var(--circle-size-small);
|
||||
top: calc(50% - var(--circle-size) / 2);
|
||||
left: calc(50% - var(--circle-size) / 2);
|
||||
transform-origin: calc(50% - 200px);
|
||||
animation: moveHorizontal 30s ease infinite;
|
||||
}
|
||||
|
||||
.floating-gradient-background-circle-five {
|
||||
background: radial-gradient(circle at center, var(--color-primary-cta) 0, rgba(255, 255, 255, 0) 50%) no-repeat;
|
||||
mix-blend-mode: var(--blending);
|
||||
width: calc(var(--circle-size-small) * 2);
|
||||
height: calc(var(--circle-size-small) * 2);
|
||||
top: calc(50% - var(--circle-size));
|
||||
left: calc(50% - var(--circle-size));
|
||||
transform-origin: calc(50% - 800px) calc(50% + 200px);
|
||||
animation: moveInCircle 20s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes moveInCircle {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveVertical {
|
||||
0% {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(50%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveHorizontal {
|
||||
0% {
|
||||
transform: translateX(-50%) translateY(-10%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(50%) translateY(10%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-50%) translateY(-10%);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import "./FloatingGradientBackground.css";
|
||||
|
||||
interface FloatingGradientBackgroundProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const FloatingGradientBackground = ({
|
||||
className = "",
|
||||
}: FloatingGradientBackgroundProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"fixed top-0 bottom-0 left-0 right-0 w-full h-full z-0 pointer-events-none blur-[40px]",
|
||||
"[mask-image:linear-gradient(to_bottom,transparent,#010101_20%,#010101_80%,transparent)]",
|
||||
"[mask-composite:intersect]",
|
||||
"[-webkit-mask-image:linear-gradient(to_bottom,transparent,#010101_20%,#010101_80%,transparent)]",
|
||||
"[-webkit-mask-composite:destination-in]",
|
||||
"floating-gradient-background-container",
|
||||
className
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute opacity-[0.075] floating-gradient-background-circle-one" />
|
||||
<div className="absolute opacity-[0.125] floating-gradient-background-circle-two" />
|
||||
<div className="absolute opacity-[0.125] floating-gradient-background-circle-three" />
|
||||
<div className="absolute opacity-[0.15] floating-gradient-background-circle-four" />
|
||||
<div className="absolute opacity-[0.075] floating-gradient-background-circle-five" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FloatingGradientBackground.displayName = "FloatingGradientBackground";
|
||||
|
||||
export default memo(FloatingGradientBackground);
|
||||
122
src/components/bento/Bento3DCardGrid.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
export type GridCardItem = {
|
||||
name: string;
|
||||
icon: LucideIcon;
|
||||
};
|
||||
|
||||
interface Bento3DCardGridProps {
|
||||
useInvertedBackground: InvertedBackground;
|
||||
items: [GridCardItem, GridCardItem, GridCardItem, GridCardItem];
|
||||
centerIcon: LucideIcon;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const gridItemStyle = {
|
||||
perspective: '1000px',
|
||||
transformStyle: 'preserve-3d' as const,
|
||||
};
|
||||
|
||||
const EmptyCell = () => (
|
||||
<div
|
||||
className="relative aspect-square card shadow rounded-theme-capped opacity-50"
|
||||
style={gridItemStyle}
|
||||
/>
|
||||
);
|
||||
|
||||
const cardTranslateZ = [
|
||||
'group-hover:[transform:translateZ(10px)]',
|
||||
'group-hover:[transform:translateZ(14px)]',
|
||||
'group-hover:[transform:translateZ(18px)]',
|
||||
'group-hover:[transform:translateZ(22px)]',
|
||||
] as const;
|
||||
|
||||
const CardCell = ({ name, Icon, cardIndex }: { name: string; Icon: LucideIcon; cardIndex: number }) => (
|
||||
<div
|
||||
className={cls(
|
||||
"relative card shadow aspect-square rounded-theme-capped flex flex-col justify-between p-3 transition-transform duration-500",
|
||||
cardTranslateZ[cardIndex]
|
||||
)}
|
||||
style={gridItemStyle}
|
||||
>
|
||||
<div className="h-6 w-[var(--height-6)] aspect-square rounded-theme primary-button flex items-center justify-center">
|
||||
<Icon className="h-4/10 w-4/10 text-background" strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className="text-xs text-foreground leading-tight line-clamp-4">{name}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CenterCell = ({ Icon }: { Icon: LucideIcon }) => (
|
||||
<div
|
||||
className="aspect-square flex items-center justify-center bg-transparent border-none overflow-visible"
|
||||
style={gridItemStyle}
|
||||
>
|
||||
<div className="card shadow rounded-full h-6/10 aspect-square flex items-center justify-center">
|
||||
<Icon className="h-4/10 w-4/10 text-foreground" strokeWidth={1.25} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Bento3DCardGrid = ({
|
||||
useInvertedBackground,
|
||||
items,
|
||||
centerIcon: CenterIcon,
|
||||
className = "",
|
||||
}: Bento3DCardGridProps) => {
|
||||
void useInvertedBackground;
|
||||
|
||||
const gridPositions = [
|
||||
{ type: 'empty' },
|
||||
{ type: 'card', index: 0 },
|
||||
{ type: 'empty' },
|
||||
{ type: 'card', index: 1 },
|
||||
{ type: 'center' },
|
||||
{ type: 'card', index: 2 },
|
||||
{ type: 'empty' },
|
||||
{ type: 'card', index: 3 },
|
||||
{ type: 'empty' },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls("group w-full h-full", className)}
|
||||
style={{
|
||||
maskImage: 'linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%), linear-gradient(to bottom, transparent 0%, black 5%, black 95%, transparent 100%)',
|
||||
maskComposite: 'intersect',
|
||||
WebkitMaskImage: 'linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%), linear-gradient(to bottom, transparent 0%, black 5%, black 95%, transparent 100%)',
|
||||
WebkitMaskComposite: 'source-in',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-full h-full grid grid-cols-3 gap-4 -translate-y-9 -translate-x-8"
|
||||
style={{
|
||||
gridAutoRows: '1fr',
|
||||
perspective: '5000px',
|
||||
transformStyle: 'preserve-3d',
|
||||
transform: 'rotateX(45deg) rotateY(20deg) rotate(-25deg) scale(1.1)',
|
||||
}}
|
||||
>
|
||||
{gridPositions.map((pos, index) => {
|
||||
switch (pos.type) {
|
||||
case 'card':
|
||||
const item = items[pos.index];
|
||||
return <CardCell key={index} name={item.name} Icon={item.icon} cardIndex={pos.index} />;
|
||||
case 'center':
|
||||
return <CenterCell key={index} Icon={CenterIcon} />;
|
||||
default:
|
||||
return <EmptyCell key={index} />;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Bento3DCardGrid.displayName = "Bento3DCardGrid";
|
||||
|
||||
export default memo(Bento3DCardGrid);
|
||||
117
src/components/bento/Bento3DStackCards.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
interface StackCardProps {
|
||||
Icon: LucideIcon;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
detail: string;
|
||||
iconClassName?: string;
|
||||
titleClassName?: string;
|
||||
subtitleClassName?: string;
|
||||
detailClassName?: string;
|
||||
}
|
||||
|
||||
interface Bento3DStackCardProps extends StackCardProps {
|
||||
className?: string;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
}
|
||||
|
||||
const StackCard = memo(({
|
||||
className = "",
|
||||
Icon,
|
||||
title,
|
||||
subtitle,
|
||||
detail,
|
||||
iconClassName = "",
|
||||
titleClassName = "",
|
||||
subtitleClassName = "",
|
||||
detailClassName = "",
|
||||
useInvertedBackground,
|
||||
}: Bento3DStackCardProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"relative flex h-35 w-80 md:w-25 p-6 -skew-y-[8deg] card rounded-theme-capped flex-col justify-between transition-all duration-700",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cls(
|
||||
"relative h-5 aspect-square primary-button rounded-theme flex items-center justify-center",
|
||||
iconClassName
|
||||
)}
|
||||
>
|
||||
<Icon className="h-1/2 w-auto aspect-square text-background" strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className={cls("text-base", shouldUseLightText ? "text-background" : "text-foreground", titleClassName)}>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
<p className={cls("whitespace-nowrap text-lg", shouldUseLightText ? "text-background" : "text-foreground", subtitleClassName)}>
|
||||
{subtitle}
|
||||
</p>
|
||||
<p className={cls("text-base", shouldUseLightText ? "text-background" : "text-foreground", detailClassName)}>
|
||||
{detail}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
StackCard.displayName = "StackCard";
|
||||
|
||||
interface Bento3DStackCardsProps {
|
||||
cards: StackCardProps[];
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Bento3DStackCards = ({
|
||||
cards,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
}: Bento3DStackCardsProps) => {
|
||||
const baseClassNames = [
|
||||
"[grid-area:stack] -translate-y-14 hover:-translate-y-20",
|
||||
"[grid-area:stack] translate-x-15 translate-y-0 hover:-translate-y-5",
|
||||
"[grid-area:stack] translate-x-31 translate-y-15 hover:translate-y-10",
|
||||
];
|
||||
|
||||
const displayCards = cards.slice(0, 3).map((card, index) => ({
|
||||
...card,
|
||||
className: `${baseClassNames[index]} ${card.iconClassName || ""}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls("h-full grid [grid-template-areas:'stack'] place-items-center opacity-100 animate-in fade-in-0 duration-700", className)}
|
||||
style={{
|
||||
maskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, black 0%, black 80%, transparent 100%)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, black 0%, black 80%, transparent 100%)",
|
||||
maskComposite: "intersect",
|
||||
WebkitMaskComposite: "source-in"
|
||||
}}
|
||||
>
|
||||
{displayCards.map((cardProps, index) => (
|
||||
<StackCard
|
||||
key={index}
|
||||
{...cardProps}
|
||||
useInvertedBackground={useInvertedBackground}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Bento3DStackCards.displayName = "Bento3DStackCards";
|
||||
|
||||
export default memo(Bento3DStackCards);
|
||||
97
src/components/bento/Bento3DTaskList.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { memo, Fragment } from "react";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
export type TaskItem = {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
interface Bento3DTaskListProps {
|
||||
title: string;
|
||||
items: TaskItem[];
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Bento3DTaskList = ({
|
||||
title,
|
||||
items,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
}: Bento3DTaskListProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls("h-full w-full flex items-center justify-center", className)}
|
||||
style={{
|
||||
perspective: "1200px",
|
||||
transformStyle: "preserve-3d",
|
||||
maskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
|
||||
maskComposite: "intersect",
|
||||
WebkitMaskComposite: "source-in"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"relative w-80 md:w-25 p-6 card rounded-theme-capped flex flex-col gap-3 translate-x-4 -translate-y-5"
|
||||
)}
|
||||
style={{
|
||||
transform: "rotateX(30deg) rotateY(30deg) rotateZ(-30deg)",
|
||||
transformStyle: "preserve-3d"
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-[var(--text-base)] w-auto aspect-square rounded-theme primary-button" />
|
||||
<h3 className={cls("text-base leading-tight", shouldUseLightText ? "text-background" : "text-foreground")}>
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full min-w-0 secondary-button rounded-theme-capped flex flex-col p-5 gap-3">
|
||||
{items.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<div
|
||||
className={cls(
|
||||
"w-full min-w-0 flex items-center justify-between gap-3"
|
||||
)}
|
||||
>
|
||||
<div className="w-full min-w-0 flex items-center gap-3">
|
||||
<div
|
||||
className="h-6 w-auto aspect-square rounded-theme flex items-center justify-center primary-button"
|
||||
>
|
||||
<Icon className="h-4/10 w-4/10 aspect-square text-background" strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className={cls("text-sm truncate", shouldUseLightText ? "text-background" : "text-foreground")}>
|
||||
{item.label}
|
||||
</p>
|
||||
</div>
|
||||
<p className={cls("text-xs text-nowrap", shouldUseLightText ? "text-background/75" : "text-foreground/75")}>
|
||||
{item.time}
|
||||
</p>
|
||||
</div>
|
||||
{index !== items.length - 1 && (
|
||||
<div className="h-px bg-background-accent/50" />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Bento3DTaskList.displayName = "Bento3DTaskList";
|
||||
|
||||
export default memo(Bento3DTaskList);
|
||||
77
src/components/bento/BentoAnimatedBarChart.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useState, useEffect } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type BarData = {
|
||||
defaultHeight: number;
|
||||
hoverHeight: number;
|
||||
};
|
||||
|
||||
interface BentoAnimatedBarChartProps {
|
||||
bars?: BarData[];
|
||||
className?: string;
|
||||
barClassName?: string;
|
||||
}
|
||||
|
||||
const defaultBars: BarData[] = [
|
||||
{ defaultHeight: 100, hoverHeight: 40 },
|
||||
{ defaultHeight: 84, hoverHeight: 100 },
|
||||
{ defaultHeight: 62, hoverHeight: 75 },
|
||||
{ defaultHeight: 90, hoverHeight: 50 },
|
||||
{ defaultHeight: 70, hoverHeight: 90 },
|
||||
{ defaultHeight: 50, hoverHeight: 60 },
|
||||
{ defaultHeight: 75, hoverHeight: 85 },
|
||||
{ defaultHeight: 80, hoverHeight: 70 },
|
||||
];
|
||||
|
||||
const BentoAnimatedBarChart = ({
|
||||
bars = defaultBars,
|
||||
className = "",
|
||||
barClassName = "",
|
||||
}: BentoAnimatedBarChartProps) => {
|
||||
const [activeBar, setActiveBar] = useState(2); // Start at third bar (index 2)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setActiveBar((prev) => (prev + 1) % bars.length);
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [bars.length]);
|
||||
|
||||
return (
|
||||
<div className={cls("group w-full h-full [mask-image:linear-gradient(to_bottom,black_40%,transparent_100%)]", className)}>
|
||||
<style>{`
|
||||
.bento-bar {
|
||||
height: var(--default-height);
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.group:hover .bento-bar {
|
||||
height: var(--hover-height) !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div className="w-full h-full flex items-end gap-5">
|
||||
{bars.map((bar, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls("relative bento-bar w-full rounded-theme transition-all duration-500 ease bg-background-accent", barClassName)}
|
||||
style={
|
||||
{
|
||||
"--default-height": `${bar.defaultHeight}%`,
|
||||
"--hover-height": `${bar.hoverHeight}%`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div className={cls("absolute! inset-0 primary-button rounded-theme transition-opacity ease-in-out duration-500", activeBar === index ? "opacity-100" : "opacity-0")} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BentoAnimatedBarChart.displayName = "BentoAnimatedBarChart";
|
||||
|
||||
export default memo(BentoAnimatedBarChart);
|
||||
96
src/components/bento/BentoChatAnimation.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { Send } from "lucide-react";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
export type ChatExchange = {
|
||||
userMessage: string;
|
||||
aiResponse: string;
|
||||
};
|
||||
|
||||
interface BentoChatAnimationProps {
|
||||
aiIcon: LucideIcon;
|
||||
userIcon: LucideIcon;
|
||||
exchanges: ChatExchange[];
|
||||
placeholder: string;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const BentoChatAnimation = ({
|
||||
aiIcon: AiIcon,
|
||||
userIcon: UserIcon,
|
||||
exchanges,
|
||||
placeholder,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
}: BentoChatAnimationProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
const messages = exchanges.flatMap((exchange) => [
|
||||
{ content: exchange.userMessage, isUser: true },
|
||||
{ content: exchange.aiResponse, isUser: false },
|
||||
]);
|
||||
const duplicatedMessages = [...messages, ...messages];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"relative h-full w-full flex flex-col overflow-hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 overflow-hidden mask-fade-y">
|
||||
<div className="flex flex-col animate-marquee-vertical px-4">
|
||||
{duplicatedMessages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cls(
|
||||
"flex items-end gap-2 shrink-0 mb-4",
|
||||
message.isUser ? "flex-row-reverse" : "flex-row"
|
||||
)}
|
||||
>
|
||||
{message.isUser ? (
|
||||
<div className="shrink-0 h-8 aspect-square rounded-theme primary-button flex items-center justify-center">
|
||||
<UserIcon className="h-4/10 w-auto text-background" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="shrink-0 h-8 aspect-square rounded-theme card shadow flex items-center justify-center">
|
||||
<AiIcon className={cls("h-4/10 w-auto", shouldUseLightText ? "text-background" : "text-foreground")} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cls(
|
||||
"max-w-75/100 px-4 py-3 text-sm leading-tight",
|
||||
message.isUser
|
||||
? "primary-button rounded-theme-capped rounded-br-none text-background"
|
||||
: "card rounded-theme-capped rounded-bl-none",
|
||||
!message.isUser && (shouldUseLightText ? "text-background" : "text-foreground")
|
||||
)}
|
||||
>
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card shadow rounded-theme p-2 pl-5 flex items-center gap-2">
|
||||
<p className={cls("flex-1 text-sm truncate", shouldUseLightText ? "text-background/75" : "text-foreground/75")}>
|
||||
{placeholder}
|
||||
</p>
|
||||
<div className="h-7 w-auto aspect-square primary-button rounded-theme flex items-center justify-center">
|
||||
<Send className="h-4/10 w-auto text-background" strokeWidth={1.75} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BentoChatAnimation.displayName = "BentoChatAnimation";
|
||||
|
||||
export default memo(BentoChatAnimation);
|
||||
204
src/components/bento/BentoGlobe.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import createGlobe, { COBEOptions } from "cobe";
|
||||
|
||||
// Helper function to convert CSS color to RGB array
|
||||
const getRGBFromCSSVar = (varName: string): [number, number, number] => {
|
||||
if (typeof window === "undefined") return [0.5, 0.5, 0.5];
|
||||
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
|
||||
|
||||
// Handle CSS named colors by creating a temporary element to get computed RGB
|
||||
if (value && !value.startsWith("#") && !value.startsWith("rgb") && !value.includes("%") && !value.match(/^\d+\s+\d+\s+\d+$/)) {
|
||||
const temp = document.createElement("div");
|
||||
temp.style.color = value;
|
||||
document.body.appendChild(temp);
|
||||
const computed = getComputedStyle(temp).color;
|
||||
document.body.removeChild(temp);
|
||||
|
||||
if (computed && computed.startsWith("rgb")) {
|
||||
const match = computed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||
if (match) {
|
||||
const r = parseInt(match[1]) / 255;
|
||||
const g = parseInt(match[2]) / 255;
|
||||
const b = parseInt(match[3]) / 255;
|
||||
return [r, g, b];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle rgba/rgb format (e.g., "rgba(18, 0, 6, .9)" or "rgb(255, 255, 255)")
|
||||
if (value.startsWith("rgb")) {
|
||||
const match = value.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||
if (match) {
|
||||
const r = parseInt(match[1]) / 255;
|
||||
const g = parseInt(match[2]) / 255;
|
||||
const b = parseInt(match[3]) / 255;
|
||||
return [r, g, b];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle hex format (e.g., "#ffffff", "#ffffffaa", or shorthand "#fff", "#f0f")
|
||||
if (value.startsWith("#")) {
|
||||
let hex = value.replace("#", "");
|
||||
// Expand shorthand hex (e.g., "93f" -> "9933ff")
|
||||
if (hex.length === 3 || hex.length === 4) {
|
||||
hex = hex.split("").map(c => c + c).join("").substring(0, 6);
|
||||
}
|
||||
// Take only first 6 characters (ignore alpha channel if present)
|
||||
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
// Handle HSL format (e.g., "0 0% 100%")
|
||||
if (value.includes("%")) {
|
||||
const [h, s, l] = value.split(/\s+/).map(v => parseFloat(v));
|
||||
// Convert HSL to RGB
|
||||
const sNorm = s / 100;
|
||||
const lNorm = l / 100;
|
||||
const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm;
|
||||
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
||||
const m = lNorm - c / 2;
|
||||
let r = 0, g = 0, b = 0;
|
||||
if (h < 60) { r = c; g = x; b = 0; }
|
||||
else if (h < 120) { r = x; g = c; b = 0; }
|
||||
else if (h < 180) { r = 0; g = c; b = x; }
|
||||
else if (h < 240) { r = 0; g = x; b = c; }
|
||||
else if (h < 300) { r = x; g = 0; b = c; }
|
||||
else { r = c; g = 0; b = x; }
|
||||
return [(r + m), (g + m), (b + m)];
|
||||
}
|
||||
|
||||
// Handle RGB format (e.g., "255 255 255")
|
||||
const [r, g, b] = value.split(/\s+/).map(v => parseFloat(v) / 255);
|
||||
return [r || 0.5, g || 0.5, b || 0.5];
|
||||
};
|
||||
|
||||
const getGlobeConfig = (): COBEOptions => ({
|
||||
width: 800,
|
||||
height: 800,
|
||||
onRender: () => {},
|
||||
devicePixelRatio: 2,
|
||||
phi: 0,
|
||||
theta: 0.3,
|
||||
dark: 0,
|
||||
diffuse: 0.4,
|
||||
mapSamples: 16000,
|
||||
mapBrightness: 1.2,
|
||||
baseColor: getRGBFromCSSVar("--card"),
|
||||
markerColor: getRGBFromCSSVar("--primary-cta"),
|
||||
glowColor: getRGBFromCSSVar("--card"),
|
||||
markers: [
|
||||
{ location: [14.5995, 120.9842], size: 0.03 },
|
||||
{ location: [19.076, 72.8777], size: 0.1 },
|
||||
{ location: [23.8103, 90.4125], size: 0.05 },
|
||||
{ location: [30.0444, 31.2357], size: 0.07 },
|
||||
{ location: [39.9042, 116.4074], size: 0.08 },
|
||||
{ location: [-23.5505, -46.6333], size: 0.1 },
|
||||
{ location: [19.4326, -99.1332], size: 0.1 },
|
||||
{ location: [40.7128, -74.006], size: 0.1 },
|
||||
{ location: [34.6937, 135.5022], size: 0.05 },
|
||||
{ location: [41.0082, 28.9784], size: 0.06 },
|
||||
],
|
||||
});
|
||||
|
||||
interface GlobeProps {
|
||||
className?: string;
|
||||
config?: COBEOptions;
|
||||
}
|
||||
|
||||
const GlobeComponent = ({
|
||||
className = "",
|
||||
config,
|
||||
}: GlobeProps) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const globeRef = useRef<{ destroy: () => void } | null>(null);
|
||||
const phiRef = useRef(0);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [globeConfig, setGlobeConfig] = useState<COBEOptions | null>(null);
|
||||
|
||||
const onRender = useCallback(
|
||||
(state: Record<string, number>) => {
|
||||
phiRef.current += 0.005;
|
||||
state.phi = phiRef.current;
|
||||
state.width = dimensions.width * 2;
|
||||
state.height = dimensions.width * 2;
|
||||
},
|
||||
[dimensions]
|
||||
);
|
||||
|
||||
const onResize = useCallback(() => {
|
||||
if (canvasRef.current) {
|
||||
const newWidth = canvasRef.current.offsetWidth;
|
||||
setDimensions(prev => {
|
||||
if (prev.width === newWidth) return prev;
|
||||
return { width: newWidth, height: newWidth };
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", onResize);
|
||||
onResize();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, [onResize]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize globe config with CSS variables
|
||||
const defaultConfig = getGlobeConfig();
|
||||
setGlobeConfig(config ? { ...defaultConfig, ...config } : defaultConfig);
|
||||
}, [config]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current || dimensions.width === 0 || !globeConfig) return;
|
||||
|
||||
if (globeRef.current) {
|
||||
globeRef.current.destroy();
|
||||
}
|
||||
|
||||
globeRef.current = createGlobe(canvasRef.current, {
|
||||
...globeConfig,
|
||||
width: dimensions.width * 2,
|
||||
height: dimensions.width * 2,
|
||||
onRender,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.style.opacity = "1";
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (globeRef.current) {
|
||||
globeRef.current.destroy();
|
||||
globeRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [dimensions, globeConfig, onRender]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"absolute inset-0 mx-auto w-full aspect-square",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<canvas
|
||||
className="size-full opacity-0 transition-opacity duration-500 [contain:layout_paint_size]"
|
||||
ref={canvasRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GlobeComponent.displayName = "BentoGlobe";
|
||||
|
||||
export const BentoGlobe = React.memo(GlobeComponent);
|
||||
72
src/components/bento/BentoIconInfoCards.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type BentoInfoItem = {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
interface BentoIconInfoCardsProps {
|
||||
items: BentoInfoItem[];
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
cardClassName?: string;
|
||||
iconWrapperClassName?: string;
|
||||
iconClassName?: string;
|
||||
labelClassName?: string;
|
||||
valueClassName?: string;
|
||||
}
|
||||
|
||||
const BentoIconInfoCards = ({
|
||||
items,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
cardClassName = "",
|
||||
iconWrapperClassName = "",
|
||||
iconClassName = "",
|
||||
labelClassName = "",
|
||||
valueClassName = "",
|
||||
}: BentoIconInfoCardsProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
|
||||
const duplicatedItems = [...items, ...items];
|
||||
|
||||
return (
|
||||
<div className={cls("h-full min-h-0 overflow-hidden mask-fade-y", className)}>
|
||||
<div className="flex flex-col animate-marquee-vertical px-px">
|
||||
{duplicatedItems.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cls("card shadow rounded-theme-capped p-3 flex items-center justify-between flex-shrink-0 mb-4", cardClassName)}
|
||||
>
|
||||
<div className="w-full min-w-0 flex items-center gap-3">
|
||||
<div className={cls("h-10 w-auto aspect-square rounded-theme flex items-center justify-center secondary-button", iconWrapperClassName)}>
|
||||
<Icon className={cls("h-4/10 w-4/10 text-foreground", iconClassName)} strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className={cls("text-base truncate", shouldUseLightText ? "text-background" : "text-foreground", labelClassName)}>
|
||||
{item.label}
|
||||
</p>
|
||||
</div>
|
||||
<p className={cls("text-base", shouldUseLightText ? "text-background" : "text-foreground", valueClassName)}>
|
||||
{item.value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BentoIconInfoCards.displayName = "BentoIconInfoCards";
|
||||
|
||||
export default memo(BentoIconInfoCards);
|
||||
145
src/components/bento/BentoLineChart/BentoLineChart.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import { formatNumber, calculateYAxisWidth, type ChartDataItem } from "./utils";
|
||||
import CustomTooltip from "./CustomTooltip";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
interface BentoLineChartProps {
|
||||
data?: ChartDataItem[];
|
||||
dataKey?: string;
|
||||
metricLabel?: string;
|
||||
isPercentage?: boolean;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultData: ChartDataItem[] = [
|
||||
{ value: 120 },
|
||||
{ value: 180 },
|
||||
{ value: 150 },
|
||||
{ value: 280 },
|
||||
{ value: 220 },
|
||||
{ value: 350 },
|
||||
{ value: 300 },
|
||||
{ value: 250 },
|
||||
];
|
||||
|
||||
const BentoLineChart = memo<BentoLineChartProps>(
|
||||
({
|
||||
data = defaultData,
|
||||
dataKey = "value",
|
||||
metricLabel = "Value",
|
||||
isPercentage = false,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
const yAxisWidth = calculateYAxisWidth(data, isPercentage);
|
||||
|
||||
const strokeColor = "var(--primary-cta)";
|
||||
const gridColor = "color-mix(in srgb, var(--background-accent) 30%, transparent)";
|
||||
const tickColor = shouldUseLightText ? "var(--background)" : "var(--foreground)";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls("w-full h-full **:outline-none **:focus:outline-none", className)}
|
||||
style={{
|
||||
maskImage: "linear-gradient(to bottom, black 40%, transparent 100%)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, black 40%, transparent 100%)",
|
||||
}}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 10,
|
||||
right: 5,
|
||||
left: 0,
|
||||
bottom: 14,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="bentoLineChartFill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={strokeColor} stopOpacity={0.4} />
|
||||
<stop offset="95%" stopColor={strokeColor} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="bentoFadeGradient" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stopColor="black" stopOpacity={0} />
|
||||
<stop offset="5%" stopColor="black" stopOpacity={0} />
|
||||
<stop offset="15%" stopColor="white" stopOpacity={1} />
|
||||
<stop offset="95%" stopColor="white" stopOpacity={1} />
|
||||
<stop offset="100%" stopColor="black" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<mask id="bentoFadeMask">
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="url(#bentoFadeGradient)"
|
||||
/>
|
||||
</mask>
|
||||
</defs>
|
||||
<CartesianGrid
|
||||
vertical={false}
|
||||
stroke={gridColor}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tick={{
|
||||
fill: tickColor,
|
||||
fontSize: 10,
|
||||
}}
|
||||
width={yAxisWidth}
|
||||
tickFormatter={(value) =>
|
||||
isPercentage ? `${value}%` : formatNumber(value)
|
||||
}
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
<CustomTooltip
|
||||
metricLabel={metricLabel}
|
||||
isPercentage={isPercentage}
|
||||
totalItems={data.length}
|
||||
/>
|
||||
}
|
||||
cursor={{
|
||||
stroke: gridColor,
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
dataKey={dataKey}
|
||||
type="monotone"
|
||||
fill="url(#bentoLineChartFill)"
|
||||
stroke={strokeColor}
|
||||
strokeWidth={2}
|
||||
mask="url(#bentoFadeMask)"
|
||||
activeDot={{
|
||||
fill: strokeColor,
|
||||
r: 5,
|
||||
}}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
BentoLineChart.displayName = "BentoLineChart";
|
||||
|
||||
export default BentoLineChart;
|
||||
64
src/components/bento/BentoLineChart/CustomTooltip.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { formatNumber } from "./utils";
|
||||
|
||||
interface CustomTooltipProps {
|
||||
active?: boolean;
|
||||
payload?: Array<{
|
||||
value: number;
|
||||
color: string;
|
||||
}>;
|
||||
label?: number;
|
||||
metricLabel?: string;
|
||||
isPercentage?: boolean;
|
||||
totalItems: number;
|
||||
}
|
||||
|
||||
const CustomTooltip = memo<CustomTooltipProps>(
|
||||
({
|
||||
active,
|
||||
payload,
|
||||
label = 0,
|
||||
metricLabel = "Value",
|
||||
isPercentage = false,
|
||||
totalItems,
|
||||
}: CustomTooltipProps) => {
|
||||
if (active && payload && payload.length) {
|
||||
const value = isPercentage
|
||||
? `${payload[0].value}%`
|
||||
: formatNumber(payload[0].value);
|
||||
const today = new Date();
|
||||
const daysAgo = totalItems - 1 - label;
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() - daysAgo);
|
||||
return (
|
||||
<div className="card rounded-theme-capped p-3">
|
||||
<p className="text-xs text-foreground mb-2">
|
||||
{date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-1.5 aspect-square rounded-full"
|
||||
style={{
|
||||
backgroundColor: payload[0].color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-foreground">
|
||||
{metricLabel}: {value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
CustomTooltip.displayName = "CustomTooltip";
|
||||
|
||||
export default CustomTooltip;
|
||||
33
src/components/bento/BentoLineChart/utils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const formatNumber = (value: number): string => {
|
||||
if (value >= 100000) {
|
||||
const millions = value / 1000000;
|
||||
return `${millions.toFixed(1)}M`;
|
||||
}
|
||||
if (value >= 1000) {
|
||||
const thousands = value / 1000;
|
||||
const rounded = Math.round(thousands * 10) / 10;
|
||||
return `${rounded}K`;
|
||||
}
|
||||
return value.toString();
|
||||
};
|
||||
|
||||
export interface ChartDataItem {
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const calculateYAxisWidth = (
|
||||
data: ChartDataItem[],
|
||||
isPercentage: boolean
|
||||
): number => {
|
||||
const maxValue = Math.max(...data.map((item) => item.value));
|
||||
const formattedMax = isPercentage ? `${maxValue}%` : formatNumber(maxValue);
|
||||
|
||||
let multiplier = 7;
|
||||
if (formattedMax.length === 2) {
|
||||
multiplier = 8;
|
||||
} else if (formattedMax.length === 3) {
|
||||
multiplier = 10;
|
||||
}
|
||||
|
||||
return formattedMax.length * multiplier;
|
||||
};
|
||||
1460
src/components/bento/BentoMap.tsx
Normal file
71
src/components/bento/BentoMarquee.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import Marquee from "react-fast-marquee";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
type BentoMarqueeProps = {
|
||||
centerIcon: LucideIcon;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
} & (
|
||||
| { variant: "text"; texts: string[] }
|
||||
| { variant: "icon"; icons: LucideIcon[] }
|
||||
);
|
||||
|
||||
const BentoMarquee = (props: BentoMarqueeProps) => {
|
||||
const { centerIcon, useInvertedBackground, className = "" } = props;
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
const CenterIcon = centerIcon;
|
||||
const items = props.variant === "text"
|
||||
? [...props.texts, ...props.texts]
|
||||
: [...props.icons, ...props.icons];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls("relative h-full w-full flex flex-col overflow-hidden", className)}
|
||||
style={{
|
||||
maskImage: "radial-gradient(ellipse at center, black 0%, black 30%, transparent 70%)",
|
||||
WebkitMaskImage: "radial-gradient(ellipse at center, black 0%, black 30%, transparent 70%)"
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-1/2 left-1/2 -translate-1/2 h-auto w-full flex flex-col justify-center gap-2 opacity-60">
|
||||
{Array.from({ length: 10 }).map((_, rowIndex) => (
|
||||
<Marquee
|
||||
key={rowIndex}
|
||||
gradient={false}
|
||||
speed={10}
|
||||
direction={rowIndex % 2 === 0 ? "left" : "right"}
|
||||
>
|
||||
{items.map((item, itemIndex) => (
|
||||
<div
|
||||
key={itemIndex}
|
||||
className={cls("relative mx-1 card rounded-theme flex items-center justify-center", props.variant === "icon" ? "p-2 aspect-square" : "px-4 py-2")}
|
||||
>
|
||||
{props.variant === "text" ? (
|
||||
<p className={cls("text-sm leading-tight", shouldUseLightText ? "text-background" : "text-foreground")}>{item as string}</p>
|
||||
) : (
|
||||
(() => {
|
||||
const Icon = item as LucideIcon;
|
||||
return <Icon className={cls("h-1/2 w-1/2", shouldUseLightText ? "text-background" : "text-foreground")} strokeWidth={1.5} />;
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute! top-1/2 left-1/2 -translate-1/2 z-10 h-18 w-auto aspect-square primary-button backdrop-blur-xs rounded-theme flex items-center justify-center">
|
||||
<CenterIcon className="h-4/10 w-4/10 text-background" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BentoMarquee.displayName = "BentoMarquee";
|
||||
|
||||
export default memo(BentoMarquee);
|
||||
104
src/components/bento/BentoOrbitingIcons.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls, shouldUseInvertedText } from "@/lib/utils";
|
||||
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
export type OrbitingItem = {
|
||||
icon: LucideIcon;
|
||||
ring?: 1 | 2 | 3; // Which ring to orbit on (1=innermost, 3=outermost), defaults to 2
|
||||
duration?: number; // Animation duration in seconds, defaults to 10
|
||||
};
|
||||
|
||||
interface BentoOrbitingIconsProps {
|
||||
centerIcon: LucideIcon;
|
||||
items: OrbitingItem[];
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const BentoOrbitingIcons = ({
|
||||
centerIcon,
|
||||
items,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
}: BentoOrbitingIconsProps) => {
|
||||
const theme = useTheme();
|
||||
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
|
||||
const CenterIcon = centerIcon;
|
||||
|
||||
const circleStyles = "secondary-button border border-background-accent! shadow rounded-full";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls("relative h-full flex flex-col overflow-hidden", className)}
|
||||
style={{
|
||||
perspective: "2000px",
|
||||
maskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%), linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%)",
|
||||
maskComposite: "intersect",
|
||||
WebkitMaskComposite: "source-in"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex-1 rounded-t-theme-capped gap-2 flex items-center justify-center w-full h-full inset-x-0 p-2 relative"
|
||||
style={{
|
||||
transform: "rotateY(20deg) rotateX(20deg) rotateZ(-20deg)"
|
||||
}}
|
||||
>
|
||||
{/* Background concentric circles */}
|
||||
<div className={cls("absolute! top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 shrink-0 h-[15rem] w-[15rem] z-[9] opacity-85", circleStyles)} />
|
||||
<div className={cls("absolute! top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 shrink-0 h-[20rem] w-[20rem] z-[8] opacity-65", circleStyles)} />
|
||||
<div className={cls("absolute! top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 shrink-0 h-[25rem] w-[25rem] z-[7] opacity-45", circleStyles)} />
|
||||
<div className={cls("absolute! top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 shrink-0 h-[30rem] w-[30rem] z-[6] opacity-25", circleStyles)} />
|
||||
|
||||
{/* Center circle with icon */}
|
||||
<div className={cls("absolute! inset-0 shrink-0 h-40 w-[10rem] z-10 m-auto flex items-center justify-center", circleStyles)}>
|
||||
|
||||
<div className="absolute! primary-button h-[5rem] w-[5rem] rounded-full flex items-center justify-center" >
|
||||
<CenterIcon className="absolute h-1/2 w-1/2 text-background" strokeWidth={1.25} />
|
||||
</div>
|
||||
|
||||
{/* Orbiting items */}
|
||||
{items.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
const ring = item.ring || 2;
|
||||
// Ring radii: 7.5rem=120px, 10rem=160px, 12.5rem=200px
|
||||
const radiusMap = { 1: 120, 2: 160, 3: 200 };
|
||||
const radius = radiusMap[ring];
|
||||
const duration = item.duration || 10;
|
||||
// Evenly distribute items around the circle
|
||||
const initialPosition = (360 / items.length) * index;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cls("!absolute top-1/2 left-1/2 h-[2.5rem] w-[2.5rem] card shadow rounded-theme flex items-center justify-center")}
|
||||
style={{
|
||||
marginLeft: '-1.25rem',
|
||||
marginTop: '-1.25rem',
|
||||
animation: `orbit ${duration}s linear infinite`,
|
||||
"--initial-position": `${initialPosition}deg`,
|
||||
"--translate-position": `${radius}px`,
|
||||
"--orbit-duration": `${duration}s`,
|
||||
} as React.CSSProperties & {
|
||||
"--initial-position": string;
|
||||
"--translate-position": string;
|
||||
"--orbit-duration": string;
|
||||
}}
|
||||
>
|
||||
<Icon className={cls("h-4/10 w-4/10", shouldUseLightText ? "text-background" : "text-foreground")} strokeWidth={1.5} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BentoOrbitingIcons.displayName = "BentoOrbitingIcons";
|
||||
|
||||
export default memo(BentoOrbitingIcons);
|
||||
115
src/components/bento/BentoPhoneAnimation.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
export type PhoneApp = {
|
||||
name: string;
|
||||
icon: LucideIcon;
|
||||
};
|
||||
|
||||
export type PhoneApps8 = [PhoneApp, PhoneApp, PhoneApp, PhoneApp, PhoneApp, PhoneApp, PhoneApp, PhoneApp];
|
||||
|
||||
interface BentoPhoneAnimationProps {
|
||||
statusIcon: LucideIcon;
|
||||
alertIcon: LucideIcon;
|
||||
alertTitle: string;
|
||||
alertMessage: string;
|
||||
apps: PhoneApps8;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const BentoPhoneAnimation = ({
|
||||
statusIcon: StatusIcon,
|
||||
alertIcon: AlertIcon,
|
||||
alertTitle,
|
||||
alertMessage,
|
||||
apps,
|
||||
className = "",
|
||||
}: BentoPhoneAnimationProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"group/phone relative h-full flex flex-auto items-center justify-center overflow-hidden cursor-pointer",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
maskImage: "linear-gradient(to bottom, black 60%, transparent 100%)",
|
||||
WebkitMaskImage: "linear-gradient(to bottom, black 60%, transparent 100%)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"absolute inset-x-0 top-0 h-full overflow-hidden isolate",
|
||||
"pt-8 transition-[padding] duration-500 ease-out group-hover/phone:pt-0",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"relative mx-auto card shadow h-100 w-[calc(100%-var(--vw-2)*2)] rounded-[3vw] p-2",
|
||||
)}
|
||||
>
|
||||
<div className="w-full min-w-0 relative h-full overflow-hidden secondary-button rounded-[2.6vw] p-8 pt-6" >
|
||||
<div
|
||||
className="relative z-10 mx-auto h-7 w-auto aspect-square card shadow flex items-center justify-center rounded-full"
|
||||
>
|
||||
<StatusIcon className="h-4/10 w-4/10 text-foreground transition-colors duration-300 group-hover/phone:text-primary-cta" />
|
||||
</div>
|
||||
<div
|
||||
className={cls(
|
||||
"absolute! left-8 right-8 z-2 gap-[0.5vw] p-3 card flex flex-row items-center rounded-theme-capped",
|
||||
"-translate-y-30 scale-90 blur-[2px] opacity-50",
|
||||
"transition-all duration-500 ease-out",
|
||||
"group-hover/phone:translate-y-0 group-hover/phone:scale-100 group-hover/phone:blur-none group-hover/phone:opacity-100",
|
||||
)}
|
||||
style={{ top: "calc(var(--vw-1_5) + var(--height-7) + var(--vw-1_5))" }}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"relative h-8 w-auto aspect-square primary-button flex shrink-0 items-center justify-center rounded-theme",
|
||||
)}
|
||||
>
|
||||
<AlertIcon className="h-4/10 w-4/10 text-background" />
|
||||
</div>
|
||||
<div className="min-w-0 flex flex-col gap-0">
|
||||
<h3
|
||||
className={cls(
|
||||
"text-sm leading-tight text-foreground",
|
||||
)}
|
||||
>
|
||||
{alertTitle}
|
||||
</h3>
|
||||
<p
|
||||
className={cls(
|
||||
"text-xs text-foreground/75 leading-tight truncate",
|
||||
)}
|
||||
>
|
||||
{alertMessage}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-0 grid grid-cols-4 gap-6 mt-6">
|
||||
{apps.map(({ name, icon: Icon }) => (
|
||||
<div key={name} className="w-full min-w-0 flex flex-col items-center gap-2">
|
||||
<div className="aspect-square w-full primary-button rounded-theme-capped flex items-center justify-center">
|
||||
<Icon className="h-2/5 w-2/5 text-background" strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className="w-full text-xs text-foreground text-center truncate">
|
||||
{name}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BentoPhoneAnimation.displayName = "BentoPhoneAnimation";
|
||||
|
||||
export default memo(BentoPhoneAnimation);
|
||||
83
src/components/bento/BentoRevealIcon.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
interface BentoRevealIconProps {
|
||||
icon: LucideIcon;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const BentoRevealIcon = ({
|
||||
icon: Icon,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
}: BentoRevealIconProps) => {
|
||||
void useInvertedBackground;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"group relative h-full w-full flex items-center justify-center overflow-hidden",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
maskImage: "linear-gradient(to right, transparent, black 15%, black 85%, transparent), linear-gradient(to bottom, transparent, black 15%, black 85%, transparent)",
|
||||
WebkitMaskImage: "linear-gradient(to right, transparent, black 15%, black 85%, transparent), linear-gradient(to bottom, transparent, black 15%, black 85%, transparent)",
|
||||
maskComposite: "intersect",
|
||||
WebkitMaskComposite: "source-in",
|
||||
}}
|
||||
>
|
||||
<div className="relative h-26 w-[6.5rem]">
|
||||
<div
|
||||
className="absolute right-full top-1/2 -mt-48 transition-transform duration-500 ease-out group-hover:-translate-x-12"
|
||||
style={{ transform: "translateX(calc(52px + 1px - 2px))" }}
|
||||
>
|
||||
<div className="relative h-96 aspect-[224/280] -scale-x-100">
|
||||
<svg viewBox="0 0 224 280" fill="none" className="absolute inset-0 h-full w-full overflow-visible">
|
||||
<path fill="currentColor" className="text-background-accent/10" d="M8 .25a8 8 0 0 0-8 8v91.704c0 2.258.954 4.411 2.628 5.927l10.744 9.738A7.998 7.998 0 0 1 16 121.546v36.408a7.998 7.998 0 0 1-2.628 5.927l-10.744 9.738A7.998 7.998 0 0 0 0 179.546v92.204a8 8 0 0 0 8 8h308a8 8 0 0 0 8-8V8.25a8 8 0 0 0-8-8H8Z" />
|
||||
<path stroke="currentColor" className="text-background-accent" d="M.5 99.954V8.25A7.5 7.5 0 0 1 8 .75h308a7.5 7.5 0 0 1 7.5 7.5v263.5a7.5 7.5 0 0 1-7.5 7.5H8a7.5 7.5 0 0 1-7.5-7.5v-92.204a7.5 7.5 0 0 1 2.464-5.557l10.744-9.737a8.5 8.5 0 0 0 2.792-6.298v-36.408a8.5 8.5 0 0 0-2.792-6.298l-10.744-9.737A7.5 7.5 0 0 1 .5 99.954Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute left-full top-1/2 -mt-48 transition-transform duration-500 ease-out group-hover:translate-x-12"
|
||||
style={{ transform: "translateX(calc(-52px - 1px + 2px))" }}
|
||||
>
|
||||
<div className="relative h-96 aspect-[224/280]">
|
||||
<svg viewBox="0 0 224 280" fill="none" className="absolute inset-0 h-full w-full overflow-visible">
|
||||
<path fill="currentColor" className="text-background-accent/10" d="M8 .25a8 8 0 0 0-8 8v91.704c0 2.258.954 4.411 2.628 5.927l10.744 9.738A7.998 7.998 0 0 1 16 121.546v36.408a7.998 7.998 0 0 1-2.628 5.927l-10.744 9.738A7.998 7.998 0 0 0 0 179.546v92.204a8 8 0 0 0 8 8h308a8 8 0 0 0 8-8V8.25a8 8 0 0 0-8-8H8Z" />
|
||||
<path stroke="currentColor" className="text-background-accent" d="M.5 99.954V8.25A7.5 7.5 0 0 1 8 .75h308a7.5 7.5 0 0 1 7.5 7.5v263.5a7.5 7.5 0 0 1-7.5 7.5H8a7.5 7.5 0 0 1-7.5-7.5v-92.204a7.5 7.5 0 0 1 2.464-5.557l10.744-9.737a8.5 8.5 0 0 0 2.792-6.298v-36.408a8.5 8.5 0 0 0-2.792-6.298l-10.744-9.737A7.5 7.5 0 0 1 .5 99.954Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-full p-2">
|
||||
<div className="relative w-full h-full primary-button rounded-theme flex items-center justify-center">
|
||||
<Icon className="relative z-10 h-4/10 w-auto text-background" strokeWidth={1.25} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="absolute inset-px z-10 rounded-full mix-blend-overlay"
|
||||
style={{ clipPath: "circle(50%)" }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 z-10 transition-transform duration-500 ease-out group-hover:translate-x-0 group-hover:translate-y-0"
|
||||
style={{
|
||||
backgroundImage: "linear-gradient(to bottom right, transparent 30%, black, transparent 70%)",
|
||||
transform: "translate(-65px, -65px)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BentoRevealIcon.displayName = "BentoRevealIcon";
|
||||
|
||||
export default memo(BentoRevealIcon);
|
||||
115
src/components/bento/BentoTimeline.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { cls } from "@/lib/utils";
|
||||
import { Check, Loader } from "lucide-react";
|
||||
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
|
||||
|
||||
export type TimelineItem = {
|
||||
label: string;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
interface BentoTimelineProps {
|
||||
heading: string;
|
||||
subheading: string;
|
||||
items: [TimelineItem, TimelineItem, TimelineItem];
|
||||
completedLabel: string;
|
||||
useInvertedBackground: InvertedBackground;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const itemDelays = [
|
||||
{ check: 'delay-[150ms]', label: 'delay-[200ms]', detail: 'delay-[250ms]' },
|
||||
{ check: 'delay-[350ms]', label: 'delay-[400ms]', detail: 'delay-[450ms]' },
|
||||
{ check: 'delay-[550ms]', label: 'delay-[600ms]', detail: 'delay-[650ms]' },
|
||||
] as const;
|
||||
|
||||
const BentoTimeline = ({
|
||||
heading,
|
||||
subheading,
|
||||
items,
|
||||
completedLabel,
|
||||
useInvertedBackground,
|
||||
className = "",
|
||||
}: BentoTimelineProps) => {
|
||||
void useInvertedBackground;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
"group relative h-full w-full flex items-center justify-center overflow-hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="absolute h-full aspect-square rounded-full border border-background-accent/30 scale-100" />
|
||||
<div className="absolute h-full aspect-square rounded-full border border-background-accent/30 scale-80" />
|
||||
<div className="absolute h-full aspect-square rounded-full border border-background-accent/30 scale-60" />
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-full min-w-0 flex flex-col gap-3 p-4">
|
||||
<div className="card shadow rounded-theme-capped p-3 flex items-center gap-2">
|
||||
<Loader className="h-[var(--text-sm)] w-auto text-primary transition-transform duration-1000 ease-out group-hover:rotate-[360deg]" strokeWidth={1.5} />
|
||||
<p className="text-xs text-foreground truncate">{heading}</p>
|
||||
<p className="text-xs text-foreground/75 ml-auto text-nowrap">{subheading}</p>
|
||||
</div>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="card shadow rounded-theme-capped px-3 py-2 flex items-center gap-2"
|
||||
>
|
||||
<div className="relative h-6 w-auto aspect-square card shadow rounded-theme flex items-center justify-center">
|
||||
<div className="absolute h-3/10 w-3/10 primary-button rounded-theme transition-opacity duration-300 group-hover:opacity-0" />
|
||||
<div
|
||||
className={cls(
|
||||
"absolute! inset-0 rounded-theme primary-button flex items-center justify-center",
|
||||
"opacity-0 scale-75 transition-all duration-300",
|
||||
`group-hover:opacity-100 group-hover:scale-100 ${itemDelays[index].check}`
|
||||
)}
|
||||
>
|
||||
<Check className="h-1/2 w-1/2 text-background" strokeWidth={2} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full min-w-0 max-w-full flex-1 flex items-center gap-10 justify-between">
|
||||
<p
|
||||
className={cls(
|
||||
"text-xs text-foreground truncate opacity-0 transition-all duration-300",
|
||||
`group-hover:opacity-100 ${itemDelays[index].label}`
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</p>
|
||||
<p
|
||||
className={cls(
|
||||
"text-xs text-foreground/75 text-nowrap opacity-0 translate-y-1 transition-all duration-300",
|
||||
`group-hover:opacity-100 group-hover:translate-y-0 ${itemDelays[index].detail}`
|
||||
)}
|
||||
>
|
||||
{item.detail}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="primary-button rounded-theme-capped p-3 flex items-center justify-center">
|
||||
<div className="absolute flex gap-2 transition-opacity duration-500 delay-[900ms] group-hover:opacity-0">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="h-2 w-auto aspect-square rounded-theme bg-background" />
|
||||
))}
|
||||
</div>
|
||||
<p
|
||||
className="text-xs text-background truncate opacity-0 transition-opacity duration-500 delay-[900ms] group-hover:opacity-100"
|
||||
>
|
||||
{completedLabel}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BentoTimeline.displayName = "BentoTimeline";
|
||||
|
||||
export default memo(BentoTimeline);
|
||||
43
src/components/button/Button.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import ButtonHoverMagnetic from "./ButtonHoverMagnetic/ButtonHoverMagnetic";
|
||||
import ButtonIconArrow from "./ButtonIconArrow";
|
||||
import ButtonShiftHover from "./ButtonShiftHover/ButtonShiftHover";
|
||||
import ButtonTextStagger from "./ButtonTextStagger/ButtonTextStagger";
|
||||
import ButtonTextUnderline from "./ButtonTextUnderline";
|
||||
import ButtonHoverBubble from "./ButtonHoverBubble";
|
||||
import ButtonExpandHover from "./ButtonExpandHover";
|
||||
import ButtonElasticEffect from "./ButtonElasticEffect/ButtonElasticEffect";
|
||||
import ButtonBounceEffect from "./ButtonBounceEffect/ButtonBounceEffect";
|
||||
import ButtonDirectionalHover from "./ButtonDirectionalHover/ButtonDirectionalHover";
|
||||
import ButtonTextShift from "./ButtonTextShift/ButtonTextShift";
|
||||
import ButtonSlideBackground from "./ButtonSlideBackground";
|
||||
import type { ButtonVariantProps } from "./types";
|
||||
|
||||
export type { ButtonVariant, ButtonVariantProps, ButtonPropsForVariant } from "./types";
|
||||
|
||||
const buttonComponents = {
|
||||
"hover-magnetic": ButtonHoverMagnetic,
|
||||
"hover-bubble": ButtonHoverBubble,
|
||||
"expand-hover": ButtonExpandHover,
|
||||
"elastic-effect": ButtonElasticEffect,
|
||||
"bounce-effect": ButtonBounceEffect,
|
||||
"icon-arrow": ButtonIconArrow,
|
||||
"shift-hover": ButtonShiftHover,
|
||||
"text-stagger": ButtonTextStagger,
|
||||
"text-shift": ButtonTextShift,
|
||||
"text-underline": ButtonTextUnderline,
|
||||
"directional-hover": ButtonDirectionalHover,
|
||||
"slide-background": ButtonSlideBackground,
|
||||
} as const;
|
||||
|
||||
const Button = (props: ButtonVariantProps) => {
|
||||
const { variant = "hover-magnetic", ...restProps } = props;
|
||||
const ButtonComponent = buttonComponents[variant];
|
||||
return <ButtonComponent {...restProps} />;
|
||||
};
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
export default memo(Button);
|
||||
30
src/components/button/ButtonBounceEffect/BounceButton.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.bounce-button {
|
||||
--ease-elastic: linear(0, 0.55 7.5%, 0.85 12%, 0.95 14%, 1.03 16.5%, 1.09 20%, 1.13 22%, 1.14 23%, 1.15 24.5%, 1.15 26%, 1.13 28%, 1.11 31%, 1.05 39%, 1.02 43%, 0.99 47%, 0.98 52%, 0.97 59%, 1.002 81%, 1);
|
||||
transition: transform 0.65s var(--ease-elastic);
|
||||
}
|
||||
|
||||
.bounce-button [data-button-animate-chars] span {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-shadow: 0px calc(var(--text-sm) * 1.5) currentColor;
|
||||
transform: translateY(0) rotate(0.001deg);
|
||||
transition: transform 0.65s var(--ease-elastic);
|
||||
}
|
||||
|
||||
.bounce-button:hover {
|
||||
transform: scale(0.92) rotate(-3deg);
|
||||
}
|
||||
|
||||
.bounce-button:hover [data-button-animate-chars] span {
|
||||
transform: translateY(calc(var(--text-sm) * -1.5)) rotate(3deg);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.bounce-button:hover {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
.bounce-button:hover [data-button-animate-chars] span {
|
||||
transform: translateY(0) rotate(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, memo } from "react";
|
||||
import { useCharAnimation } from "../useCharAnimation";
|
||||
import { useButtonClick } from "../useButtonClick";
|
||||
import { cls } from "@/lib/utils";
|
||||
import "./BounceButton.css";
|
||||
|
||||
interface ButtonBounceEffectProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
className?: string;
|
||||
bgClassName?: string;
|
||||
textClassName?: string;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
scrollToSection?: boolean;
|
||||
}
|
||||
|
||||
const ButtonBounceEffect = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className = "",
|
||||
bgClassName = "",
|
||||
textClassName = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
scrollToSection,
|
||||
}: ButtonBounceEffectProps) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const handleClick = useButtonClick(href, onClick, scrollToSection);
|
||||
|
||||
useCharAnimation(buttonRef, text);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
data-href={href}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel || text}
|
||||
className={cls(
|
||||
"bounce-button relative cursor-pointer flex items-center justify-center bg-transparent border-none leading-none no-underline h-9 px-6 min-w-0 w-fit max-w-full rounded-theme text-background",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"bounce-button-bg absolute inset-0 rounded-theme primary-button",
|
||||
bgClassName
|
||||
)}
|
||||
></div>
|
||||
<span
|
||||
data-button-animate-chars=""
|
||||
className={cls(
|
||||
"bounce-button-text relative text-sm inline-block overflow-hidden truncate whitespace-nowrap",
|
||||
textClassName
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonBounceEffect.displayName = "ButtonBounceEffect";
|
||||
|
||||
export default memo(ButtonBounceEffect);
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, memo } from "react";
|
||||
import { useDirectionalHover } from "./useDirectionalHover";
|
||||
import { useButtonClick } from "../useButtonClick";
|
||||
import { cls } from "@/lib/utils";
|
||||
import "./DirectionalButton.css";
|
||||
|
||||
export interface ButtonDirectionalHoverProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
className?: string;
|
||||
bgClassName?: string;
|
||||
textClassName?: string;
|
||||
circleClassName?: string;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
scrollToSection?: boolean;
|
||||
}
|
||||
|
||||
const ButtonDirectionalHover = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className = "",
|
||||
bgClassName = "",
|
||||
textClassName = "",
|
||||
circleClassName = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
scrollToSection,
|
||||
}: ButtonDirectionalHoverProps) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const handleClick = useButtonClick(href, onClick, scrollToSection);
|
||||
|
||||
useDirectionalHover(buttonRef);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type={type}
|
||||
data-href={href}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel || text}
|
||||
className={cls(
|
||||
"directional-button relative cursor-pointer flex items-center justify-center bg-transparent border-none leading-none no-underline h-9 px-6 min-w-0 w-fit max-w-full rounded-theme text-background",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"directional-button-bg absolute inset-0 rounded-theme primary-button",
|
||||
bgClassName
|
||||
)}
|
||||
></div>
|
||||
<div className="directional-button-circle-wrap">
|
||||
<div
|
||||
className={cls(
|
||||
"directional-button-circle bg-accent",
|
||||
circleClassName
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<span
|
||||
className={cls(
|
||||
"directional-button-text relative text-sm inline-block overflow-hidden truncate whitespace-nowrap",
|
||||
textClassName
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonDirectionalHover.displayName = "ButtonDirectionalHover";
|
||||
|
||||
export default memo(ButtonDirectionalHover);
|
||||
@@ -0,0 +1,37 @@
|
||||
.directional-button-circle-wrap {
|
||||
border-radius: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.directional-button-circle {
|
||||
pointer-events: none;
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transition: transform 0.7s cubic-bezier(0.625, 0.05, 0, 1);
|
||||
transform: translate(-50%, -50%) scale(0) rotate(0.001deg);
|
||||
}
|
||||
|
||||
.directional-button-circle::before {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.directional-button:hover .directional-button-circle {
|
||||
transform: translate(-50%, -50%) scale(1) rotate(0.001deg);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.directional-button:hover .directional-button-circle {
|
||||
transform: translate(-50%, -50%) scale(0) rotate(0.001deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useCallback, RefObject } from "react";
|
||||
|
||||
export const useDirectionalHover = (
|
||||
buttonRef: RefObject<HTMLButtonElement | null>,
|
||||
circleSelector: string = ".directional-button-circle"
|
||||
) => {
|
||||
const handleHover = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
const button = buttonRef.current;
|
||||
if (!button) return;
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const buttonWidth = buttonRect.width;
|
||||
const buttonHeight = buttonRect.height;
|
||||
const buttonCenterX = buttonRect.left + buttonWidth / 2;
|
||||
|
||||
const mouseX = event.clientX;
|
||||
const mouseY = event.clientY;
|
||||
|
||||
const offsetXFromLeft = ((mouseX - buttonRect.left) / buttonWidth) * 100;
|
||||
const offsetYFromTop = ((mouseY - buttonRect.top) / buttonHeight) * 100;
|
||||
|
||||
let offsetXFromCenter = ((mouseX - buttonCenterX) / (buttonWidth / 2)) * 50;
|
||||
offsetXFromCenter = Math.abs(offsetXFromCenter);
|
||||
|
||||
const circle = button.querySelector(circleSelector) as HTMLElement;
|
||||
if (circle) {
|
||||
circle.style.left = `${offsetXFromLeft.toFixed(1)}%`;
|
||||
circle.style.top = `${offsetYFromTop.toFixed(1)}%`;
|
||||
circle.style.width = `${115 + offsetXFromCenter * 2}%`;
|
||||
}
|
||||
},
|
||||
[buttonRef, circleSelector]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const button = buttonRef.current;
|
||||
if (!button) return;
|
||||
|
||||
button.addEventListener("mouseenter", handleHover);
|
||||
button.addEventListener("mouseleave", handleHover);
|
||||
|
||||
return () => {
|
||||
button.removeEventListener("mouseenter", handleHover);
|
||||
button.removeEventListener("mouseleave", handleHover);
|
||||
};
|
||||
}, [buttonRef, handleHover]);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import useElasticEffect from "./useElasticEffect";
|
||||
import { useButtonClick } from "../useButtonClick";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface ButtonElasticEffectProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
scrollToSection?: boolean;
|
||||
}
|
||||
|
||||
const ButtonElasticEffect = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className = "",
|
||||
textClassName = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
scrollToSection,
|
||||
}: ButtonElasticEffectProps) => {
|
||||
const elasticRef = useElasticEffect<HTMLButtonElement>();
|
||||
const handleClick = useButtonClick(href, onClick, scrollToSection);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={elasticRef}
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
data-href={href}
|
||||
aria-label={ariaLabel || text}
|
||||
className={cls(
|
||||
"relative cursor-pointer h-9 min-w-0 w-fit max-w-full px-6 primary-button rounded-theme text-background",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className={cls("text-sm block overflow-hidden truncate whitespace-nowrap", textClassName)}>{text}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonElasticEffect.displayName = "ButtonElasticEffect";
|
||||
|
||||
export default memo(ButtonElasticEffect);
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useEffect, useCallback } from "react";
|
||||
import gsap from "gsap";
|
||||
|
||||
const useElasticEffect = <T extends HTMLElement>() => {
|
||||
const elementRef = useRef<T>(null);
|
||||
const hoverLockedRef = useRef(false);
|
||||
const timelineRef = useRef<gsap.core.Timeline | null>(null);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
const el = elementRef.current;
|
||||
if (!el || hoverLockedRef.current) return;
|
||||
|
||||
hoverLockedRef.current = true;
|
||||
setTimeout(() => {
|
||||
hoverLockedRef.current = false;
|
||||
}, 500);
|
||||
|
||||
const w = el.offsetWidth;
|
||||
const h = el.offsetHeight;
|
||||
const fs = parseFloat(getComputedStyle(el).fontSize);
|
||||
const stretch = 0.75 * fs;
|
||||
const sx = (w + stretch) / w;
|
||||
const sy = (h - stretch * 0.33) / h;
|
||||
|
||||
if (timelineRef.current) {
|
||||
timelineRef.current.kill();
|
||||
}
|
||||
|
||||
timelineRef.current = gsap
|
||||
.timeline()
|
||||
.to(el, { scaleX: sx, scaleY: sy, duration: 0.1, ease: "power1.out" })
|
||||
.to(el, { scaleX: 1, scaleY: 1, duration: 1, ease: "elastic.out(1, 0.3)" });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip on touch devices
|
||||
if (window.matchMedia("(hover: none) and (pointer: coarse)").matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = elementRef.current;
|
||||
if (!el) return;
|
||||
|
||||
el.addEventListener("mouseenter", handleMouseEnter);
|
||||
|
||||
return () => {
|
||||
el.removeEventListener("mouseenter", handleMouseEnter);
|
||||
if (timelineRef.current) {
|
||||
timelineRef.current.kill();
|
||||
}
|
||||
};
|
||||
}, [handleMouseEnter]);
|
||||
|
||||
return elementRef;
|
||||
};
|
||||
|
||||
export default useElasticEffect;
|
||||
92
src/components/button/ButtonExpandHover.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import { useButtonClick } from "./useButtonClick";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface ButtonExpandHoverProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
iconClassName?: string;
|
||||
iconBgClassName?: string;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
scrollToSection?: boolean;
|
||||
}
|
||||
|
||||
const ButtonExpandHover = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className = "",
|
||||
textClassName = "",
|
||||
iconClassName = "",
|
||||
iconBgClassName = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
scrollToSection,
|
||||
}: ButtonExpandHoverProps) => {
|
||||
const handleClick = useButtonClick(href, onClick, scrollToSection);
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
data-href={href}
|
||||
aria-label={ariaLabel || text}
|
||||
className={cls(
|
||||
"group relative cursor-pointer h-fit min-w-0 w-fit max-w-full rounded-theme text-sm text-background pointer-events-auto outline-none",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="relative h-9 w-full px-5"
|
||||
style={{ paddingRight: "calc(2.25rem + 0.75rem)" }}
|
||||
>
|
||||
<div className="h-9 flex items-center" >
|
||||
<span
|
||||
className={cls(
|
||||
"relative z-10 block overflow-hidden truncate whitespace-nowrap md:transition-colors md:duration-[900ms] md:[transition-timing-function:cubic-bezier(.77,0,.18,1)]",
|
||||
textClassName
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute overflow-hidden top-[2px] bottom-[2px] left-[2px] right-[2px] rounded-theme flex justify-end">
|
||||
<div
|
||||
className={cls(
|
||||
"relative z-10 h-full w-auto aspect-square flex items-center justify-center",
|
||||
iconClassName
|
||||
)}
|
||||
>
|
||||
<ArrowUpRight
|
||||
className="h-1/2 w-auto aspect-square"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cls(
|
||||
"absolute z-0 h-full w-full rounded-theme",
|
||||
"md:transition-transform md:duration-[900ms] md:[transition-timing-function:cubic-bezier(.77,0,.18,1)]",
|
||||
"-translate-x-[calc(-100%+2.25rem-4px)] md:group-hover:translate-x-0",
|
||||
iconBgClassName
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonExpandHover.displayName = "ButtonExpandHover";
|
||||
|
||||
export default memo(ButtonExpandHover);
|
||||
83
src/components/button/ButtonHoverBubble.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { ArrowDownRight } from "lucide-react";
|
||||
import { useButtonClick } from "./useButtonClick";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface ButtonHoverBubbleProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
className?: string;
|
||||
bgClassName?: string;
|
||||
textClassName?: string;
|
||||
iconClassName?: string;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
scrollToSection?: boolean;
|
||||
}
|
||||
|
||||
const ButtonHoverBubble = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className = "",
|
||||
bgClassName = "",
|
||||
textClassName = "",
|
||||
iconClassName = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
scrollToSection,
|
||||
}: ButtonHoverBubbleProps) => {
|
||||
const handleClick = useButtonClick(href, onClick, scrollToSection);
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel || text}
|
||||
data-href={href}
|
||||
className={cls(
|
||||
"relative group flex justify-center items-center min-w-0 w-fit max-w-full rounded-theme cursor-pointer pointer-events-auto outline-none",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"flex justify-center items-center h-9 aspect-square rounded-theme relative",
|
||||
"scale-0 md:transition-transform md:duration-700 md:ease-[cubic-bezier(0.625,0.05,0,1)] md:origin-left md:group-hover:scale-100",
|
||||
iconClassName
|
||||
)}
|
||||
>
|
||||
<ArrowDownRight strokeWidth={1.5} className="h-[35%] w-auto aspect-square object-contain md:transition-transform md:duration-700 md:group-hover:rotate-[-45deg]" />
|
||||
</div>
|
||||
<div
|
||||
className={cls(
|
||||
"flex justify-center items-center h-9 px-4 min-w-0 w-fit max-w-full rounded-theme relative",
|
||||
"-translate-x-[var(--height-9)] md:transition-transform md:duration-700 md:ease-[cubic-bezier(0.625,0.05,0,1)] md:group-hover:translate-x-0",
|
||||
bgClassName
|
||||
)}
|
||||
>
|
||||
<span className={cls("text-sm block overflow-hidden truncate whitespace-nowrap", textClassName)}>{text}</span>
|
||||
</div>
|
||||
<div
|
||||
className={cls(
|
||||
"flex justify-center items-center h-9 aspect-square rounded-theme absolute right-0 z-20",
|
||||
"scale-100 md:transition-transform md:duration-700 md:ease-[cubic-bezier(0.625,0.05,0,1)] md:origin-right md:group-hover:scale-0",
|
||||
iconClassName
|
||||
)}
|
||||
>
|
||||
<ArrowDownRight strokeWidth={1.5} className="h-[35%] w-auto aspect-square object-contain md:transition-transform md:duration-700 md:group-hover:rotate-[-45deg]" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonHoverBubble.displayName = "ButtonHoverBubble";
|
||||
|
||||
export default memo(ButtonHoverBubble);
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import useMagneticEffect from "./useMagneticEffect";
|
||||
import { useButtonClick } from "../useButtonClick";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface ButtonHoverMagneticProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
strengthFactor?: number;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
scrollToSection?: boolean;
|
||||
}
|
||||
|
||||
const ButtonHoverMagnetic = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className = "",
|
||||
textClassName = "",
|
||||
strengthFactor = 20,
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
scrollToSection,
|
||||
}: ButtonHoverMagneticProps) => {
|
||||
const magneticRef = useMagneticEffect(strengthFactor);
|
||||
const handleClick = useButtonClick(href, onClick, scrollToSection);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={magneticRef as React.RefObject<HTMLButtonElement>}
|
||||
data-href={href}
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel || text}
|
||||
className={cls(
|
||||
"relative cursor-pointer h-9 min-w-0 w-fit max-w-full px-6 primary-button rounded-theme text-background",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className={cls("text-sm block overflow-hidden truncate whitespace-nowrap", textClassName)}>{text}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonHoverMagnetic.displayName = "ButtonHoverMagnetic";
|
||||
|
||||
export default memo(ButtonHoverMagnetic);
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const useMagneticEffect = (strengthFactor = 10) => {
|
||||
const elementRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
import("gsap").then((gsap) => {
|
||||
const element = elementRef.current;
|
||||
|
||||
if (!element || window.innerWidth < 768) return;
|
||||
|
||||
const resetEl = (el: HTMLElement, immediate: boolean) => {
|
||||
if (!el) return;
|
||||
gsap.default.killTweensOf(el);
|
||||
(immediate ? gsap.default.set : gsap.default.to)(el, {
|
||||
x: "0vw",
|
||||
y: "0vw",
|
||||
rotate: "0deg",
|
||||
clearProps: "all",
|
||||
...(!immediate && { ease: "elastic.out(1, 0.3)", duration: 1.6 })
|
||||
});
|
||||
};
|
||||
|
||||
const resetOnEnter = () => {
|
||||
resetEl(element, true);
|
||||
};
|
||||
|
||||
const moveMagnet = (e: MouseEvent) => {
|
||||
const b = element.getBoundingClientRect();
|
||||
const strength = strengthFactor;
|
||||
|
||||
const offsetX = ((e.clientX - b.left) / element.offsetWidth - 0.5) * (strength / 16);
|
||||
const offsetY = ((e.clientY - b.top) / element.offsetHeight - 0.5) * (strength / 16);
|
||||
|
||||
gsap.default.to(element, {
|
||||
x: offsetX + "vw",
|
||||
y: offsetY + "vw",
|
||||
rotate: "0.001deg",
|
||||
ease: "power4.out",
|
||||
duration: 1.6
|
||||
});
|
||||
};
|
||||
|
||||
const resetMagnet = () => {
|
||||
gsap.default.to(element, {
|
||||
x: "0vw",
|
||||
y: "0vw",
|
||||
ease: "elastic.out(1, 0.3)",
|
||||
duration: 1.6,
|
||||
clearProps: "all"
|
||||
});
|
||||
};
|
||||
|
||||
element.addEventListener("mouseenter", resetOnEnter);
|
||||
element.addEventListener("mousemove", moveMagnet);
|
||||
element.addEventListener("mouseleave", resetMagnet);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener("mouseenter", resetOnEnter);
|
||||
element.removeEventListener("mousemove", moveMagnet);
|
||||
element.removeEventListener("mouseleave", resetMagnet);
|
||||
};
|
||||
});
|
||||
}, [strengthFactor]);
|
||||
|
||||
return elementRef;
|
||||
};
|
||||
|
||||
export default useMagneticEffect;
|
||||
66
src/components/button/ButtonIconArrow.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { memo } from "react";
|
||||
import { useButtonClick } from "./useButtonClick";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
interface ButtonIconArrowProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
iconClassName?: string;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
scrollToSection?: boolean;
|
||||
}
|
||||
|
||||
const ButtonIconArrow = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className = "",
|
||||
textClassName = "",
|
||||
iconClassName = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
scrollToSection,
|
||||
}: ButtonIconArrowProps) => {
|
||||
const handleClick = useButtonClick(href, onClick, scrollToSection);
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
data-href={href}
|
||||
aria-label={ariaLabel || text}
|
||||
className={cls(
|
||||
"relative group cursor-pointer h-9 min-w-0 w-fit max-w-full primary-button rounded-theme px-6 text-sm text-background flex items-center gap-3",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className={cls(
|
||||
"block overflow-hidden truncate whitespace-nowrap md:transition-transform md:duration-[600ms] md:[transition-timing-function:cubic-bezier(.25,.8,.25,1)] md:group-hover:[transform:translateX(calc(var(--height-9)/4))]",
|
||||
textClassName
|
||||
)}>
|
||||
{text}
|
||||
</span>
|
||||
<div className={cls(
|
||||
"h-5 w-[var(--height-5)] aspect-square rounded-theme flex items-center justify-center md:transition-transform md:duration-[600ms] md:[transition-timing-function:cubic-bezier(.25,.8,.25,1)] md:group-hover:scale-[0.2] md:group-hover:rotate-90",
|
||||
iconClassName || "secondary-button text-foreground"
|
||||
)}>
|
||||
<ArrowRight className="h-1/2 w-1/2 md:transition-opacity md:duration-[600ms] md:[transition-timing-function:cubic-bezier(.25,.8,.25,1)] md:group-hover:opacity-0" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonIconArrow.displayName = "ButtonIconArrow";
|
||||
|
||||
export default memo(ButtonIconArrow);
|
||||
73
src/components/button/ButtonShiftHover/ButtonShiftHover.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, memo } from "react";
|
||||
import { useCharAnimation } from "../useCharAnimation";
|
||||
import { useButtonClick } from "../useButtonClick";
|
||||
import { cls } from "@/lib/utils";
|
||||
import "./ShiftButton.css";
|
||||
|
||||
interface ButtonShiftHoverProps {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
className?: string;
|
||||
bgClassName?: string;
|
||||
textClassName?: string;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
scrollToSection?: boolean;
|
||||
}
|
||||
|
||||
const ButtonShiftHover = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className = "",
|
||||
bgClassName = "",
|
||||
textClassName = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
scrollToSection,
|
||||
}: ButtonShiftHoverProps) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const handleClick = useButtonClick(href, onClick, scrollToSection);
|
||||
|
||||
useCharAnimation(buttonRef, text);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
data-href={href}
|
||||
aria-label={ariaLabel || text}
|
||||
className={cls(
|
||||
"shift-button group relative cursor-pointer flex gap-2 items-center justify-center bg-transparent border-none leading-none no-underline h-9 px-5 pr-4 min-w-0 w-fit max-w-full rounded-theme text-background text-sm",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
textClassName,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"shift-button-bg absolute inset-0 rounded-theme transition-transform duration-[600ms] primary-button",
|
||||
bgClassName
|
||||
)}
|
||||
></div>
|
||||
<span
|
||||
data-button-animate-chars=""
|
||||
className="shift-button-text relative inline-block overflow-hidden truncate whitespace-nowrap"
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
<div className="relative h-[1em] w-auto aspect-square rounded-theme border border-current scale-65 transition-all duration-300 md:group-hover:bg-current md:group-hover:scale-40" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonShiftHover.displayName = "ButtonShiftHover";
|
||||
|
||||
export default memo(ButtonShiftHover);
|
||||