Merge pull request #1386 from PIXELUPLABS/pixelup-dev
GSAP optimisation
This commit is contained in:
@@ -332,3 +332,10 @@ body {
|
||||
color: #b08aff;
|
||||
text-decoration-color: #b08aff;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
HERO REVEAL — desktop only (lg+), driven by GSAP
|
||||
On mobile, GSAP is skipped and hero elements are immediately visible.
|
||||
The lg:opacity-0 utility class on .hero-reveal / .hero-cta elements
|
||||
ensures they start hidden on desktop where GSAP animates them in.
|
||||
========================================================================== */
|
||||
|
||||
@@ -7,11 +7,13 @@ import "./globals.css";
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const url = "https://useautumn.com";
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@autumn/website",
|
||||
"dependencies": {
|
||||
"@gsap/react": "^2.1.2",
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@next/mdx": "^16.2.4",
|
||||
"@types/react": "^19.2.14",
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"framer-motion": "^12.38.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"gsap": "^3.15.0",
|
||||
"lottie-react": "^2.4.1",
|
||||
"lottie-web": "^5.13.0",
|
||||
"matter-js": "^0.20.0",
|
||||
"motion": "^12.38.0",
|
||||
"next": "16.2.4",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"ngrok": "^5.0.0-beta.2",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
@@ -159,6 +161,8 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@mdx-js/loader": ["@mdx-js/loader@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" }, "peerDependencies": { "webpack": ">=5" }, "optionalPeers": ["webpack"] }, "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ=="],
|
||||
|
||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
||||
|
||||
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
|
||||
@@ -169,6 +173,8 @@
|
||||
|
||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.1", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA=="],
|
||||
|
||||
"@next/mdx": ["@next/mdx@16.2.4", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-e/3bgla+/oF3vDlndI0eFPa0bnP47HPVA0InsAJi7Jr3DwV8WpEGuOcm/3PdI5/93FfNiBhMVeVHZpm1sFlmJw=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="],
|
||||
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="],
|
||||
@@ -805,10 +811,10 @@
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"matter-js": ["matter-js@0.20.0", "", {}, "sha512-iC9fYR7zVT3HppNnsFsp9XOoQdQN2tUyfaKg4CHLH8bN+j6GT4Gw7IH2rP0tflAebrHFw730RR3DkVSZRX8hwA=="],
|
||||
|
||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
|
||||
|
||||
"mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="],
|
||||
|
||||
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
|
||||
|
||||
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||
@@ -831,6 +837,8 @@
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
|
||||
"micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="],
|
||||
|
||||
"micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
|
||||
|
||||
"micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
|
||||
@@ -907,8 +915,6 @@
|
||||
|
||||
"next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": "dist/bin/next" }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="],
|
||||
|
||||
"next-mdx-remote": ["next-mdx-remote@6.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^4.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ=="],
|
||||
|
||||
"ngrok": ["ngrok@5.0.0-beta.2", "", { "dependencies": { "extract-zip": "^2.0.1", "got": "^11.8.5", "lodash.clonedeep": "^4.5.0", "uuid": "^7.0.0 || ^8.0.0", "yaml": "^2.2.2" }, "optionalDependencies": { "hpagent": "^0.1.2" }, "bin": "bin/ngrok" }, "sha512-UzsyGiJ4yTTQLCQD11k1DQaMwq2/SsztBg2b34zAqcyjS25qjDpogMKPaCKHwe/APRTHeel3iDXcVctk5CNaCQ=="],
|
||||
|
||||
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
|
||||
@@ -1005,6 +1011,8 @@
|
||||
|
||||
"rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
|
||||
|
||||
"remark-frontmatter": ["remark-frontmatter@5.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0", "unified": "^11.0.0" } }, "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ=="],
|
||||
|
||||
"remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
@@ -1137,8 +1145,6 @@
|
||||
|
||||
"unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
|
||||
|
||||
"unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
@@ -1157,8 +1163,6 @@
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-matter": ["vfile-matter@5.0.1", "", { "dependencies": { "vfile": "^6.0.0", "yaml": "^2.0.0" } }, "sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
@@ -1217,6 +1221,10 @@
|
||||
|
||||
"is-bun-module/semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"micromark-extension-frontmatter/fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"use client";
|
||||
import gsap from "gsap";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
||||
import type { PixelHoverHandle, PixelIconComponent } from "@/lib/types";
|
||||
import { getGsap } from "@/lib/lazyGsap";
|
||||
|
||||
type GSAPTimeline = {
|
||||
play: () => GSAPTimeline;
|
||||
reverse: () => GSAPTimeline;
|
||||
kill: () => void;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: structural shim for GSAP's overloaded .to() signature
|
||||
to: (...args: any[]) => GSAPTimeline;
|
||||
};
|
||||
|
||||
export const DashboardIconPixel = forwardRef<
|
||||
PixelHoverHandle,
|
||||
{ Icon: PixelIconComponent }
|
||||
>(function DashboardIconPixel({ Icon }, ref) {
|
||||
const iconRef = useRef<SVGSVGElement | null>(null);
|
||||
const tlRef = useRef<gsap.core.Timeline | null>(null);
|
||||
const tlRef = useRef<GSAPTimeline | null>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
restart: () => tlRef.current?.play(),
|
||||
@@ -16,47 +24,40 @@ export const DashboardIconPixel = forwardRef<
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const pixelEls =
|
||||
iconRef.current?.querySelectorAll<SVGGraphicsElement>(".icon-pixel-path");
|
||||
if (!pixelEls?.length) return;
|
||||
const el = iconRef.current;
|
||||
if (!el) return;
|
||||
let cancelled = false;
|
||||
|
||||
// Sort pixels by visual position: bottom-left → top-right diagonal
|
||||
const pixels = Array.from(pixelEls).sort((a, b) => {
|
||||
const aBox = a.getBBox();
|
||||
const bBox = b.getBBox();
|
||||
return (
|
||||
aBox.x +
|
||||
aBox.width / 2 -
|
||||
(aBox.y + aBox.height / 2) -
|
||||
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
|
||||
);
|
||||
});
|
||||
getGsap().then((gsap) => {
|
||||
if (cancelled) return;
|
||||
const pixelEls = el.querySelectorAll<SVGGraphicsElement>(".icon-pixel-path");
|
||||
if (!pixelEls.length) return;
|
||||
|
||||
gsap.set(pixels, {
|
||||
opacity: 0.15,
|
||||
scale: 0.8,
|
||||
transformOrigin: "left bottom",
|
||||
fill: "currentColor",
|
||||
});
|
||||
|
||||
tlRef.current = gsap.timeline({ paused: true });
|
||||
|
||||
tlRef.current
|
||||
.to(pixels, {
|
||||
opacity: 1,
|
||||
scale: 1.15,
|
||||
fill: "#FFFFFF",
|
||||
duration: 0.2,
|
||||
stagger: 0.01,
|
||||
ease: "power2.out",
|
||||
})
|
||||
.to(pixels, {
|
||||
scale: 1,
|
||||
duration: 0.01,
|
||||
ease: "back.out(3)",
|
||||
// Sort pixels by visual position: bottom-left → top-right diagonal
|
||||
const pixels = Array.from(pixelEls).sort((a, b) => {
|
||||
const aBox = a.getBBox();
|
||||
const bBox = b.getBBox();
|
||||
return (
|
||||
aBox.x + aBox.width / 2 - (aBox.y + aBox.height / 2) -
|
||||
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
|
||||
);
|
||||
});
|
||||
|
||||
gsap.set(pixels, {
|
||||
opacity: 0.15,
|
||||
scale: 0.8,
|
||||
transformOrigin: "left bottom",
|
||||
fill: "currentColor",
|
||||
});
|
||||
|
||||
tlRef.current = gsap.timeline({ paused: true });
|
||||
tlRef.current!
|
||||
.to(pixels, { opacity: 1, scale: 1.15, fill: "#FFFFFF", duration: 0.2, stagger: 0.01, ease: "power2.out" })
|
||||
.to(pixels, { scale: 1, duration: 0.01, ease: "back.out(3)" });
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
tlRef.current?.kill();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
|
||||
import { motion, useMotionValue, useSpring, useTransform } from "motion/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { LayoutProps } from "@/lib/types";
|
||||
import AnimatedFooterImage from "./animated-footer-image";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useState } from "react";
|
||||
import { AnimatedPlusMinus, faqData } from "@/app/constant";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useGSAP } from "@gsap/react";
|
||||
import gsap from "gsap";
|
||||
import { motion } from "motion/react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { CTALines, IconCTADocs, IconCTAStart } from "@/app/constant";
|
||||
import { getGsap } from "@/lib/lazyGsap";
|
||||
|
||||
// `AutumnConfig` pulls in `react-syntax-highlighter` + `highlight.js` (~100KB
|
||||
// gzipped + meaningful parse cost on mobile). It only renders on `xl+`
|
||||
@@ -88,88 +87,51 @@ export default function Hero() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useGSAP(
|
||||
() => {
|
||||
gsap.set(".hero-root", { opacity: 0 });
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
gsap.set(".hero-bg", {
|
||||
opacity: 0,
|
||||
filter: "blur(6px) brightness(1)",
|
||||
scale: 0.97,
|
||||
transformOrigin: "center top",
|
||||
});
|
||||
let ctx: { revert: () => void } | null = null;
|
||||
let cancelled = false;
|
||||
|
||||
gsap.set(".hero-reveal", {
|
||||
opacity: 0,
|
||||
y: 25,
|
||||
filter: "blur(12px)",
|
||||
scale: 0.96,
|
||||
transformOrigin: "center bottom",
|
||||
});
|
||||
// Mobile: CSS animations in globals.css handle the hero reveal.
|
||||
// Guard here so mobile never initiates the ~60KB GSAP dynamic import.
|
||||
if (window.innerWidth < 1024) return;
|
||||
|
||||
gsap.set(".hero-cta", { opacity: 0, scale: 0.95 });
|
||||
getGsap().then((gsap) => {
|
||||
if (cancelled) return;
|
||||
ctx = gsap.context(() => {
|
||||
gsap.set(".hero-reveal", {
|
||||
opacity: 0,
|
||||
y: 25,
|
||||
scale: 0.96,
|
||||
transformOrigin: "center bottom",
|
||||
});
|
||||
gsap.set(".hero-cta", { opacity: 0, scale: 0.95 });
|
||||
|
||||
const tl = gsap.timeline({
|
||||
defaults: { overwrite: "auto" },
|
||||
});
|
||||
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
|
||||
|
||||
tl.to(".hero-root", { opacity: 1, duration: 0.3, ease: "none" })
|
||||
// hero-bg is intentionally NOT hidden — it is the LCP element and
|
||||
// must be visible from first paint. The brightness flash still runs.
|
||||
tl.to(".hero-bg", { filter: "brightness(1.6)", duration: 0.125, ease: "power2.in" })
|
||||
.to(".hero-bg", { filter: "brightness(1)", duration: 0.125, ease: "power2.out" })
|
||||
.to(".hero-reveal", { opacity: 1, y: 0, scale: 1, duration: 1.1, stagger: 0.1, ease: "power3.out" }, "-=0.2")
|
||||
.to(".hero-cta", { opacity: 1, scale: 1, duration: 0.3, stagger: 0.06, ease: "back.out(1.5)" }, "-=0.1");
|
||||
}, container);
|
||||
});
|
||||
|
||||
.to(".hero-bg", {
|
||||
opacity: 1,
|
||||
filter: "blur(0px) brightness(1)",
|
||||
scale: 1,
|
||||
duration: 0.4,
|
||||
ease: "power2.out",
|
||||
})
|
||||
|
||||
.to(".hero-bg", {
|
||||
filter: "blur(0px) brightness(1.6)",
|
||||
duration: 0.125,
|
||||
ease: "power2.in",
|
||||
})
|
||||
|
||||
.to(".hero-bg", {
|
||||
filter: "blur(0px) brightness(1)",
|
||||
duration: 0.125,
|
||||
ease: "power2.out",
|
||||
})
|
||||
|
||||
.to(
|
||||
".hero-reveal",
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
filter: "blur(0px)",
|
||||
scale: 1,
|
||||
duration: 1.1,
|
||||
stagger: 0.1,
|
||||
ease: "power3.out",
|
||||
},
|
||||
"-=0.2",
|
||||
)
|
||||
|
||||
.to(
|
||||
".hero-cta",
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.3,
|
||||
stagger: 0.06,
|
||||
ease: "back.out(1.5)",
|
||||
},
|
||||
"-=0.1",
|
||||
);
|
||||
},
|
||||
{ scope: containerRef },
|
||||
);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
ctx?.revert();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<div className="relative hero-root opacity-0 flex flex-col items-stretch pb-0 md:pb-12 mb-0 bg-[#0F0F0F]">
|
||||
<div className="relative hero-root flex flex-col items-stretch pb-0 md:pb-12 mb-0 bg-[#0F0F0F]">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-col gap-6 px-4 xl:px-22.75 py-8 bg-[#0F0F0F] mt-26">
|
||||
<h4 className="hero-reveal relative uppercase font-mono tracking-[-2%] text-[12px] md:text-sm leading-sm text-white md:text-[#FFFFFF99] bg-[#2c2c2d] w-fit p-2 min-h-[30px] md:min-h-[36px] flex items-center">
|
||||
<h4 className="hero-reveal lg:opacity-0 relative uppercase font-mono tracking-[-2%] text-[12px] md:text-sm leading-sm text-white md:text-[#FFFFFF99] bg-[#2c2c2d] w-fit p-2 min-h-[30px] md:min-h-[36px] flex items-center">
|
||||
<span className="invisible select-none" aria-hidden="true">
|
||||
{BADGE_TEXT}
|
||||
</span>
|
||||
@@ -178,13 +140,13 @@ export default function Hero() {
|
||||
</span>
|
||||
</h4>
|
||||
<div className="flex flex-col gap-6 w-full px-0 lg:px-0">
|
||||
<h1 className="hero-reveal text-[44px] md:text-[56px] w-full max-w-sm sm:max-w-[480px] md:max-w-xl leading-[44px] tracking-[-5%] md:leading-14 font-sans">
|
||||
<h1 className="hero-reveal lg:opacity-0 text-[44px] md:text-[56px] w-full max-w-sm sm:max-w-[480px] md:max-w-xl leading-[44px] tracking-[-5%] md:leading-14 font-sans">
|
||||
<span className="text-[#FFFFFF99] font-normal">
|
||||
Drop-in credits and billing for
|
||||
</span>{" "}
|
||||
<span className="text-white block md:inline">AI agents</span>
|
||||
</h1>
|
||||
<p className="hero-reveal tracking-[-2%] w-full max-w-xs sm:max-w-[480px] md:max-w-xl text-[#FFFFFF99] md:text-[16px] text-[14px] font-light leading-5 font-sans">
|
||||
<p className="hero-reveal lg:opacity-0 tracking-[-2%] w-full max-w-xs sm:max-w-[480px] md:max-w-xl text-[#FFFFFF99] md:text-[16px] text-[14px] font-light leading-5 font-sans">
|
||||
Stop rebuilding usage limits, credit ledgers and payment logic.{" "}
|
||||
<span className="text-white font-light">
|
||||
Autumn is the customer database
|
||||
@@ -201,7 +163,7 @@ export default function Hero() {
|
||||
so mobile never downloads them, and desktop fills the
|
||||
already-reserved space once it hydrates.
|
||||
*/}
|
||||
<div className="hero-reveal relative w-[50vw] max-w-[720px] min-h-[525px] p-16 py-0 mx-auto hidden xl:block">
|
||||
<div className="hero-reveal lg:opacity-0 relative w-[50vw] max-w-[720px] min-h-[525px] p-16 py-0 mx-auto hidden xl:block">
|
||||
{isXl && (
|
||||
<>
|
||||
<div className="absolute inset-0 z-0 pointer-events-none">
|
||||
@@ -224,7 +186,7 @@ export default function Hero() {
|
||||
<div className="border-t border-[#292929]" />
|
||||
<div className="flex flex-nowrap items-center xl:px-22.75 px-4 bg-[#0F0F0F] w-full overflow-hidden">
|
||||
{/* Primary CTA */}
|
||||
<div className="hero-cta w-full md:w-fit md:flex-shrink-0">
|
||||
<div className="hero-cta lg:opacity-0 w-full md:w-fit md:flex-shrink-0">
|
||||
<Link
|
||||
href={
|
||||
isLoggedIn
|
||||
@@ -253,7 +215,7 @@ export default function Hero() {
|
||||
</div>
|
||||
|
||||
{/* Secondary CTA */}
|
||||
<div className="hero-cta w-full md:w-fit md:flex-shrink-0">
|
||||
<div className="hero-cta lg:opacity-0 w-full md:w-fit md:flex-shrink-0">
|
||||
<Link href={"https://cal.com/ayrod"}>
|
||||
<motion.div
|
||||
initial="initial"
|
||||
@@ -274,7 +236,7 @@ export default function Hero() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hero-cta hidden md:flex flex-nowrap gap-2 md:gap-3 ml-2 md:ml-3 h-10.5 md:h-12.5 flex-1">
|
||||
<div className="hero-cta lg:opacity-0 hidden md:flex flex-nowrap gap-2 md:gap-3 ml-2 md:ml-3 h-10.5 md:h-12.5 flex-1">
|
||||
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||
<div className="border-r border-[#292929] h-full hidden md:block" />
|
||||
@@ -305,11 +267,12 @@ export default function Hero() {
|
||||
aria-hidden="true"
|
||||
fill
|
||||
priority
|
||||
fetchPriority="high"
|
||||
sizes="100vw"
|
||||
className="hero-bg absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 pointer-events-none select-none"
|
||||
/>
|
||||
|
||||
<div className="hero-reveal relative z-10 w-[96%] sm:w-[90%] max-w-[520px] flex justify-center items-center">
|
||||
<div className="hero-reveal lg:opacity-0 relative z-10 w-[96%] sm:w-[90%] max-w-[520px] flex justify-center items-center">
|
||||
{/* <AutumnConfig lines={16} initialDelay={1000} awaitEvent="preloader:complete" /> */}
|
||||
<Image
|
||||
src={"/images/hero/autumn_mobile.svg"}
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect } from "react";
|
||||
import Hero from "./hero";
|
||||
import LazySection from "./lazy-section";
|
||||
import SectionDivider from "./section-divider";
|
||||
|
||||
// All below-fold sections are code-split into separate lazy chunks so the
|
||||
// initial JS bundle only contains the hero. Framer-motion, GSAP ScrollTrigger,
|
||||
// and lottie-web are pulled into these chunks rather than the main bundle.
|
||||
// LazySection gates each component behind an IntersectionObserver so chunks
|
||||
// and their heavy assets (Lottie JSON, ScrollTrigger) only download as the
|
||||
// user scrolls toward them rather than all at once on page load.
|
||||
const LogoWall = dynamic(() => import("./logo-wall"));
|
||||
const Problem = dynamic(() => import("./problem"));
|
||||
const Solution = dynamic(() => import("./solution"));
|
||||
@@ -52,22 +56,22 @@ export default function HomeSections() {
|
||||
</div>
|
||||
<LogoWall /> */}
|
||||
<SectionDivider title="THE PROBLEM" />
|
||||
<Problem />
|
||||
<LazySection><Problem /></LazySection>
|
||||
<SectionDivider title="THE SOLUTION" />
|
||||
<Solution />
|
||||
<LazySection><Solution /></LazySection>
|
||||
<SectionDivider title="PRICING MODELS" />
|
||||
<PricingModels />
|
||||
<LazySection><PricingModels /></LazySection>
|
||||
<SectionDivider title="FEATURES" />
|
||||
<Features />
|
||||
<LazySection><Features /></LazySection>
|
||||
<SectionDivider title="TESTIMONIALS" />
|
||||
<Testimonials />
|
||||
<LazySection><Testimonials /></LazySection>
|
||||
<SectionDivider title="PRODUCTION SCALE" />
|
||||
<ProductionScale />
|
||||
<LazySection><ProductionScale /></LazySection>
|
||||
<SectionDivider title="PRICING" />
|
||||
<Pricing />
|
||||
<LazySection><Pricing /></LazySection>
|
||||
<SectionDivider title="FAQ" />
|
||||
<FAQ />
|
||||
<Footer />
|
||||
<LazySection><FAQ /></LazySection>
|
||||
<LazySection><Footer /></LazySection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
37
apps/website/components/lazy-section.tsx
Normal file
37
apps/website/components/lazy-section.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
|
||||
export default function LazySection({ children }: { children: ReactNode }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
// iOS < 12.2 has no IntersectionObserver — mount everything immediately.
|
||||
if (!("IntersectionObserver" in window)) {
|
||||
setMounted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
// intersectionRatio > 0 guards against a Safari bug where
|
||||
// isIntersecting fires as false on the initial sync callback.
|
||||
if (entry.isIntersecting || entry.intersectionRatio > 0) {
|
||||
setMounted(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "1000px 0px" },
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// min-height: 1px prevents the wrapper collapsing to zero height before
|
||||
// content mounts, which would cause iOS to jump scroll position on mount.
|
||||
return <div ref={ref} style={{ minHeight: "1px" }}>{mounted ? children : null}</div>;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useGSAP } from "@gsap/react";
|
||||
import gsap from "gsap";
|
||||
import { motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -29,6 +27,7 @@ import type {
|
||||
PixelIconComponent,
|
||||
} from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getGsap } from "@/lib/lazyGsap";
|
||||
import { DashboardIconPixel } from "./dashboard-icon-pixel";
|
||||
|
||||
const NAV_LINKS = [
|
||||
@@ -42,10 +41,18 @@ const NAV_LINKS = [
|
||||
},
|
||||
];
|
||||
|
||||
type GSAPTimeline = {
|
||||
play: () => GSAPTimeline;
|
||||
reverse: () => GSAPTimeline;
|
||||
kill: () => void;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: structural shim for GSAP's overloaded .to() signature
|
||||
to: (...args: any[]) => GSAPTimeline;
|
||||
};
|
||||
|
||||
const NavIconPixel = forwardRef<PixelHoverHandle, { Icon: PixelIconComponent }>(
|
||||
function NavIconPixel({ Icon }, ref) {
|
||||
const iconRef = useRef<SVGSVGElement | null>(null);
|
||||
const tlRef = useRef<gsap.core.Timeline | null>(null);
|
||||
const tlRef = useRef<GSAPTimeline | null>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
restart: () => tlRef.current?.play(),
|
||||
@@ -53,46 +60,39 @@ const NavIconPixel = forwardRef<PixelHoverHandle, { Icon: PixelIconComponent }>(
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
const pixelEls =
|
||||
iconRef.current?.querySelectorAll<SVGPathElement>(".icon-pixel-path");
|
||||
if (!pixelEls?.length) return;
|
||||
const el = iconRef.current;
|
||||
if (!el) return;
|
||||
let cancelled = false;
|
||||
|
||||
const pixels = Array.from(pixelEls).sort((a, b) => {
|
||||
const aBox = a.getBBox();
|
||||
const bBox = b.getBBox();
|
||||
return (
|
||||
aBox.x +
|
||||
aBox.width / 2 -
|
||||
(aBox.y + aBox.height / 2) -
|
||||
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
|
||||
);
|
||||
});
|
||||
getGsap().then((gsap) => {
|
||||
if (cancelled) return;
|
||||
const pixelEls = el.querySelectorAll<SVGPathElement>(".icon-pixel-path");
|
||||
if (!pixelEls.length) return;
|
||||
|
||||
gsap.set(pixels, {
|
||||
opacity: 0.15,
|
||||
scale: 0.8,
|
||||
transformOrigin: "left bottom",
|
||||
fill: "currentColor",
|
||||
});
|
||||
|
||||
tlRef.current = gsap.timeline({ paused: true });
|
||||
|
||||
tlRef.current
|
||||
.to(pixels, {
|
||||
opacity: 1,
|
||||
scale: 1.15,
|
||||
fill: "#FFFFFF",
|
||||
duration: 0.01,
|
||||
stagger: 0.025,
|
||||
ease: "power2.out",
|
||||
})
|
||||
.to(pixels, {
|
||||
scale: 1,
|
||||
duration: 0.01,
|
||||
ease: "back.out(3)",
|
||||
const pixels = Array.from(pixelEls).sort((a, b) => {
|
||||
const aBox = a.getBBox();
|
||||
const bBox = b.getBBox();
|
||||
return (
|
||||
aBox.x + aBox.width / 2 - (aBox.y + aBox.height / 2) -
|
||||
(bBox.x + bBox.width / 2 - (bBox.y + bBox.height / 2))
|
||||
);
|
||||
});
|
||||
|
||||
gsap.set(pixels, {
|
||||
opacity: 0.15,
|
||||
scale: 0.8,
|
||||
transformOrigin: "left bottom",
|
||||
fill: "currentColor",
|
||||
});
|
||||
|
||||
tlRef.current = gsap.timeline({ paused: true });
|
||||
tlRef.current!
|
||||
.to(pixels, { opacity: 1, scale: 1.15, fill: "#FFFFFF", duration: 0.01, stagger: 0.025, ease: "power2.out" })
|
||||
.to(pixels, { scale: 1, duration: 0.01, ease: "back.out(3)" });
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
tlRef.current?.kill();
|
||||
};
|
||||
}, []);
|
||||
@@ -189,137 +189,21 @@ export default function Navbar({
|
||||
};
|
||||
}, [menuOpen]);
|
||||
|
||||
useGSAP(
|
||||
() => {
|
||||
if (!animateIntro) return;
|
||||
// Skip the entrance animation on non-desktop viewports, or when
|
||||
// hydration landed well after first paint. On mobile the GSAP chunk
|
||||
// can arrive many seconds after the server-rendered navbar has
|
||||
// already painted; hiding it with `gsap.set({ opacity: 0 })` at
|
||||
// that point and re-animating it in is a visible flash that's much
|
||||
// worse than no animation.
|
||||
if (
|
||||
window.matchMedia("(max-width: 1023px)").matches ||
|
||||
performance.now() > 300
|
||||
) {
|
||||
return;
|
||||
}
|
||||
gsap.set(".nav-root", { opacity: 0 });
|
||||
gsap.set(".nav-logo", {
|
||||
opacity: 0,
|
||||
filter: "blur(6px) brightness(1)",
|
||||
scale: 0.92,
|
||||
transformOrigin: "left center",
|
||||
});
|
||||
gsap.set(".nav-link", { opacity: 0, y: -8 });
|
||||
gsap.set(".nav-dashboard", { opacity: 0, scale: 0.95 });
|
||||
const navMobileRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const tl = gsap.timeline({ defaults: { overwrite: "auto" } });
|
||||
|
||||
tl.to(".nav-root", { opacity: 1, duration: 0.3, ease: "none" })
|
||||
.to(".nav-logo", {
|
||||
opacity: 1,
|
||||
filter: "blur(0px) brightness(1)",
|
||||
scale: 1,
|
||||
duration: 0.7,
|
||||
ease: "power2.out",
|
||||
})
|
||||
.to(".nav-logo", {
|
||||
filter: "blur(0px) brightness(1.6)",
|
||||
duration: 0.225,
|
||||
ease: "power2.in",
|
||||
})
|
||||
.to(".nav-logo", {
|
||||
filter: "blur(0px) brightness(1)",
|
||||
duration: 0.125,
|
||||
ease: "power2.out",
|
||||
})
|
||||
.to(
|
||||
".nav-link",
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.25,
|
||||
stagger: 0.06,
|
||||
ease: "power2.out",
|
||||
},
|
||||
"-=0.05",
|
||||
)
|
||||
.to(
|
||||
".nav-dashboard",
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.3,
|
||||
ease: "back.out(1.5)",
|
||||
},
|
||||
"-=0.1",
|
||||
);
|
||||
},
|
||||
{ scope: containerRef, dependencies: [animateIntro] },
|
||||
);
|
||||
|
||||
const mobileTl = useRef<gsap.core.Timeline | null>(null);
|
||||
|
||||
useGSAP(
|
||||
() => {
|
||||
gsap.set(".nav-mobile", {
|
||||
opacity: 0,
|
||||
clipPath: "inset(0% 0 100% 0)",
|
||||
pointerEvents: "none",
|
||||
});
|
||||
|
||||
gsap.set(
|
||||
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
|
||||
{
|
||||
opacity: 0,
|
||||
filter: "blur(8px)",
|
||||
scale: 0.95,
|
||||
},
|
||||
);
|
||||
|
||||
mobileTl.current = gsap.timeline({
|
||||
paused: true,
|
||||
defaults: { overwrite: "auto" },
|
||||
});
|
||||
|
||||
mobileTl.current
|
||||
.to(".nav-mobile", {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
pointerEvents: "auto",
|
||||
clipPath: "inset(0% 0 0% 0)",
|
||||
duration: 0.2,
|
||||
ease: "power3.inOut",
|
||||
})
|
||||
.to(
|
||||
".nav-mobile a, .nav-mobile img, .nav-mobile button, .nav-mobile .border-b",
|
||||
{
|
||||
opacity: 1,
|
||||
filter: "blur(0px)",
|
||||
scale: 1,
|
||||
duration: 0.15,
|
||||
stagger: 0.01,
|
||||
ease: "power2.out",
|
||||
},
|
||||
"-=0.1",
|
||||
);
|
||||
},
|
||||
{ scope: containerRef },
|
||||
);
|
||||
|
||||
useGSAP(
|
||||
() => {
|
||||
if (mobileTl.current) {
|
||||
if (menuOpen) {
|
||||
mobileTl.current.timeScale(1).play();
|
||||
} else {
|
||||
mobileTl.current.timeScale(1.8).reverse();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ scope: containerRef, dependencies: [menuOpen] },
|
||||
);
|
||||
useEffect(() => {
|
||||
const el = navMobileRef.current;
|
||||
if (!el) return;
|
||||
if (menuOpen) {
|
||||
el.style.opacity = "1";
|
||||
el.style.pointerEvents = "auto";
|
||||
el.style.clipPath = "inset(0% 0 0% 0)";
|
||||
} else {
|
||||
el.style.opacity = "0";
|
||||
el.style.pointerEvents = "none";
|
||||
el.style.clipPath = "inset(0% 0 100% 0)";
|
||||
}
|
||||
}, [menuOpen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -404,8 +288,9 @@ export default function Navbar({
|
||||
</nav>
|
||||
</div>
|
||||
<div
|
||||
ref={navMobileRef}
|
||||
className={cn(
|
||||
"fixed nav-mobile inset-x-0 z-40 flex h-[calc(100dvh-58px)] flex-col overflow-x-hidden overflow-y-auto bg-[#000000] px-4 pb-8 font-mono uppercase transition-all duration-300 md:px-(--page-pad) lg:top-5",
|
||||
"fixed inset-x-0 z-40 flex h-[calc(100dvh-58px)] flex-col overflow-x-hidden overflow-y-auto bg-[#000000] px-4 pb-8 font-mono uppercase md:px-(--page-pad) lg:top-5",
|
||||
scrolled && !recoilHidden
|
||||
? "top-[58px] sm:top-[60px]"
|
||||
: "top-[66px] sm:top-[62px]",
|
||||
@@ -414,6 +299,7 @@ export default function Navbar({
|
||||
opacity: 0,
|
||||
pointerEvents: "none",
|
||||
clipPath: "inset(0% 0 100% 0)",
|
||||
transition: "opacity 0.2s ease, clip-path 0.2s cubic-bezier(0.87, 0, 0.13, 1)",
|
||||
}}
|
||||
>
|
||||
{/* Nav items */}
|
||||
|
||||
@@ -66,9 +66,16 @@ export default function SolutionAnimation() {
|
||||
// Defer the 1.4–2.1 MB Lottie JSON fetch until the element is close to
|
||||
// the viewport. Without this the browser fetches it during initial load
|
||||
// and blocks the main thread while parsing the large JSON blob.
|
||||
if (!("IntersectionObserver" in window)) {
|
||||
initAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
// intersectionRatio > 0 guards against a Safari bug where
|
||||
// isIntersecting fires as false on the initial sync callback.
|
||||
if (entries[0].isIntersecting || entries[0].intersectionRatio > 0) {
|
||||
observer.disconnect();
|
||||
initAnimation();
|
||||
}
|
||||
|
||||
10
apps/website/lib/lazyGsap.ts
Normal file
10
apps/website/lib/lazyGsap.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Loads GSAP exactly once — first call triggers the dynamic import,
|
||||
// subsequent calls reuse the same promise.
|
||||
type Gsap = (typeof import("gsap"))["default"];
|
||||
|
||||
let promise: Promise<Gsap> | null = null;
|
||||
|
||||
export const getGsap = (): Promise<Gsap> => {
|
||||
if (!promise) promise = import("gsap").then((m) => m.default);
|
||||
return promise;
|
||||
};
|
||||
@@ -7,13 +7,13 @@ const nextConfig = {
|
||||
pageExtensions: ["ts", "tsx", "md", "mdx"],
|
||||
allowedDevOrigins: ["*.ngrok-free.dev"],
|
||||
experimental: {
|
||||
optimizePackageImports: ["framer-motion", "motion", "gsap", "@gsap/react"],
|
||||
optimizePackageImports: ["motion", "gsap", "@gsap/react"],
|
||||
optimizeCss: true,
|
||||
},
|
||||
images: {
|
||||
// Serve AVIF to supporting browsers (better compression than WebP),
|
||||
// falling back to WebP. Next.js negotiates via Accept header automatically.
|
||||
// formats: ["image/avif", "image/webp"],
|
||||
formats: ["image/avif", "image/webp"],
|
||||
},
|
||||
async headers() {
|
||||
if (!isProd) return [];
|
||||
|
||||
@@ -16,12 +16,9 @@
|
||||
"@next/mdx": "^16.2.4",
|
||||
"@types/react": "^19.2.14",
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"framer-motion": "^12.38.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"gsap": "^3.15.0",
|
||||
"lottie-react": "^2.4.1",
|
||||
"lottie-web": "^5.13.0",
|
||||
"matter-js": "^0.20.0",
|
||||
"motion": "^12.38.0",
|
||||
"next": "16.2.4",
|
||||
"ngrok": "^5.0.0-beta.2",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 884 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 22 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 98 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.7 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 996 KiB |
Reference in New Issue
Block a user