Initial commit

This commit is contained in:
2026-04-10 02:44:26 +03:00
commit 17859e7de5
48 changed files with 3904 additions and 0 deletions

4
.env Normal file
View File

@@ -0,0 +1,4 @@
NEXT_PUBLIC_API_URL=https://dev.api.webild.io
NEXT_PUBLIC_PROJECT_ID=20e1ab85-8867-4496-baca-ef453bcf7615

View File

@@ -0,0 +1,37 @@
name: Code Check
on:
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
run: git clone --depth 1 --branch ${{ gitea.ref_name }} ${{ gitea.server_url }}/${{ gitea.repository }}.git . || exit 1
- name: Install dependencies
run: |
if [ -d "/var/node_modules_cache/node_modules" ]; then
ln -s /var/node_modules_cache/node_modules ./node_modules
else
npm ci --prefer-offline --no-audit
fi
timeout-minutes: 5
- name: TypeScript check
run: npm run typecheck 2>&1 | tee build.log
timeout-minutes: 3
- name: ESLint check
run: npm run lint 2>&1 | tee -a build.log
timeout-minutes: 3
- name: Upload build log on failure
if: failure()
uses: actions/upload-artifact@v3
with:
name: build-log
path: build.log
retention-days: 1

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
.DS_Store
*.local

View File

@@ -0,0 +1,58 @@
================================================================================
THEME OPTIONS (v4)
================================================================================
STYLE PROPS
-----------
1. borderRadius
• "rounded"
• "soft"
• "pill"
2. contentWidth
• "small"
• "compact"
• "medium"
• "mediumLarge"
3. cardStyle
• "solid"
• "outline"
• "gradient-mesh"
• "gradient-radial"
• "inset"
• "glass-elevated"
• "glass-depth"
• "gradient-bordered"
• "layered-gradient"
• "soft-shadow"
• "subtle-shadow"
• "elevated-border"
• "inner-glow"
• "spotlight"
4. primaryButtonStyle
• "gradient"
• "shadow"
• "flat"
• "radial-glow"
• "diagonal-gradient"
• "double-inset"
• "primary-glow"
• "inset-glow"
• "soft-glow"
• "glass-shimmer"
• "neon-outline"
• "lifted"
• "depth-layers"
• "accent-edge"
• "metallic"
5. secondaryButtonStyle
• "glass"
• "solid"
• "layered"
• "radial-glow"
================================================================================

677
colorThemes.json Normal file
View File

@@ -0,0 +1,677 @@
{
"lightTheme": {
"minimalDarkBlue": {
"--background": "#ffffff",
"--card": "#f9f9f9",
"--foreground": "#000612e6",
"--primary-cta": "#15479c",
"--secondary-cta": "#f9f9f9",
"--accent": "#e2e2e2",
"--background-accent": "#c4c4c4",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#000612e6"
},
"minimalDarkGreen": {
"--background": "#ffffff",
"--card": "#f9f9f9",
"--foreground": "#000f06e6",
"--primary-cta": "#0a7039",
"--secondary-cta": "#f9f9f9",
"--accent": "#e2e2e2",
"--background-accent": "#c4c4c4",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#000f06e6"
},
"minimalLightRed": {
"--background": "#ffffff",
"--card": "#f9f9f9",
"--foreground": "#120006e6",
"--primary-cta": "#e63946",
"--secondary-cta": "#f9f9f9",
"--accent": "#e2e2e2",
"--background-accent": "#c4c4c4",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#120006e6"
},
"minimalBrightBlue": {
"--background": "#ffffff",
"--card": "#f9f9f9",
"--foreground": "#000612e6",
"--primary-cta": "#106EFB",
"--secondary-cta": "#f9f9f9",
"--accent": "#e2e2e2",
"--background-accent": "#106EFB",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#000612e6"
},
"minimalBrightOrange": {
"--background": "#ffffff",
"--card": "#f9f9f9",
"--foreground": "#120a00e6",
"--primary-cta": "#E34400",
"--secondary-cta": "#f9f9f9",
"--accent": "#e2e2e2",
"--background-accent": "#E34400",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#120a00e6"
},
"minimalGoldenOrange": {
"--background": "#ffffff",
"--card": "#f9f9f9",
"--foreground": "#120a00e6",
"--primary-cta": "#FF7B05",
"--secondary-cta": "#f9f9f9",
"--accent": "#e2e2e2",
"--background-accent": "#FF7B05",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#120a00e6"
},
"minimalLightOrange": {
"--background": "#ffffff",
"--card": "#f9f9f9",
"--foreground": "#120a00e6",
"--primary-cta": "#ff8c42",
"--secondary-cta": "#f9f9f9",
"--accent": "#e2e2e2",
"--background-accent": "#c4c4c4",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#120a00e6"
},
"darkBlue": {
"--background": "#f5faff",
"--card": "#f1f8ff",
"--foreground": "#001122",
"--primary-cta": "#15479c",
"--secondary-cta": "#ffffff",
"--accent": "#a8cce8",
"--background-accent": "#7ba3cf",
"--primary-cta-text": "#f5faff",
"--secondary-cta-text": "#001122"
},
"darkGreen": {
"--background": "#fafffb",
"--card": "#f7fffa",
"--foreground": "#001a0a",
"--primary-cta": "#0a7039",
"--secondary-cta": "#ffffff",
"--accent": "#a8d9be",
"--background-accent": "#6bbf8e",
"--primary-cta-text": "#fafffb",
"--secondary-cta-text": "#001a0a"
},
"lightRed": {
"--background": "#fffafa",
"--card": "#fff7f7",
"--foreground": "#1a0000",
"--primary-cta": "#e63946",
"--secondary-cta": "#ffffff",
"--accent": "#f5c4c7",
"--background-accent": "#f09199",
"--primary-cta-text": "#fffafa",
"--secondary-cta-text": "#1a0000"
},
"lightPurple": {
"--background": "#fbfaff",
"--card": "#f7f5ff",
"--foreground": "#0f0022",
"--primary-cta": "#8b5cf6",
"--secondary-cta": "#ffffff",
"--accent": "#d8cef5",
"--background-accent": "#c4a8f9",
"--primary-cta-text": "#fbfaff",
"--secondary-cta-text": "#0f0022"
},
"warmCream": {
"--background": "#f6f0e9",
"--card": "#efe7dd",
"--foreground": "#2b180a",
"--primary-cta": "#2b180a",
"--secondary-cta": "#efe7dd",
"--accent": "#94877c",
"--background-accent": "#afa094",
"--primary-cta-text": "#f6f0e9",
"--secondary-cta-text": "#2b180a"
},
"grayBlueAccent": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1c1c1c",
"--secondary-cta": "#ffffff",
"--accent": "#15479c",
"--background-accent": "#a8cce8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayGreenAccent": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1c1c1c",
"--secondary-cta": "#ffffff",
"--accent": "#159c49",
"--background-accent": "#a8e8ba",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayRedAccent": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1c1c1c",
"--secondary-cta": "#ffffff",
"--accent": "#e63946",
"--background-accent": "#e8bea8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayPurpleAccent": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1c1c1c",
"--secondary-cta": "#ffffff",
"--accent": "#6139e6",
"--background-accent": "#b3a8e8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"warmBeige": {
"--background": "#efebe5",
"--card": "#f7f2ea",
"--foreground": "#000000",
"--primary-cta": "#000000",
"--secondary-cta": "#ffffff",
"--accent": "#ffffff",
"--background-accent": "#e1b875",
"--primary-cta-text": "#efebe5",
"--secondary-cta-text": "#000000"
},
"grayTealGreen": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1f514c",
"--secondary-cta": "#ffffff",
"--accent": "#159c49",
"--background-accent": "#a8e8ba",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayNavyBlue": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#1f3251",
"--secondary-cta": "#ffffff",
"--accent": "#15479c",
"--background-accent": "#a8cce8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayBurgundyRed": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#511f1f",
"--secondary-cta": "#ffffff",
"--accent": "#e63946",
"--background-accent": "#e8bea8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"grayIndigoPurple": {
"--background": "#f5f5f5",
"--card": "#ffffff",
"--foreground": "#1c1c1c",
"--primary-cta": "#341f51",
"--secondary-cta": "#ffffff",
"--accent": "#6139e6",
"--background-accent": "#b3a8e8",
"--primary-cta-text": "#f5f5f5",
"--secondary-cta-text": "#1c1c1c"
},
"warmgrayPink": {
"--background": "#f7f6f7",
"--card": "#ffffff",
"--foreground": "#1b0c25",
"--primary-cta": "#1b0c25",
"--secondary-cta": "#ffffff",
"--accent": "#ff93e4",
"--background-accent": "#e8a8c3",
"--primary-cta-text": "#f7f6f7",
"--secondary-cta-text": "#1b0c25"
},
"warmgrayOrange": {
"--background": "#f7f6f7",
"--card": "#ffffff",
"--foreground": "#25190c",
"--primary-cta": "#ff6207",
"--secondary-cta": "#ffffff",
"--accent": "#ffce93",
"--background-accent": "#e8cfa8",
"--primary-cta-text": "#f7f6f7",
"--secondary-cta-text": "#25190c"
},
"warmgrayBlue": {
"--background": "#f7f6f7",
"--card": "#ffffff",
"--foreground": "#0c1325",
"--primary-cta": "#0798ff",
"--secondary-cta": "#ffffff",
"--accent": "#93c7ff",
"--background-accent": "#a8cde8",
"--primary-cta-text": "#f7f6f7",
"--secondary-cta-text": "#0c1325"
},
"warmgrayIndigo": {
"--background": "#f7f6f7",
"--card": "#ffffff",
"--foreground": "#0c1325",
"--primary-cta": "#0b07ff",
"--secondary-cta": "#ffffff",
"--accent": "#93b7ff",
"--background-accent": "#a8bae8",
"--primary-cta-text": "#f7f6f7",
"--secondary-cta-text": "#0c1325"
},
"lavenderPeach": {
"--background": "#e3deea",
"--card": "#ffffff",
"--foreground": "#27231f",
"--primary-cta": "#27231f",
"--secondary-cta": "#ffffff",
"--accent": "#c68a62",
"--background-accent": "#c68a62",
"--primary-cta-text": "#e3deea",
"--secondary-cta-text": "#27231f"
},
"lavenderBlue": {
"--background": "#e3deea",
"--card": "#ffffff",
"--foreground": "#1f2027",
"--primary-cta": "#1f2027",
"--secondary-cta": "#ffffff",
"--accent": "#627dc6",
"--background-accent": "#627dc6",
"--primary-cta-text": "#e3deea",
"--secondary-cta-text": "#1f2027"
},
"warmStone": {
"--background": "#f5f4ef",
"--card": "#dad6cd",
"--foreground": "#2a2928",
"--primary-cta": "#2a2928",
"--secondary-cta": "#ecebea",
"--accent": "#ffffff",
"--background-accent": "#c6b180",
"--primary-cta-text": "#f5f4ef",
"--secondary-cta-text": "#2a2928"
},
"warmStoneGray": {
"--background": "#f5f4f0",
"--card": "#ffffff",
"--foreground": "#1a1a1a",
"--primary-cta": "#2c2c2c",
"--secondary-cta": "#f5f4f0",
"--accent": "#8a8a8a",
"--background-accent": "#e8e6e1",
"--primary-cta-text": "#f5f4f0",
"--secondary-cta-text": "#1a1a1a"
},
"warmGreen": {
"--background": "#fffefe",
"--card": "#f6f7f4",
"--foreground": "#080908",
"--primary-cta": "#0e3a29",
"--secondary-cta": "#e7eecd",
"--accent": "#35c18b",
"--background-accent": "#ecebe4",
"--primary-cta-text": "#fffefe",
"--secondary-cta-text": "#080908"
},
"warmSand": {
"--background": "#fcf6ec",
"--card": "#f3ede2",
"--foreground": "#2e2521",
"--primary-cta": "#2e2521",
"--secondary-cta": "#ffffff",
"--accent": "#b2a28b",
"--background-accent": "#b2a28b",
"--primary-cta-text": "#fcf6ec",
"--secondary-cta-text": "#2e2521"
},
"warmgrayRed": {
"--background": "#f7f6f7",
"--card": "#ffffff",
"--foreground": "#250c0d",
"--primary-cta": "#b82b40",
"--secondary-cta": "#ffffff",
"--accent": "#b90941",
"--background-accent": "#e8a8b6",
"--primary-cta-text": "#f7f6f7",
"--secondary-cta-text": "#250c0d"
}
},
"darkTheme": {
"minimal": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#ffffffe6",
"--primary-cta": "#e6e6e6",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#ffffffe6"
},
"minimalLightBlue": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f0f8ffe6",
"--primary-cta": "#cee7ff",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#f0f8ffe6"
},
"minimalLightGreen": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f5fffae6",
"--primary-cta": "#80da9b",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#f5fffae6"
},
"minimalLightRed": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#fff5f5e6",
"--primary-cta": "#ff7a7a",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#fff5f5e6"
},
"minimalLightPurple": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f8f5ffe6",
"--primary-cta": "#c89bff",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#f8f5ffe6"
},
"lightBlueWhite": {
"--background": "#010912",
"--card": "#152840",
"--foreground": "#e6f0ff",
"--primary-cta": "#cee7ff",
"--secondary-cta": "#0e1a29",
"--accent": "#3f5c79",
"--background-accent": "#004a93",
"--primary-cta-text": "#010912",
"--secondary-cta-text": "#ffffff"
},
"lime": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f5f5f5",
"--primary-cta": "#dfff1c",
"--secondary-cta": "#1a1a1a",
"--accent": "#8b9a1b",
"--background-accent": "#5d6b00",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#ffffff"
},
"gold": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f5f5f5",
"--primary-cta": "#ffdf7d",
"--secondary-cta": "#1a1a1a",
"--accent": "#b8860b",
"--background-accent": "#8b6914",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#ffffff"
},
"crimson": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#f5f5f5",
"--primary-cta": "#ff0000",
"--secondary-cta": "#1a1a1a",
"--accent": "#991b1b",
"--background-accent": "#7f1d1d",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#ffffff"
},
"midnightIce": {
"--background": "#000000",
"--card": "#0c0c0c",
"--foreground": "#ffffff",
"--primary-cta": "#cee7ff",
"--secondary-cta": "#000000",
"--accent": "#535353",
"--background-accent": "#CEE7FF",
"--primary-cta-text": "#000000",
"--secondary-cta-text": "#ffffff"
},
"midnightBlue": {
"--background": "#000000",
"--card": "#0c0c0c",
"--foreground": "#ffffff",
"--primary-cta": "#106EFB",
"--secondary-cta": "#000000",
"--accent": "#535353",
"--background-accent": "#106EFB",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#ffffff"
},
"blueOrangeAccent": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#ffffff",
"--primary-cta": "#1f7cff",
"--secondary-cta": "#010101",
"--accent": "#1f7cff",
"--background-accent": "#f96b2f",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#ffffff"
},
"orangeBlueAccent": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#ffffff",
"--primary-cta": "#e34400",
"--secondary-cta": "#010101",
"--accent": "#ff7b05",
"--background-accent": "#106efb",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#ffffff"
},
"minimalBrightOrange": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#ffffff",
"--primary-cta": "#e34400",
"--secondary-cta": "#010101",
"--accent": "#737373",
"--background-accent": "#e34400",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#ffffff"
},
"minimalLightOrange": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#fffaf5e6",
"--primary-cta": "#ffaa70",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#fffaf5e6"
},
"minimalLightYellow": {
"--background": "#0a0a0a",
"--card": "#1a1a1a",
"--foreground": "#fffffae6",
"--primary-cta": "#fde047",
"--secondary-cta": "#1a1a1a",
"--accent": "#737373",
"--background-accent": "#737373",
"--primary-cta-text": "#0a0a0a",
"--secondary-cta-text": "#fffffae6"
},
"lightBlue": {
"--background": "#010912",
"--card": "#152840",
"--foreground": "#e6f0ff",
"--primary-cta": "#cee7ff",
"--secondary-cta": "#0e1a29",
"--accent": "#3f5c79",
"--background-accent": "#004a93",
"--primary-cta-text": "#010912",
"--secondary-cta-text": "#e6f0ff"
},
"lightGreen": {
"--background": "#000802",
"--card": "#0b1a0b",
"--foreground": "#e6ffe6",
"--primary-cta": "#80da9b",
"--secondary-cta": "#07170b",
"--accent": "#38714a",
"--background-accent": "#2c6541",
"--primary-cta-text": "#000802",
"--secondary-cta-text": "#e6ffe6"
},
"lightRed": {
"--background": "#080000",
"--card": "#1e0d0d",
"--foreground": "#ffe6e6",
"--primary-cta": "#ff7a7a",
"--secondary-cta": "#1e0909",
"--accent": "#7b4242",
"--background-accent": "#65292c",
"--primary-cta-text": "#080000",
"--secondary-cta-text": "#ffe6e6"
},
"darkRed": {
"--background": "#060000",
"--card": "#1d0d0d",
"--foreground": "#ffe6e6",
"--primary-cta": "#ff3d4a",
"--secondary-cta": "#1f0a0a",
"--accent": "#7b2d2d",
"--background-accent": "#b8111f",
"--primary-cta-text": "#ffffff",
"--secondary-cta-text": "#ffe6e6"
},
"lightPurple": {
"--background": "#050012",
"--card": "#040121",
"--foreground": "#f0e6ff",
"--primary-cta": "#c89bff",
"--secondary-cta": "#1d123b",
"--accent": "#684f7b",
"--background-accent": "#65417c",
"--primary-cta-text": "#050012",
"--secondary-cta-text": "#f0e6ff"
},
"lightOrange": {
"--background": "#080200",
"--card": "#1a0d0b",
"--foreground": "#ffe6d5",
"--primary-cta": "#ffaa70",
"--secondary-cta": "#170b07",
"--accent": "#7b5e4a",
"--background-accent": "#b8541e",
"--primary-cta-text": "#080200",
"--secondary-cta-text": "#ffe6d5"
},
"deepBlue": {
"--background": "#020617",
"--card": "#0f172a",
"--foreground": "#e2e8f0",
"--primary-cta": "#c4d8f9",
"--secondary-cta": "#041633",
"--accent": "#2d30f3",
"--background-accent": "#1d4ed8",
"--primary-cta-text": "#020617",
"--secondary-cta-text": "#e2e8f0"
},
"violet": {
"--background": "#030128",
"--card": "#241f48",
"--foreground": "#ffffff",
"--primary-cta": "#ffffff",
"--secondary-cta": "#131136",
"--accent": "#44358a",
"--background-accent": "#b597fe",
"--primary-cta-text": "#030128",
"--secondary-cta-text": "#d5d4f6"
},
"ruby": {
"--background": "#000000",
"--card": "#481f1f",
"--foreground": "#ffffff",
"--primary-cta": "#ffffff",
"--secondary-cta": "#361311",
"--accent": "#51000b",
"--background-accent": "#ff2231",
"--primary-cta-text": "#280101",
"--secondary-cta-text": "#f6d4d4"
},
"emerald": {
"--background": "#000000",
"--card": "#1f4035",
"--foreground": "#ffffff",
"--primary-cta": "#ffffff",
"--secondary-cta": "#0d2b1f",
"--accent": "#0d5238",
"--background-accent": "#10b981",
"--primary-cta-text": "#051a12",
"--secondary-cta-text": "#d4f6e8"
},
"indigo": {
"--background": "#000000",
"--card": "#1f1f40",
"--foreground": "#ffffff",
"--primary-cta": "#ffffff",
"--secondary-cta": "#0d0d2b",
"--accent": "#3d2880",
"--background-accent": "#663cff",
"--primary-cta-text": "#0a051a",
"--secondary-cta-text": "#d4d4f6"
},
"forest": {
"--background": "#000000",
"--card": "#1a2f1d",
"--foreground": "#ffffff",
"--primary-cta": "#ffffff",
"--secondary-cta": "#0d200f",
"--accent": "#1a3d1f",
"--background-accent": "#355e3b",
"--primary-cta-text": "#0a1a0c",
"--secondary-cta-text": "#d4f6d8"
},
"mint": {
"--background": "#000000",
"--card": "#1a2a1a",
"--foreground": "#ffffff",
"--primary-cta": "#ffffff",
"--secondary-cta": "#0d1a0d",
"--accent": "#2d4a2d",
"--background-accent": "#c1e1c1",
"--primary-cta-text": "#0a150a",
"--secondary-cta-text": "#e1f6e1"
}
}
}

41
cssOptions.json Normal file
View File

@@ -0,0 +1,41 @@
{
"cards": {
"solid": "background: var(--color-card);",
"outline": "background: var(--color-card);\nborder: 1px solid color-mix(in srgb, var(--color-accent) 25%, transparent);",
"gradient-mesh": "background:\n radial-gradient(at 0% 0%, color-mix(in srgb, var(--color-accent) 15%, transparent) 0px, transparent 50%),\n radial-gradient(at 100% 0%, color-mix(in srgb, var(--color-accent) 10%, transparent) 0px, transparent 50%),\n radial-gradient(at 100% 100%, color-mix(in srgb, var(--color-accent) 20%, transparent) 0px, transparent 50%),\n radial-gradient(at 0% 100%, color-mix(in srgb, var(--color-accent) 12%, transparent) 0px, transparent 50%),\n var(--color-card);",
"gradient-radial": "background: radial-gradient(circle at center, color-mix(in srgb, var(--color-card) 100%, var(--color-accent) 20%) 0%, var(--color-card) 90%);",
"inset": "background: color-mix(in srgb, var(--color-card) 95%, var(--color-accent) 5%);\nbox-shadow:\n inset 2px 2px 4px color-mix(in srgb, var(--color-foreground) 8%, transparent),\n inset -2px -2px 4px color-mix(in srgb, var(--color-background) 20%, transparent);",
"glass-elevated": "backdrop-filter: blur(8px);\nbackground: linear-gradient(to bottom right, color-mix(in srgb, var(--color-card) 80%, transparent), color-mix(in srgb, var(--color-card) 40%, transparent));\nbox-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\nborder: 1px solid var(--color-card);",
"glass-depth": "background: color-mix(in srgb, var(--color-card) 80%, transparent);\nbackdrop-filter: blur(14px);\nbox-shadow:\n inset 0 0 20px 0 color-mix(in srgb, var(--color-accent) 7.5%, transparent);\nborder: 1px solid color-mix(in srgb, var(--color-accent) 7.5%, transparent);",
"gradient-bordered": "background: linear-gradient(180deg, color-mix(in srgb, var(--color-card) 100%, var(--color-accent) 5%) -35%, var(--color-card) 65%);\nbox-shadow: 0px 0px 10px 4px color-mix(in srgb, var(--color-accent) 4%, transparent);\nborder: 1px solid color-mix(in srgb, var(--color-accent) 15%, transparent);",
"layered-gradient": "background:\n linear-gradient(color-mix(in srgb, var(--color-accent) 6%, transparent) 0%, transparent 59.26%),\n linear-gradient(var(--color-card) 0%, var(--color-card) 100%),\n var(--color-card);\nbox-shadow:\n 20px 18px 7px color-mix(in srgb, var(--color-accent) 0%, transparent),\n 2px 2px 2px color-mix(in srgb, var(--color-accent) 6.5%, transparent),\n 1px 1px 2px color-mix(in srgb, var(--color-accent) 2%, transparent);\nborder: 2px solid var(--color-secondary-cta);",
"soft-shadow": "background: var(--color-card);\nbox-shadow: color-mix(in srgb, var(--color-accent) 10%, transparent) 0px 0.706592px 0.706592px -0.666667px, color-mix(in srgb, var(--color-accent) 8%, transparent) 0px 1.80656px 1.80656px -1.33333px, color-mix(in srgb, var(--color-accent) 7%, transparent) 0px 3.62176px 3.62176px -2px, color-mix(in srgb, var(--color-accent) 7%, transparent) 0px 6.8656px 6.8656px -2.66667px, color-mix(in srgb, var(--color-accent) 5%, transparent) 0px 13.6468px 13.6468px -3.33333px, color-mix(in srgb, var(--color-accent) 2%, transparent) 0px 30px 30px -4px, var(--color-background) 0px 3px 1px 0px inset;",
"subtle-shadow": "background: var(--color-card);\nbox-shadow: color-mix(in srgb, var(--color-foreground) 5%, transparent) 0px 4px 32px 0px;",
"elevated-border": "background: linear-gradient(180deg, color-mix(in srgb, var(--color-card) 100%, var(--color-foreground) 3%) 0%, var(--color-card) 100%);\nbox-shadow: 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 8%, transparent), 0 4px 6px -1px color-mix(in srgb, var(--color-foreground) 5%, transparent), 0 10px 15px -3px color-mix(in srgb, var(--color-foreground) 4%, transparent);\nborder: 1px solid color-mix(in srgb, var(--color-foreground) 6%, transparent);",
"inner-glow": "background: var(--color-card);\nbox-shadow: inset 0 0 30px 0 color-mix(in srgb, var(--color-foreground) 4%, transparent), inset 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 8%, transparent), 0 4px 12px -4px color-mix(in srgb, var(--color-foreground) 8%, transparent);",
"spotlight": "background:\n radial-gradient(ellipse at 0% 0%, color-mix(in srgb, var(--color-accent) 20%, transparent) 0%, transparent 50%),\n var(--color-card);\nbox-shadow: inset 1px 1px 0 0 color-mix(in srgb, var(--color-foreground) 10%, transparent), 0 4px 16px -4px color-mix(in srgb, var(--color-foreground) 10%, transparent);"
},
"primaryButtons": {
"gradient": "background: linear-gradient(to bottom, color-mix(in srgb, var(--color-primary-cta) 75%, transparent), var(--color-primary-cta));\nbox-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;",
"shadow": "background: var(--color-primary-cta);\nbox-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-primary-cta) 40%, transparent);",
"flat": "background: var(--color-primary-cta);",
"radial-glow": "background:\n radial-gradient(circle at 0% 0%, color-mix(in srgb, var(--color-background) 32.5%, transparent) 0%, transparent 45%),\n radial-gradient(circle at 100% 100%, color-mix(in srgb, var(--color-background) 32.5%, transparent) 0%, transparent 45%),\n var(--color-primary-cta);\nbox-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 30%, transparent);",
"diagonal-gradient": "background: linear-gradient(to bottom right, color-mix(in srgb, var(--color-primary-cta) 80%, transparent), var(--color-foreground));\nbox-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 30%, transparent);",
"double-inset": "background: var(--color-primary-cta);\nbox-shadow: color-mix(in srgb, var(--color-background) 15%, transparent) 0px 4px 10px 0px inset, color-mix(in srgb, var(--color-background) 15%, transparent) 0px -4px 8px 0px inset;",
"primary-glow": "background: var(--color-primary-cta);\nbox-shadow: color-mix(in srgb, var(--color-background) 20%, transparent) 0px 3px 1px 0px inset, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 0.839802px 0.503881px -0.3125px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 1.99048px 1.19429px -0.625px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 3.63084px 2.1785px -0.9375px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 6.03627px 3.62176px -1.25px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 9.74808px 5.84885px -1.5625px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 15.9566px 9.57398px -1.875px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 27.4762px 16.4857px -2.1875px, color-mix(in srgb, var(--color-primary-cta) 13%, transparent) 0px 50px 30px -2.5px;",
"inset-glow": "background: linear-gradient(180deg, color-mix(in srgb, var(--color-primary-cta) 65%, var(--color-background)) -35%, var(--color-primary-cta) 65%);\nbox-shadow: 0 10px 18px -7px color-mix(in srgb, var(--color-background) 50%, transparent), inset 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 15%, transparent);\nborder: 1px solid color-mix(in srgb, var(--color-foreground) 20%, transparent);",
"soft-glow": "background: radial-gradient(ellipse at 50% -20%, color-mix(in srgb, var(--color-primary-cta) 70%, var(--color-foreground)) 0%, var(--color-primary-cta) 70%);\nbox-shadow: 0 8px 24px -6px color-mix(in srgb, var(--color-primary-cta) 35%, transparent), inset 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 20%, transparent);",
"glass-shimmer": "background: linear-gradient(165deg, color-mix(in srgb, var(--color-primary-cta) 85%, var(--color-foreground)) 0%, var(--color-primary-cta) 40%, color-mix(in srgb, var(--color-primary-cta) 90%, var(--color-background)) 100%);\nbox-shadow: inset 0 1px 1px 0 color-mix(in srgb, var(--color-foreground) 25%, transparent), inset 0 -1px 1px 0 color-mix(in srgb, var(--color-background) 15%, transparent), 0 4px 12px -2px color-mix(in srgb, var(--color-primary-cta) 25%, transparent);",
"neon-outline": "background: var(--color-primary-cta);\nbox-shadow: 0 0 5px color-mix(in srgb, var(--color-accent) 50%, transparent), 0 0 15px color-mix(in srgb, var(--color-accent) 30%, transparent), 0 0 30px color-mix(in srgb, var(--color-accent) 15%, transparent), inset 0 0 8px color-mix(in srgb, var(--color-accent) 10%, transparent);",
"lifted": "background: linear-gradient(180deg, color-mix(in srgb, var(--color-primary-cta) 95%, var(--color-foreground)) 0%, var(--color-primary-cta) 50%, color-mix(in srgb, var(--color-primary-cta) 95%, var(--color-background)) 100%);\nbox-shadow: inset 0 2px 3px 0 color-mix(in srgb, var(--color-foreground) 20%, transparent), inset 0 -2px 3px 0 color-mix(in srgb, var(--color-background) 25%, transparent), 0 2px 4px -1px color-mix(in srgb, var(--color-background) 40%, transparent);",
"depth-layers": "background: var(--color-primary-cta);\nbox-shadow: 0 1px 2px color-mix(in srgb, var(--color-primary-cta) 20%, transparent), 0 2px 4px color-mix(in srgb, var(--color-primary-cta) 20%, transparent), 0 4px 8px color-mix(in srgb, var(--color-primary-cta) 15%, transparent), 0 8px 16px color-mix(in srgb, var(--color-primary-cta) 10%, transparent), 0 16px 32px color-mix(in srgb, var(--color-primary-cta) 5%, transparent);",
"accent-edge": "background: linear-gradient(180deg, var(--color-primary-cta) 0%, color-mix(in srgb, var(--color-primary-cta) 90%, var(--color-background)) 100%);\nbox-shadow: 0 0 0 1px color-mix(in srgb, var(--color-accent) 60%, transparent), 0 4px 12px -2px color-mix(in srgb, var(--color-accent) 35%, transparent), inset 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 20%, transparent);",
"metallic": "background: linear-gradient(135deg, color-mix(in srgb, var(--color-primary-cta) 80%, var(--color-foreground)) 0%, var(--color-primary-cta) 25%, color-mix(in srgb, var(--color-primary-cta) 90%, var(--color-background)) 50%, var(--color-primary-cta) 75%, color-mix(in srgb, var(--color-primary-cta) 85%, var(--color-foreground)) 100%);\nbox-shadow: inset 0 1px 0 0 color-mix(in srgb, var(--color-foreground) 30%, transparent), 0 3px 8px -2px color-mix(in srgb, var(--color-background) 50%, transparent);"
},
"secondaryButtons": {
"glass": "backdrop-filter: blur(8px);\nbackground: linear-gradient(to bottom right, color-mix(in srgb, var(--color-secondary-cta) 80%, transparent), var(--color-secondary-cta));\nbox-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\nborder: 1px solid var(--color-secondary-cta);",
"solid": "background: var(--color-secondary-cta);",
"layered": "background:\n linear-gradient(color-mix(in srgb, var(--color-accent) 5%, transparent) 0%, transparent 59.26%),\n linear-gradient(var(--color-secondary-cta), var(--color-secondary-cta)),\n linear-gradient(var(--color-secondary-cta), var(--color-secondary-cta)),\n linear-gradient(color-mix(in srgb, var(--color-accent) 5%, transparent) 0%, transparent 59.26%),\n linear-gradient(color-mix(in srgb, var(--color-secondary-cta) 60%, transparent), color-mix(in srgb, var(--color-secondary-cta) 60%, transparent)),\n var(--color-secondary-cta);\nbox-shadow:\n 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 10%, transparent);\nborder: 1px solid var(--color-secondary-cta);",
"radial-glow": "background:\n radial-gradient(circle at 0% 0%, color-mix(in srgb, var(--color-accent) 15%, transparent) 0%, transparent 40%),\n radial-gradient(circle at 100% 100%, color-mix(in srgb, var(--color-accent) 15%, transparent) 0%, transparent 40%),\n var(--color-secondary-cta);\nbox-shadow: 2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 10%, transparent);"
}
}

23
eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

237
fontThemes.json Normal file
View File

@@ -0,0 +1,237 @@
{
"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;"
},
"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;"
},
"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;"
},
"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;"
},
"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;"
},
"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;"
},
"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;"
},
"nunitoSans": {
"name": "Nunito Sans",
"import": "import { Nunito_Sans } from \"next/font/google\";",
"initialization": "const nunitoSans = Nunito_Sans({\n variable: \"--font-nunito-sans\",\n subsets: [\"latin\"],\n});",
"className": "${nunitoSans.variable}",
"cssVariable": "font-family: var(--font-nunito-sans), sans-serif;"
},
"raleway": {
"name": "Raleway",
"import": "import { Raleway } from \"next/font/google\";",
"initialization": "const raleway = Raleway({\n variable: \"--font-raleway\",\n subsets: [\"latin\"],\n});",
"className": "${raleway.variable}",
"cssVariable": "font-family: var(--font-raleway), sans-serif;"
},
"archivo": {
"name": "Archivo",
"import": "import { Archivo } from \"next/font/google\";",
"initialization": "const archivo = Archivo({\n variable: \"--font-archivo\",\n subsets: [\"latin\"],\n});",
"className": "${archivo.variable}",
"cssVariable": "font-family: var(--font-archivo), sans-serif;"
},
"figtree": {
"name": "Figtree",
"import": "import { Figtree } from \"next/font/google\";",
"initialization": "const figtree = Figtree({\n variable: \"--font-figtree\",\n subsets: [\"latin\"],\n});",
"className": "${figtree.variable}",
"cssVariable": "font-family: var(--font-figtree), sans-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"
}
},
"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"
}
},
"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"
}
},
"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"
}
},
"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"
}
},
"libreBaskervilleInter": {
"name": "Libre Baskerville + Inter",
"description": "Classic serif headings with neutral body. Elegant and readable.",
"headingFont": "libreBaskerville",
"bodyFont": "inter",
"imports": "import { Libre_Baskerville } from \"next/font/google\";\nimport { Inter } from \"next/font/google\";",
"initializations": "const libreBaskerville = Libre_Baskerville({\n variable: \"--font-libre-baskerville\",\n subsets: [\"latin\"],\n weight: [\"400\", \"700\"],\n});\n\nconst inter = Inter({\n variable: \"--font-inter\",\n subsets: [\"latin\"],\n});",
"classNames": "${libreBaskerville.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-libre-baskerville), serif;\n}",
"bodyFontFamily": "var(--font-inter), sans-serif",
"headingsFontFamily": "var(--font-libre-baskerville), serif"
}
}
}
}

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Webild Components</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

40
package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "webild-components",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"typecheck": "tsc -b"
},
"dependencies": {
"@tailwindcss/vite": "^4.2.2",
"clsx": "^2.1.1",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"motion": "^12.38.0",
"lucide-react": "^1.7.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.0",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.0",
"vite": "^8.0.4"
}
}

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

267
registry.json Normal file
View File

@@ -0,0 +1,267 @@
{
"componentRegistry": {
"navbar": [
{
"name": "NavbarCentered",
"import": "import NavbarCentered from '@/components/ui/NavbarCentered';",
"path": "@/components/ui/NavbarCentered",
"description": "Centered navigation bar with logo, links and buttons",
"propsSchema": {
"logo": "string",
"navItems": "{ name: string; href: string }[]",
"ctaButton": "{ text: string; href: string }"
}
}
]
},
"sectionRegistry": {
"hero": [
{
"name": "HeroBillboard",
"import": "import HeroBillboard from '@/components/sections/hero/HeroBillboard';",
"path": "@/components/sections/hero/HeroBillboard",
"description": "Full-width centered hero with media below text content",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton": "{ text: string; href: string }",
"secondaryButton": "{ text: string; href: string }",
"imageSrc": "string \u2014 image URL (provide imageSrc OR videoSrc, not both)",
"videoSrc": "string \u2014 video URL (provide imageSrc OR videoSrc, not both)"
}
},
{
"name": "HeroBillboardGallery",
"import": "import HeroBillboardGallery from '@/components/sections/hero/HeroBillboardGallery';",
"path": "@/components/sections/hero/HeroBillboardGallery",
"description": "Full-width centered hero with image gallery below",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton": "{ text: string; href: string }",
"secondaryButton": "{ text: string; href: string }",
"items": "({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[]"
}
},
{
"name": "HeroSplit",
"import": "import HeroSplit from '@/components/sections/hero/HeroSplit';",
"path": "@/components/sections/hero/HeroSplit",
"description": "Split layout hero with text on left, media on right",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton": "{ text: string; href: string }",
"secondaryButton": "{ text: string; href: string }",
"imageSrc": "string \u2014 image URL (provide imageSrc OR videoSrc, not both)",
"videoSrc": "string \u2014 video URL (provide imageSrc OR videoSrc, not both)"
}
}
],
"about": [
{
"name": "AboutFeaturesSplit",
"import": "import AboutFeaturesSplit from '@/components/sections/about/AboutFeaturesSplit';",
"path": "@/components/sections/about/AboutFeaturesSplit",
"description": "About section with feature list on left, media on right",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"items": "{ icon: LucideIcon (import from lucide-react, pass as component reference e.g. icon={Star} not icon=\"Star\"); title: string; description: string }[]",
"imageSrc": "string \u2014 image URL (provide imageSrc OR videoSrc, not both)",
"videoSrc": "string \u2014 video URL (provide imageSrc OR videoSrc, not both)"
}
},
{
"name": "AboutMediaOverlay",
"import": "import AboutMediaOverlay from '@/components/sections/about/AboutMediaOverlay';",
"path": "@/components/sections/about/AboutMediaOverlay",
"description": "About section with text overlaid on media background",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"imageSrc": "string \u2014 image URL (provide imageSrc OR videoSrc, not both)",
"videoSrc": "string \u2014 video URL (provide imageSrc OR videoSrc, not both)"
}
},
{
"name": "AboutTestimonial",
"import": "import AboutTestimonial from '@/components/sections/about/AboutTestimonial';",
"path": "@/components/sections/about/AboutTestimonial",
"description": "Testimonial section with quote and author info",
"propsSchema": {
"tag": "string",
"quote": "string",
"author": "string",
"role": "string",
"imageSrc": "string \u2014 image URL (provide imageSrc OR videoSrc, not both)",
"videoSrc": "string \u2014 video URL (provide imageSrc OR videoSrc, not both)"
}
},
{
"name": "AboutTextSplit",
"import": "import AboutTextSplit from '@/components/sections/about/AboutTextSplit';",
"path": "@/components/sections/about/AboutTextSplit",
"description": "Text-only about section with title on left, descriptions on right",
"propsSchema": {
"title": "string",
"descriptions": "string[]",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }"
}
}
],
"features": [
{
"name": "FeaturesMediaCards",
"import": "import FeaturesMediaCards from '@/components/sections/features/FeaturesMediaCards';",
"path": "@/components/sections/features/FeaturesMediaCards",
"description": "Features section with media cards in grid or carousel",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"items": "({ title: string; description: string } & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }))[]"
}
},
{
"name": "FeaturesAlternatingSplit",
"import": "import FeaturesAlternatingSplit from '@/components/sections/features/FeaturesAlternatingSplit';",
"path": "@/components/sections/features/FeaturesAlternatingSplit",
"description": "Features section with numbered alternating left/right split cards",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"items": "({ title: string; description: string; primaryButton?: { text: string; href: string }; secondaryButton?: { text: string; href: string } } & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }))[]"
}
},
{
"name": "FeaturesLabeledList",
"import": "import FeaturesLabeledList from '@/components/sections/features/FeaturesLabeledList';",
"path": "@/components/sections/features/FeaturesLabeledList",
"description": "Features section with large labels and bullet-separated item lists",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"items": "{ label: string; title: string; bullets: string[]; primaryButton: { text: string; href: string }; secondaryButton: { text: string; href: string } }[]"
}
},
{
"name": "FeaturesComparison",
"import": "import FeaturesComparison from '@/components/sections/features/FeaturesComparison';",
"path": "@/components/sections/features/FeaturesComparison",
"description": "Features comparison section with negative and positive lists side by side",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"negativeItems": "string[]",
"positiveItems": "string[]"
}
},
{
"name": "FeaturesDetailedSteps",
"import": "import FeaturesDetailedSteps from '@/components/sections/features/FeaturesDetailedSteps';",
"path": "@/components/sections/features/FeaturesDetailedSteps",
"description": "Features section with detailed step cards including tag, title, subtitle, description, and tilted media",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"steps": "({ tag: string; title: string; subtitle: string; description: string } & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }))[]"
}
},
{
"name": "FeaturesTaggedCards",
"import": "import FeaturesTaggedCards from '@/components/sections/features/FeaturesTaggedCards';",
"path": "@/components/sections/features/FeaturesTaggedCards",
"description": "Features section with media cards featuring tag overlay on image and content card below",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"items": "({ tag: string; title: string; description: string; primaryButton?: { text: string; href: string }; secondaryButton?: { text: string; href: string } } & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }))[]"
}
},
{
"name": "FeaturesArrowCards",
"import": "import FeaturesArrowCards from '@/components/sections/features/FeaturesArrowCards';",
"path": "@/components/sections/features/FeaturesArrowCards",
"description": "Features section with media cards featuring title, tags, and arrow icon for navigation",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"items": "({ title: string; tags: string[] } & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }))[]"
}
},
{
"name": "FeaturesDetailedCards",
"import": "import FeaturesDetailedCards from '@/components/sections/features/FeaturesDetailedCards';",
"path": "@/components/sections/features/FeaturesDetailedCards",
"description": "Features section with horizontal split cards featuring title, author, tags, description, and media",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"items": "({ title: string; description: string; tags: string[] } & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }))[]"
}
},
{
"name": "FeaturesDualMedia",
"import": "import FeaturesDualMedia from '@/components/sections/features/FeaturesDualMedia';",
"path": "@/components/sections/features/FeaturesDualMedia",
"description": "Features section with icon cards featuring two media items side by side",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"items": "{ icon: LucideIcon (import from lucide-react, pass as component reference e.g. icon={Star} not icon=\"Star\"); title: string; description: string; mediaItems: [({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }), ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })] }[]"
}
},
{
"name": "FeaturesMediaCarousel",
"import": "import FeaturesMediaCarousel from '@/components/sections/features/FeaturesMediaCarousel';",
"path": "@/components/sections/features/FeaturesMediaCarousel",
"description": "Carousel of cards with media background and text overlay at bottom",
"propsSchema": {
"tag": "string",
"title": "string",
"description": "string",
"primaryButton?": "{ text: string; href: string }",
"secondaryButton?": "{ text: string; href: string }",
"items": "({ title: string; description: string; buttonIcon: LucideIcon (import from lucide-react, pass as component reference e.g. icon={Star} not icon=\"Star\"); buttonHref?: string; buttonOnClick?: () => void } & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }))[]"
}
}
]
}
}

94
src/App.tsx Normal file
View File

@@ -0,0 +1,94 @@
import AboutTestimonial from '@/components/sections/about/AboutTestimonial';
import FeaturesDetailedSteps from '@/components/sections/features/FeaturesDetailedSteps';
import FeaturesTaggedCards from '@/components/sections/features/FeaturesTaggedCards';
import HeroBillboard from '@/components/sections/hero/HeroBillboard';
import NavbarCentered from '@/components/ui/NavbarCentered';
export default function App() {
return (
<>
<div id="nav" data-section="nav">
<NavbarCentered
logo="SweetCrumb"
navItems={[
{
name: "Home",
id: "hero",
},
{
name: "Our Story",
id: "about",
},
{
name: "Specials",
id: "products",
},
]}
ctaButton={{
text: "Order Now",
href: "#order",
}}
/>
</div>
<div id="hero" data-section="hero">
<HeroBillboard
tag="Est. 1994"
title="Baking Warmth Daily"
description="Authentic, artisanal bread and pastries baked from scratch every morning in the heart of the city."
primaryButton={{
text: "Browse Menu",
href: "#products",
}}
secondaryButton={{
text: "Learn More",
href: "#about",
}}
imageSrc="https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=eubn7c"
/>
</div>
<div id="about" data-section="about">
<AboutTestimonial
tag="Our Philosophy"
quote="Our bread is the product of time, love, and the finest local ingredients. We believe in simplicity and patience."
author="Elena Rossi"
role="Head Baker"
imageSrc="https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=ix09w5"
/>
</div>
<div id="products" data-section="products">
<FeaturesTaggedCards
tag="Signature Baked Goods"
title="Fresh from the Oven"
description="Explore our daily selection of handcrafted delights made with traditional techniques."
items={[
{
tag: "Signature",
title: "Butter Croissants",
description: "Layers of buttery, flaky pastry folded 27 times.",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=1wtxpt",
},
{
tag: "Artisan",
title: "Rustic Sourdough",
description: "Naturally leavened loaf with a crackling crust.",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=cdpsmo",
},
{
tag: "Seasonal",
title: "Berry Tart",
description: "Fresh berries on a bed of vanilla cream.",
imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/default/no-image.jpg?id=beel6i",
},
]}
/>
</div>
<div id="features" data-section="features">
<FeaturesDetailedSteps />
</div>
</>
);
}

View File

@@ -0,0 +1,80 @@
import type { LucideIcon } from "lucide-react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type AboutFeaturesSplitProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: { icon: LucideIcon; title: string; description: string }[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const AboutFeaturesSplit = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
imageSrc,
videoSrc,
}: AboutFeaturesSplitProps) => {
return (
<section aria-label="About section" className="py-20">
<div className="flex flex-col gap-8 mx-auto w-content-width">
<div className="flex flex-col items-center gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<div className="flex flex-col md:flex-row md:items-stretch gap-6">
<div className="flex flex-col justify-center gap-6 p-6 w-full md:w-4/10 2xl:w-3/10 card rounded">
{items.map((item, index) => (
<div key={item.title}>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-center shrink-0 mb-1 h-10 w-10 primary-button rounded">
<item.icon className="h-2/5 w-2/5 text-primary-cta-text" strokeWidth={1.5} />
</div>
<h3 className="text-xl font-medium">{item.title}</h3>
<p className="text-base leading-tight">{item.description}</p>
</div>
{index < items.length - 1 && (
<div className="mt-6 border-b border-accent/40" />
)}
</div>
))}
</div>
<div className="p-4 w-full md:w-6/10 2xl:w-7/10 h-80 md:h-auto card rounded overflow-hidden">
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} />
</div>
</div>
</div>
</section>
);
};
export default AboutFeaturesSplit;

View File

@@ -0,0 +1,70 @@
import { motion } from "motion/react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type AboutMediaOverlayProps = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const AboutMediaOverlay = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
}: AboutMediaOverlayProps) => {
return (
<section aria-label="About section" className="py-20">
<div className="relative flex items-center justify-center py-8 md:py-12 mx-auto w-content-width rounded overflow-hidden">
<div className="absolute inset-0">
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} />
<div className="absolute inset-0 bg-background/40 backdrop-blur-xs pointer-events-none select-none" />
</div>
<div className="relative z-10 flex items-center justify-center px-5 py-10 mx-auto min-h-100 md:min-h-120 md:w-1/2 w-content-width">
<div className="flex flex-col items-center gap-3 md:gap-1 text-center">
<motion.span
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="mb-3 px-3 py-1 text-sm card rounded"
>
{tag}
</motion.span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="text-base md:text-lg leading-tight"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap max-md:justify-center gap-4 mt-3">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
</div>
</div>
</section>
);
};
export default AboutMediaOverlay;

View File

@@ -0,0 +1,61 @@
import { motion } from "motion/react";
import { Quote } from "lucide-react";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type AboutTestimonialProps = {
tag: string;
quote: string;
author: string;
role: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const AboutTestimonial = ({
tag,
quote,
author,
role,
imageSrc,
videoSrc,
}: AboutTestimonialProps) => {
return (
<section aria-label="Testimonial section" className="py-20">
<div className="grid grid-cols-1 md:grid-cols-5 gap-6 mx-auto w-content-width">
<div className="relative md:col-span-3 p-8 md:px-12 md:py-20 card rounded">
<div className="absolute flex items-center justify-center -top-7 -left-7 md:-top-8 md:-left-8 h-14 md:h-16 aspect-square primary-button rounded">
<Quote className="h-5/10 text-primary-cta-text" strokeWidth={1.5} />
</div>
<div className="relative flex flex-col justify-center gap-4 md:gap-6 py-8 md:py-4 h-full">
<span className="w-fit px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={quote}
variant="slide-up"
tag="p"
className="text-3xl md:text-4xl font-medium leading-tight"
/>
<div className="flex items-center gap-2">
<span className="text-base">{author}</span>
<span className="text-accent"></span>
<span className="text-base text-foreground/75">{role}</span>
</div>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="md:col-span-2 aspect-square md:aspect-auto md:h-full card rounded overflow-hidden"
>
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} />
</motion.div>
</div>
</section>
);
};
export default AboutTestimonial;

View File

@@ -0,0 +1,56 @@
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
interface AboutTextSplitProps {
title: string;
descriptions: string[];
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
}
const AboutTextSplit = ({
title,
descriptions,
primaryButton,
secondaryButton,
}: AboutTextSplitProps) => {
return (
<section aria-label="About section" className="py-20">
<div className="flex flex-col gap-30 mx-auto w-content-width">
<div className="flex flex-col md:flex-row gap-3 md:gap-15">
<div className="w-full md:w-1/2">
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-7xl font-medium"
/>
</div>
<div className="flex flex-col gap-6 w-full md:w-1/2">
{descriptions.map((desc, index) => (
<TextAnimation
key={index}
text={desc}
variant="slide-up"
tag="p"
className="text-base md:text-2xl leading-tight text-foreground/75"
/>
))}
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap max-md:justify-center gap-4">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
</div>
<div className="w-full border-b border-foreground/10" />
</div>
</section>
);
};
export default AboutTextSplit;

View File

@@ -0,0 +1,93 @@
import { motion } from "motion/react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import { cls } from "@/lib/utils";
type FeatureItem = {
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesAlternatingSplitProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesAlternatingSplit = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesAlternatingSplitProps) => {
return (
<section aria-label="Features section" className="py-20">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<div className="flex flex-col gap-4 w-content-width mx-auto">
{items.map((item, index) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className={cls("flex flex-col gap-6 md:gap-12 p-6 md:p-12 card rounded", index % 2 === 0 ? "md:flex-row" : "md:flex-row-reverse")}
>
<div className="flex flex-col justify-center w-full md:w-1/2 gap-3">
<span className="flex items-center justify-center h-8 w-8 mb-1 text-sm rounded primary-button text-primary-cta-text">
{index + 1}
</span>
<h3 className="text-4xl md:text-5xl font-medium leading-tight text-balance">{item.title}</h3>
<p className="text-base leading-tight text-balance">{item.description}</p>
{(item.primaryButton || item.secondaryButton) && (
<div className="flex flex-wrap gap-4 mt-2">
{item.primaryButton && <Button text={item.primaryButton.text} href={item.primaryButton.href} variant="primary" />}
{item.secondaryButton && <Button text={item.secondaryButton.text} href={item.secondaryButton.href} variant="secondary" />}
</div>
)}
</div>
<div className="w-full md:w-1/2 aspect-square rounded overflow-hidden">
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} />
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesAlternatingSplit;

View File

@@ -0,0 +1,90 @@
import { motion } from "motion/react";
import { ArrowRight } from "lucide-react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import GridOrCarousel from "@/components/ui/GridOrCarousel";
type FeatureItem = {
title: string;
tags: string[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesArrowCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesArrowCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesArrowCardsProps) => {
return (
<section aria-label="Features section" className="py-20">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<GridOrCarousel carouselThreshold={3}>
{items.map((item) => (
<div key={item.title} className="flex flex-col h-full gap-4 cursor-pointer group">
<div className="aspect-square rounded overflow-hidden">
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="transition-transform duration-500 ease-in-out group-hover:scale-105" />
</div>
<div className="flex flex-col justify-between flex-1 gap-4 p-5 card rounded">
<h3 className="text-xl md:text-2xl font-medium leading-tight">{item.title}</h3>
<div className="flex items-center justify-between gap-4">
<div className="flex flex-wrap items-center gap-2">
{item.tags.map((itemTag) => (
<span key={itemTag} className="px-3 py-1 text-sm card rounded">{itemTag}</span>
))}
</div>
<ArrowRight className="shrink-0 h-[1em] w-auto transition-transform duration-300 group-hover:-rotate-45" strokeWidth={1.5} />
</div>
</div>
</div>
))}
</GridOrCarousel>
</motion.div>
</div>
</section>
);
};
export default FeaturesArrowCards;

View File

@@ -0,0 +1,83 @@
import { motion } from "motion/react";
import { Check, X } from "lucide-react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
interface FeaturesComparisonProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
negativeItems: string[];
positiveItems: string[];
}
const FeaturesComparison = ({
tag,
title,
description,
primaryButton,
secondaryButton,
negativeItems,
positiveItems,
}: FeaturesComparisonProps) => {
return (
<section aria-label="Features comparison section" className="py-20">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="grid grid-cols-1 md:grid-cols-2 w-content-width md:w-6/10 mx-auto gap-4"
>
<div className="flex flex-col gap-4 p-6 card rounded opacity-50">
{negativeItems.map((item) => (
<div key={item} className="flex items-center gap-2 text-base">
<X className="shrink-0 h-[1em] w-auto" />
<span className="truncate">{item}</span>
</div>
))}
</div>
<div className="flex flex-col gap-4 p-6 card rounded">
{positiveItems.map((item) => (
<div key={item} className="flex items-center gap-2 text-base">
<Check className="shrink-0 h-[1em] w-auto" />
<span className="truncate">{item}</span>
</div>
))}
</div>
</motion.div>
</div>
</section>
);
};
export default FeaturesComparison;

View File

@@ -0,0 +1,94 @@
import { motion } from "motion/react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type FeatureItem = {
title: string;
description: string;
tags: string[];
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesDetailedCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesDetailedCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesDetailedCardsProps) => {
return (
<section aria-label="Features section" className="py-20">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<div className="flex flex-col w-content-width mx-auto gap-4">
{items.map((item) => (
<motion.article
key={item.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="flex flex-col md:grid md:grid-cols-10 2xl:w-8/10 mx-auto gap-6 md:gap-10 p-6 md:p-10 cursor-pointer card rounded group"
>
<div className="flex flex-col md:col-span-6 gap-3 md:gap-12">
<h3 className="text-3xl md:text-5xl font-medium leading-tight text-balance">{item.title}</h3>
<div className="flex flex-col mt-auto gap-4">
<div className="flex flex-wrap gap-2">
{item.tags.map((itemTag) => (
<span key={itemTag} className="px-3 py-1 text-sm card rounded">{itemTag}</span>
))}
</div>
<p className="text-base md:text-2xl leading-tight text-balance">
{item.description}
</p>
</div>
</div>
<div className="aspect-square md:col-span-4 rounded overflow-hidden">
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="transition-transform duration-500 ease-in-out group-hover:scale-105" />
</div>
</motion.article>
))}
</div>
</div>
</section>
);
};
export default FeaturesDetailedCards;

View File

@@ -0,0 +1,97 @@
import { motion } from "motion/react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import { cls } from "@/lib/utils";
type StepItem = {
tag: string;
title: string;
subtitle: string;
description: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesDetailedStepsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
steps: StepItem[];
}
const FeaturesDetailedSteps = ({
tag,
title,
description,
primaryButton,
secondaryButton,
steps,
}: FeaturesDetailedStepsProps) => {
return (
<section aria-label="Features detailed steps section" className="py-20">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<div className="flex flex-col w-content-width mx-auto gap-4">
{steps.map((step, index) => {
const stepNumber = String(index + 1).padStart(2, "0");
return (
<motion.div
key={step.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="flex flex-col md:flex-row justify-between 2xl:w-8/10 mx-auto gap-6 md:gap-12 p-6 md:p-12 card rounded overflow-hidden"
>
<div className="flex flex-col justify-between w-full md:w-1/2">
<div className="flex flex-col gap-4 md:gap-6">
<span className="w-fit px-3 py-1 text-sm card rounded">{step.tag}</span>
<h3 className="text-5xl md:text-8xl font-medium leading-none">{step.title}</h3>
</div>
<div className="block md:hidden w-full h-px my-6 bg-accent/20" />
<div className="flex flex-col gap-2 md:gap-4">
<h4 className="text-2xl md:text-3xl font-medium text-balance">{step.subtitle}</h4>
<p className="text-base md:text-lg leading-tight text-balance">{step.description}</p>
</div>
</div>
<div className="flex flex-col w-full md:w-35/100 gap-10">
<span className="hidden md:block self-end text-8xl font-medium text-accent">{stepNumber}</span>
<div className={cls("aspect-square rounded overflow-hidden", index % 2 === 0 ? "rotate-3" : "-rotate-3")}>
<ImageOrVideo imageSrc={step.imageSrc} videoSrc={step.videoSrc} />
</div>
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
};
export default FeaturesDetailedSteps;

View File

@@ -0,0 +1,99 @@
import { motion } from "motion/react";
import type { LucideIcon } from "lucide-react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import GridOrCarousel from "@/components/ui/GridOrCarousel";
type FeatureItem = {
icon: LucideIcon;
title: string;
description: string;
mediaItems: [
({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never }),
({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })
];
};
interface FeaturesDualMediaProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesDualMedia = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesDualMediaProps) => {
return (
<section aria-label="Features section" className="py-20">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<GridOrCarousel carouselThreshold={2}>
{items.map((item) => {
const IconComponent = item.icon;
return (
<div key={item.title} className="flex flex-col h-full gap-5 p-5 card rounded">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-center mb-1 h-15 w-15 primary-button rounded">
<IconComponent className="h-2/5 w-2/5 text-primary-cta-text" strokeWidth={1.5} />
</div>
<h3 className="text-2xl font-medium leading-tight">{item.title}</h3>
<p className="text-base leading-tight">{item.description}</p>
</div>
<div className="grid grid-cols-2 flex-1 mt-auto gap-5">
{item.mediaItems.map((mediaItem, mediaIndex) => (
<div key={mediaIndex} className="aspect-square rounded overflow-hidden">
<ImageOrVideo imageSrc={mediaItem.imageSrc} videoSrc={mediaItem.videoSrc} />
</div>
))}
</div>
</div>
);
})}
</GridOrCarousel>
</motion.div>
</div>
</section>
);
};
export default FeaturesDualMedia;

View File

@@ -0,0 +1,100 @@
import { Fragment } from "react";
import { motion } from "motion/react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
type FeatureItem = {
label: string;
title: string;
bullets: string[];
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
};
interface FeaturesLabeledListProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesLabeledList = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesLabeledListProps) => {
return (
<section aria-label="Features section" className="py-20">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<div className="flex flex-col gap-4 w-content-width mx-auto">
{items.map((item) => (
<motion.div
key={item.label}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="flex flex-col md:flex-row gap-6 md:gap-12 p-6 md:p-12 card rounded"
>
<div className="w-full md:w-1/2 flex md:justify-start">
<h3 className="text-7xl font-medium leading-tight truncate">{item.label}</h3>
</div>
<div className="w-full h-px bg-foreground/20 md:hidden" />
<div className="flex flex-col w-full md:w-1/2 gap-4">
<h4 className="text-2xl md:text-3xl font-medium leading-tight">{item.title}</h4>
<div className="flex flex-wrap items-center gap-1">
{item.bullets.map((text, index) => (
<Fragment key={index}>
<span className="text-base">{text}</span>
{index < item.bullets.length - 1 && <span className="text-base text-accent"></span>}
</Fragment>
))}
</div>
<div className="flex flex-wrap gap-4 mt-2">
<Button text={item.primaryButton.text} href={item.primaryButton.href} variant="primary" />
<Button text={item.secondaryButton.text} href={item.secondaryButton.href} variant="secondary" />
</div>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default FeaturesLabeledList;

View File

@@ -0,0 +1,82 @@
import { motion } from "motion/react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import GridOrCarousel from "@/components/ui/GridOrCarousel";
type FeatureItem = {
title: string;
description: string;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesMediaCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesMediaCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesMediaCardsProps) => {
return (
<section aria-label="Features section" className="py-20">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<GridOrCarousel carouselThreshold={3}>
{items.map((item) => (
<div key={item.title} className="flex flex-col gap-4 p-4 h-full card rounded">
<div className="aspect-square rounded overflow-hidden">
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} />
</div>
<div className="flex flex-col gap-1">
<h3 className="text-2xl font-medium leading-tight">{item.title}</h3>
<p className="text-sm leading-tight">{item.description}</p>
</div>
</div>
))}
</GridOrCarousel>
</motion.div>
</div>
</section>
);
};
export default FeaturesMediaCards;

View File

@@ -0,0 +1,98 @@
import type { LucideIcon } from "lucide-react";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import LoopCarousel from "@/components/ui/LoopCarousel";
import Button from "@/components/ui/Button";
import { useButtonClick } from "@/hooks/useButtonClick";
type FeatureItem = {
title: string;
description: string;
buttonIcon: LucideIcon;
buttonHref?: string;
buttonOnClick?: () => void;
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesMediaCarouselProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeatureMediaCarouselCard = ({ item }: { item: FeatureItem }) => {
const handleClick = useButtonClick(item.buttonHref, item.buttonOnClick);
const Icon = item.buttonIcon;
return (
<div className="relative overflow-hidden aspect-square md:aspect-3/2 rounded">
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="absolute inset-0" />
<div className="absolute bottom-0 left-0 w-full h-1/3 backdrop-blur-xl mask-fade-top-overlay" aria-hidden="true" />
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-t from-foreground/60 to-transparent" />
<div className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-4 p-6">
<div className="flex flex-col min-w-0">
<h3 className="text-2xl md:text-3xl font-medium leading-tight text-background">{item.title}</h3>
<p className="text-sm md:text-base leading-tight text-background/75">{item.description}</p>
</div>
<button
onClick={handleClick}
type="button"
aria-label={item.buttonHref ? `Navigate to ${item.buttonHref}` : "Action button"}
className="flex items-center justify-center shrink-0 h-8 aspect-square primary-button rounded cursor-pointer"
>
<Icon className="h-2/5 w-2/5 text-primary-cta-text" strokeWidth={1.5} />
</button>
</div>
</div>
);
};
const FeaturesMediaCarousel = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesMediaCarouselProps) => {
return (
<section aria-label="Features section" className="w-full py-20">
<div className="flex flex-col w-full gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<LoopCarousel>
{items.map((item) => (
<FeatureMediaCarouselCard key={item.title} item={item} />
))}
</LoopCarousel>
</div>
</section>
);
};
export default FeaturesMediaCarousel;

View File

@@ -0,0 +1,92 @@
import { motion } from "motion/react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import GridOrCarousel from "@/components/ui/GridOrCarousel";
type FeatureItem = {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
interface FeaturesTaggedCardsProps {
tag: string;
title: string;
description: string;
primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string };
items: FeatureItem[];
}
const FeaturesTaggedCards = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: FeaturesTaggedCardsProps) => {
return (
<section aria-label="Features section" className="py-20">
<div className="flex flex-col gap-8">
<div className="flex flex-col items-center w-content-width mx-auto gap-3 md:gap-2">
<span className="px-3 py-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h2"
className="text-6xl font-medium text-center text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="md:max-w-6/10 text-lg leading-tight text-center"
/>
{(primaryButton || secondaryButton) && (
<div className="flex flex-wrap justify-center gap-4 mt-1 md:mt-2">
{primaryButton && <Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />}
{secondaryButton && <Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />}
</div>
)}
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<GridOrCarousel carouselThreshold={3}>
{items.map((item) => (
<div key={item.title} className="flex flex-col h-full gap-4 group">
<div className="relative aspect-square rounded overflow-hidden">
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} className="transition-transform duration-500 ease-in-out group-hover:scale-105" />
<span className="absolute top-4 right-4 px-3 py-1 text-sm card rounded">{item.tag}</span>
</div>
<div className="flex flex-col flex-1 gap-2 p-6 card rounded">
<h3 className="text-xl md:text-2xl font-medium leading-tight">{item.title}</h3>
<p className="text-base leading-tight">{item.description}</p>
{(item.primaryButton || item.secondaryButton) && (
<div className="flex flex-wrap gap-4 mt-2">
{item.primaryButton && <Button text={item.primaryButton.text} href={item.primaryButton.href} variant="primary" />}
{item.secondaryButton && <Button text={item.secondaryButton.text} href={item.secondaryButton.href} variant="secondary" />}
</div>
)}
</div>
</div>
))}
</GridOrCarousel>
</motion.div>
</div>
</section>
);
};
export default FeaturesTaggedCards;

View File

@@ -0,0 +1,62 @@
import { motion } from "motion/react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type HeroBillboardProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroBillboard = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
}: HeroBillboardProps) => {
return (
<section aria-label="Hero section" className="pt-25 pb-20 md:py-30">
<div className="flex flex-col gap-10 md:gap-13 w-content-width mx-auto">
<div className="flex flex-col items-center gap-3 text-center">
<span className="px-3 py-1 mb-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h1"
className="text-6xl font-medium text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="text-base md:text-lg leading-tight text-balance"
/>
<div className="flex flex-wrap max-md:justify-center gap-4 mt-2">
<Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
className="w-full p-4 card rounded overflow-hidden"
>
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} className="aspect-square md:aspect-video" />
</motion.div>
</div>
</section>
);
};
export default HeroBillboard;

View File

@@ -0,0 +1,99 @@
import { motion } from "motion/react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import { cls } from "@/lib/utils";
interface HeroBillboardGalleryProps {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
items: ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never })[];
}
const HeroBillboardGallery = ({
tag,
title,
description,
primaryButton,
secondaryButton,
items,
}: HeroBillboardGalleryProps) => {
const marqueeItems = [...items, ...items];
const galleryStyles = [
"-rotate-6 z-10 -translate-y-5",
"rotate-6 z-20 translate-y-5 -ml-15",
"-rotate-6 z-30 -translate-y-5 -ml-15",
"rotate-6 z-40 translate-y-5 -ml-15",
"-rotate-6 z-50 -translate-y-5 -ml-15",
];
return (
<section aria-label="Hero section" className="flex items-center h-fit md:h-svh pt-25 pb-20 md:py-0">
<div className="flex flex-col items-center gap-10 md:gap-15 w-full md:w-content-width mx-auto">
<div className="flex flex-col items-center gap-2 w-content-width mx-auto text-center">
<span className="px-3 py-1 mb-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="slide-up"
tag="h1"
className="text-6xl font-medium text-balance"
/>
<TextAnimation
text={description}
variant="slide-up"
tag="p"
className="text-base md:text-lg leading-tight text-balance"
/>
<div className="flex flex-wrap justify-center gap-4 mt-2">
<Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
className="block md:hidden w-full overflow-hidden mask-padding-x"
>
<div className="flex w-max animate-marquee-horizontal">
{marqueeItems.map((item, index) => (
<div key={index} className="shrink-0 w-[50vw] mr-4 aspect-4/5 p-2 card rounded overflow-hidden">
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} />
</div>
))}
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
className="hidden md:flex justify-center items-center w-full"
>
<div className="flex items-center justify-center">
{items.map((item, index) => (
<div
key={index}
className={cls(
"relative w-[23%] aspect-4/5 p-2 card rounded overflow-hidden shadow-lg transition-transform duration-500 ease-out hover:scale-110",
galleryStyles[index]
)}
>
<ImageOrVideo imageSrc={item.imageSrc} videoSrc={item.videoSrc} />
</div>
))}
</div>
</motion.div>
</div>
</section>
);
};
export default HeroBillboardGallery;

View File

@@ -0,0 +1,64 @@
import { motion } from "motion/react";
import Button from "@/components/ui/Button";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
type HeroSplitProps = {
tag: string;
title: string;
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroSplit = ({
tag,
title,
description,
primaryButton,
secondaryButton,
imageSrc,
videoSrc,
}: HeroSplitProps) => {
return (
<section aria-label="Hero section" className="flex items-center h-fit md:h-svh pt-25 pb-20 md:py-0">
<div className="flex flex-col md:flex-row items-center gap-10 md:gap-20 w-content-width mx-auto">
<div className="w-full md:w-1/2">
<div className="flex flex-col items-center md:items-start gap-3 md:gap-4">
<span className="px-3 py-1 mb-1 text-sm card rounded">{tag}</span>
<TextAnimation
text={title}
variant="fade"
tag="h1"
className="text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance"
/>
<TextAnimation
text={description}
variant="fade"
tag="p"
className="max-w-8/10 text-lg md:text-xl leading-tight text-center md:text-left"
/>
<div className="flex flex-wrap max-md:justify-center gap-4 mt-2">
<Button text={primaryButton.text} href={primaryButton.href} variant="primary" animate />
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary" animate delay={0.1} />
</div>
</div>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
className="w-full md:w-1/2 h-100 md:h-[65vh] md:max-h-[75svh] p-4 card rounded overflow-hidden"
>
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} />
</motion.div>
</div>
</section>
);
};
export default HeroSplit;

View File

@@ -0,0 +1,43 @@
import { Link } from "react-router-dom";
import { motion } from "motion/react";
import { cls } from "@/lib/utils";
import { useButtonClick } from "@/hooks/useButtonClick";
interface ButtonProps {
text: string;
variant?: "primary" | "secondary";
href?: string;
onClick?: () => void;
animate?: boolean;
delay?: number;
className?: string;
}
const Button = ({ text, variant = "primary", href, onClick, animate = false, delay = 0, className = "" }: ButtonProps) => {
const handleClick = useButtonClick(href, onClick);
const classes = cls(
"flex items-center justify-center h-9 px-6 text-sm rounded cursor-pointer",
variant === "primary" ? "primary-button text-primary-cta-text" : "secondary-button text-secondary-cta-text",
className
);
const button = href?.startsWith("/")
? <Link to={href} className={classes}>{text}</Link>
: <button onClick={handleClick} className={classes}>{text}</button>;
if (!animate) return button;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-15%" }}
transition={{ duration: 0.6, delay, ease: "easeOut" }}
>
{button}
</motion.div>
);
};
export default Button;

View File

@@ -0,0 +1,64 @@
import { Children, type ReactNode } from "react";
import useEmblaCarousel from "embla-carousel-react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cls } from "@/lib/utils";
import { useCarouselControls } from "@/hooks/useCarouselControls";
interface GridOrCarouselProps {
children: ReactNode;
carouselThreshold?: 2 | 3 | 4;
}
const GridOrCarousel = ({ children, carouselThreshold = 3 }: GridOrCarouselProps) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ dragFree: true });
const { prevDisabled, nextDisabled, scrollPrev, scrollNext, scrollProgress } = useCarouselControls(emblaApi);
const items = Children.toArray(children);
const count = items.length;
if (count <= carouselThreshold) {
return (
<div className={cls(
"grid grid-cols-1 gap-4 mx-auto w-content-width",
count === 2 && "md:grid-cols-2",
count === 3 && "md:grid-cols-3",
count === 4 && "md:grid-cols-4"
)}>
{children}
</div>
);
}
return (
<div className="flex flex-col gap-6 w-full">
<div ref={emblaRef} className="overflow-hidden w-full cursor-grab">
<div className="flex gap-4">
<div className="shrink-0 w-carousel-padding" />
{items.map((child, i) => (
<div key={i} className={cls("shrink-0", carouselThreshold === 2 ? "w-carousel-item-2" : carouselThreshold === 3 ? "w-carousel-item-3" : "w-carousel-item-4")}>{child}</div>
))}
<div className="shrink-0 w-carousel-padding" />
</div>
</div>
<div className="flex w-full">
<div className="shrink-0 w-carousel-padding-controls" />
<div className="flex justify-between items-center w-full">
<div className="relative h-2 w-1/2 card rounded overflow-hidden">
<div className="absolute top-0 bottom-0 -left-full w-full primary-button rounded" style={{ transform: `translate3d(${scrollProgress}%,0px,0px)` }} />
</div>
<div className="flex items-center gap-3">
<button onClick={scrollPrev} disabled={prevDisabled} type="button" aria-label="Previous" className="flex items-center justify-center h-8 aspect-square secondary-button rounded cursor-pointer disabled:opacity-50">
<ChevronLeft className="h-2/5 aspect-square text-secondary-cta-text" />
</button>
<button onClick={scrollNext} disabled={nextDisabled} type="button" aria-label="Next" className="flex items-center justify-center h-8 aspect-square secondary-button rounded cursor-pointer disabled:opacity-50">
<ChevronRight className="h-2/5 aspect-square text-secondary-cta-text" />
</button>
</div>
</div>
<div className="shrink-0 w-carousel-padding-controls" />
</div>
</div>
);
};
export default GridOrCarousel;

View File

@@ -0,0 +1,41 @@
import { cls } from "@/lib/utils";
interface ImageOrVideoProps {
imageSrc?: string;
videoSrc?: string;
className?: string;
}
const ImageOrVideo = ({
imageSrc,
videoSrc,
className = "",
}: ImageOrVideoProps) => {
if (videoSrc) {
return (
<video
src={videoSrc}
aria-label={videoSrc}
className={cls("w-full h-full min-h-0 object-cover rounded", className)}
autoPlay
loop
muted
playsInline
/>
);
}
if (imageSrc) {
return (
<img
src={imageSrc}
alt={imageSrc}
className={cls("w-full h-full min-h-0 object-cover rounded", className)}
/>
);
}
return null;
};
export default ImageOrVideo;

View File

@@ -0,0 +1,76 @@
import { Children, useCallback, useEffect, useState, type ReactNode } from "react";
import useEmblaCarousel from "embla-carousel-react";
import type { EmblaCarouselType } from "embla-carousel";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cls } from "@/lib/utils";
interface LoopCarouselProps {
children: ReactNode;
}
const LoopCarousel = ({ children }: LoopCarouselProps) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true, align: "center" });
const [selectedIndex, setSelectedIndex] = useState(0);
const items = Children.toArray(children);
const onSelect = useCallback((api: EmblaCarouselType) => {
setSelectedIndex(api.selectedScrollSnap());
}, []);
const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
useEffect(() => {
if (!emblaApi) return;
onSelect(emblaApi);
emblaApi.on("select", onSelect).on("reInit", onSelect);
return () => {
emblaApi.off("select", onSelect).off("reInit", onSelect);
};
}, [emblaApi, onSelect]);
return (
<div className="relative w-full md:w-content-width mx-auto">
<div ref={emblaRef} className="overflow-hidden w-full mask-fade-x">
<div className="flex w-full">
{items.map((child, index) => (
<div key={index} className="shrink-0 w-content-width md:w-[clamp(18rem,50vw,48rem)] mr-3 md:mr-6">
<div
className={cls(
"transition-all duration-500 ease-out",
selectedIndex === index ? "opacity-100 scale-100" : "opacity-70 scale-90"
)}
>
{child}
</div>
</div>
))}
</div>
</div>
<div className="absolute inset-y-0 left-0 right-0 flex items-center justify-between w-content-width mx-auto pointer-events-none">
<button
onClick={scrollPrev}
type="button"
aria-label="Previous slide"
className="flex items-center justify-center h-8 aspect-square primary-button rounded cursor-pointer pointer-events-auto"
>
<ChevronLeft className="h-2/5 aspect-square text-primary-cta-text" />
</button>
<button
onClick={scrollNext}
type="button"
aria-label="Next slide"
className="flex items-center justify-center h-8 aspect-square primary-button rounded cursor-pointer pointer-events-auto"
>
<ChevronRight className="h-2/5 aspect-square text-primary-cta-text" />
</button>
</div>
</div>
);
};
export default LoopCarousel;

View File

@@ -0,0 +1,109 @@
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react";
import { Link } from "react-router-dom";
import { Plus, ArrowRight } from "lucide-react";
import { cls } from "@/lib/utils";
import Button from "@/components/ui/Button";
interface NavbarCenteredProps {
logo: string;
navItems: { name: string; href: string }[];
ctaButton: { text: string; href: string };
}
const NavbarCentered = ({ logo, navItems, ctaButton }: NavbarCenteredProps) => {
const [isScrolled, setIsScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => setIsScrolled(window.scrollY > 50);
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<>
<nav
className={cls(
"fixed z-1000 top-0 left-0 w-full transition-all duration-500 ease-in-out",
isScrolled ? "h-15 bg-background/80 backdrop-blur-sm" : "h-20 bg-background/0 backdrop-blur-0"
)}
>
<div className="relative flex items-center justify-between h-full w-content-width mx-auto">
<Link to="/" className="text-xl font-medium text-foreground">{logo}</Link>
<div className="hidden md:flex absolute left-1/2 items-center gap-6 -translate-x-1/2">
{navItems.map((item) => (
<Link key={item.name} to={item.href} className="text-base text-foreground hover:opacity-70 transition-opacity">
{item.name}
</Link>
))}
</div>
<div className="hidden md:block">
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" />
</div>
<button
className="flex md:hidden items-center justify-center shrink-0 h-8 w-8 bg-foreground rounded cursor-pointer"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
aria-expanded={menuOpen}
>
<Plus
className={cls("w-1/2 h-1/2 text-background transition-transform duration-300", menuOpen ? "rotate-45" : "rotate-0")}
strokeWidth={1.5}
/>
</button>
</div>
</nav>
<AnimatePresence>
{menuOpen && (
<motion.div
initial={{ y: "-135%" }}
animate={{ y: 0 }}
exit={{ y: "-135%" }}
transition={{ type: "spring", damping: 26, stiffness: 170 }}
className="md:hidden fixed z-1000 top-3 left-3 right-3 p-6 card rounded"
>
<div className="flex items-center justify-between mb-6">
<p className="text-xl text-foreground">Menu</p>
<button
className="flex items-center justify-center shrink-0 h-8 w-8 bg-foreground rounded cursor-pointer"
onClick={() => setMenuOpen(false)}
aria-label="Close menu"
>
<Plus className="w-1/2 h-1/2 text-background rotate-45" strokeWidth={1.5} />
</button>
</div>
<div className="flex flex-col gap-4">
{navItems.map((item, index) => (
<div key={item.name}>
<Link
to={item.href}
onClick={() => setMenuOpen(false)}
className="flex items-center justify-between py-2 text-base font-medium text-foreground"
>
{item.name}
<ArrowRight className="h-4 w-4 text-foreground" strokeWidth={1.5} />
</Link>
{index < navItems.length - 1 && (
<div className="h-px bg-linear-to-r from-transparent via-foreground/20 to-transparent" />
)}
</div>
))}
</div>
<div className="mt-6">
<Button text={ctaButton.text} href={ctaButton.href} variant="primary" className="w-full" />
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
};
export default NavbarCentered;

View File

@@ -0,0 +1,60 @@
import { motion } from "motion/react";
type Variant = "slide-up" | "fade-blur" | "fade";
interface TextAnimationProps {
text: string;
variant: Variant;
tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "div";
className?: string;
}
const VARIANTS = {
"slide-up": {
hidden: { opacity: 0, y: "50%" },
visible: { opacity: 1, y: 0 },
},
"fade-blur": {
hidden: { opacity: 0, filter: "blur(10px)" },
visible: { opacity: 1, filter: "blur(0px)" },
},
"fade": {
hidden: { opacity: 0 },
visible: { opacity: 1 },
},
};
const EASING: Record<Variant, [number, number, number, number]> = {
"slide-up": [0.25, 0.46, 0.45, 0.94],
"fade-blur": [0.45, 0, 0.55, 1],
"fade": [0.45, 0, 0.55, 1],
};
const TextAnimation = ({ text, variant, tag = "p", className = "" }: TextAnimationProps) => {
const Tag = motion[tag] as typeof motion.p;
const words = text.split(" ");
return (
<Tag
className={className}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-20%" }}
transition={{ staggerChildren: 0.04 }}
>
{words.map((word, i) => (
<motion.span
key={i}
className="inline-block"
variants={VARIANTS[variant]}
transition={{ duration: 0.6, ease: EASING[variant] }}
>
{word}
{i < words.length - 1 && "\u00A0"}
</motion.span>
))}
</Tag>
);
};
export default TextAnimation;

View File

@@ -0,0 +1,51 @@
import { useNavigate, useLocation } from "react-router-dom";
export const useButtonClick = (href?: string, onClick?: () => void) => {
const navigate = useNavigate();
const location = useLocation();
const scrollToElement = (sectionId: string, delay: number = 100) => {
setTimeout(() => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
}, delay);
};
const handleClick = () => {
if (href) {
const isExternalLink = /^(https?:\/\/|www\.)/.test(href);
if (isExternalLink) {
window.open(
href.startsWith("www.") ? `https://${href}` : href,
"_blank",
"noopener,noreferrer"
);
} else if (href.startsWith("/")) {
const [path, hash] = href.split("#");
if (path !== location.pathname) {
navigate(path);
if (hash) {
setTimeout(() => {
window.location.hash = hash;
scrollToElement(hash, 100);
}, 100);
}
} else if (hash) {
window.location.hash = hash;
scrollToElement(hash, 50);
}
} else if (href.startsWith("#")) {
scrollToElement(href.slice(1), 50);
} else {
scrollToElement(href, 50);
}
}
onClick?.();
};
return handleClick;
};

View File

@@ -0,0 +1,45 @@
import { useCallback, useEffect, useState } from "react";
import type { EmblaCarouselType } from "embla-carousel";
export const useCarouselControls = (emblaApi: EmblaCarouselType | undefined) => {
const [prevDisabled, setPrevDisabled] = useState(true);
const [nextDisabled, setNextDisabled] = useState(true);
const [scrollProgress, setScrollProgress] = useState(0);
const scrollPrev = useCallback(() => {
if (!emblaApi) return;
emblaApi.scrollPrev();
}, [emblaApi]);
const scrollNext = useCallback(() => {
if (!emblaApi) return;
emblaApi.scrollNext();
}, [emblaApi]);
const onSelect = useCallback((api: EmblaCarouselType) => {
setPrevDisabled(!api.canScrollPrev());
setNextDisabled(!api.canScrollNext());
}, []);
const onScroll = useCallback((api: EmblaCarouselType) => {
const progress = Math.max(0, Math.min(1, api.scrollProgress()));
setScrollProgress(progress * 100);
}, []);
useEffect(() => {
if (!emblaApi) return;
onSelect(emblaApi);
onScroll(emblaApi);
emblaApi.on("reInit", onSelect).on("select", onSelect);
emblaApi.on("reInit", onScroll).on("scroll", onScroll);
return () => {
emblaApi.off("reInit", onSelect).off("select", onSelect);
emblaApi.off("reInit", onScroll).off("scroll", onScroll);
};
}, [emblaApi, onSelect, onScroll]);
return { prevDisabled, nextDisabled, scrollPrev, scrollNext, scrollProgress };
};

184
src/index.css Normal file
View File

@@ -0,0 +1,184 @@
@import "tailwindcss";
@import "./styles/masks.css";
@import "./styles/animations.css";
:root {
/* @colorThemes/lightTheme/grayBlueAccent */
--background: #f6f0e9;
--card: #efe7dd;
--foreground: #2b180a;
--primary-cta: #2b180a;
--primary-cta-text: #f6f0e9;
--secondary-cta: #efe7dd;
--secondary-cta-text: #2b180a;
--accent: #94877c;
--background-accent: #afa094;
/* @layout/border-radius/rounded */
--radius: 0.5rem;
/* @layout/content-width/medium */
--width-content-width: clamp(40rem, 80vw, 100rem);
/* @utilities/masks */
--vw-1_5: 1.5vw;
--width-x-padding-mask-fade: 5vw;
/* @layout/carousel */
--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-item-2: calc(var(--width-content-width) / 2 - var(--vw-1_5) / 2);
--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);
/* @typography/text-sizing/medium */
--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);
}
/* @typography/text-sizing/medium (mobile) */
@media (max-width: 768px) {
:root {
--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-content-width: 80vw;
--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-item-2: var(--width-content-width);
--width-carousel-item-3: var(--width-content-width);
--width-carousel-item-4: var(--width-content-width);
}
}
@theme inline {
/* Colors */
--color-background: var(--background);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-primary-cta: var(--primary-cta);
--color-primary-cta-text: var(--primary-cta-text);
--color-secondary-cta: var(--secondary-cta);
--color-secondary-cta-text: var(--secondary-cta-text);
--color-accent: var(--accent);
--color-background-accent: var(--background-accent);
/* Fonts */
--font-sans: "Inter", sans-serif;
--font-mono: monospace;
/* Border Radius */
--radius: var(--radius);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
/* Width */
--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-item-2: var(--width-carousel-item-2);
--width-carousel-item-3: var(--width-carousel-item-3);
--width-carousel-item-4: var(--width-carousel-item-4);
/* Typography */
--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);
}
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 1) rgba(255, 255, 255, 0);
}
html {
overscroll-behavior: none;
overscroll-behavior-y: none;
}
body {
margin: 0;
background-color: var(--background);
color: var(--foreground);
font-family: "Inter", sans-serif;
position: relative;
min-height: 100vh;
overscroll-behavior: none;
overscroll-behavior-y: none;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Inter", sans-serif;
}
/* WEBILD_CARD_STYLE */
/* @cards/glass-depth */
.card {
background: color-mix(in srgb, var(--color-card) 80%, transparent);
backdrop-filter: blur(14px);
box-shadow:
inset 0 0 20px 0 color-mix(in srgb, var(--color-accent) 7.5%, transparent);
border: 1px solid color-mix(in srgb, var(--color-accent) 7.5%, transparent);
}
/* WEBILD_PRIMARY_BUTTON */
/* @primaryButtons/neon-outline */
.primary-button {
background: var(--color-primary-cta);
box-shadow: 0 0 5px color-mix(in srgb, var(--color-accent) 50%, transparent), 0 0 15px color-mix(in srgb, var(--color-accent) 30%, transparent), 0 0 30px color-mix(in srgb, var(--color-accent) 15%, transparent), inset 0 0 8px color-mix(in srgb, var(--color-accent) 10%, transparent);
}
/* WEBILD_SECONDARY_BUTTON */
/* @secondaryButtons/layered */
.secondary-button {
background:
linear-gradient(color-mix(in srgb, var(--color-accent) 5%, transparent) 0%, transparent 59.26%),
linear-gradient(var(--color-secondary-cta), var(--color-secondary-cta)),
linear-gradient(var(--color-secondary-cta), var(--color-secondary-cta)),
linear-gradient(color-mix(in srgb, var(--color-accent) 5%, transparent) 0%, transparent 59.26%),
linear-gradient(color-mix(in srgb, var(--color-secondary-cta) 60%, transparent), color-mix(in srgb, var(--color-secondary-cta) 60%, transparent)),
var(--color-secondary-cta);
box-shadow:
2.10837px 3.16256px 9.48767px color-mix(in srgb, var(--color-accent) 10%, transparent);
border: 1px solid var(--color-secondary-cta);
}

10
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,10 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: (string | undefined | null | false)[]) {
return inputs.filter(Boolean).join(" ");
}
export function cls(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

158
src/styles/animations.css Normal file
View File

@@ -0,0 +1,158 @@
/* @utilities/animations */
@keyframes pulsate {
0% {
box-shadow: 0 0 0 0 var(--accent);
transform: scale(0.9);
}
50% {
transform: scale(1);
}
100% {
box-shadow: 0 0 20px 10px transparent;
transform: scale(0.9);
}
}
@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);
}
}
@keyframes marquee-vertical {
from {
transform: translateY(0);
}
to {
transform: translateY(-50%);
}
}
@keyframes marquee-vertical-reverse {
from {
transform: translateY(-50%);
}
to {
transform: translateY(0);
}
}
@keyframes marquee-horizontal {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
@keyframes marquee-horizontal-reverse {
from {
transform: translateX(-50%);
}
to {
transform: translateX(0);
}
}
@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;
}
}
/* Animation classes */
.animate-pulsate {
animation: pulsate 1.5s infinite;
}
.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;
}
.animate-spin-slow {
animation: spin-slow 15s linear infinite;
}
.animate-spin-reverse {
animation: spin-reverse 10s linear infinite;
}
.animate-marquee-vertical {
animation: marquee-vertical 40s linear infinite;
}
.animate-marquee-vertical-reverse {
animation: marquee-vertical-reverse 40s linear infinite;
}
.animate-marquee-horizontal {
animation: marquee-horizontal 15s linear infinite;
}
.animate-marquee-horizontal-reverse {
animation: marquee-horizontal-reverse 15s linear infinite;
}

61
src/styles/masks.css Normal file
View File

@@ -0,0 +1,61 @@
/* @utilities/masks */
.mask-fade-x {
mask-image: linear-gradient(to right, transparent 0%, black 5%, black 95%, transparent 100%);
}
.mask-padding-x {
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 {
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-y-medium {
mask-image: linear-gradient(
to bottom,
transparent 0%,
black 20%,
black 80%,
transparent 100%
);
}
.mask-fade-bottom-large {
mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 75%, transparent 100%);
}
.mask-fade-bottom-long {
mask-image: linear-gradient(to bottom, black 0%, black 5%, transparent 100%);
}
.mask-fade-top-long {
mask-image: linear-gradient(to top, black 0%, black 5%, transparent 100%);
}
.mask-fade-xy {
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-composite: intersect;
}
.mask-fade-top-overlay {
mask-image: linear-gradient(to bottom, transparent, black 60%);
}

27
tsconfig.app.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

5
vercel.json Normal file
View File

@@ -0,0 +1,5 @@
{
"installCommand": "npm ci",
"buildCommand": "npm run build",
"outputDirectory": ".next"
}

13
vite.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})