diff --git a/apps/website/bun.lock b/apps/website/bun.lock index cda46bfdf..f356db8fc 100644 --- a/apps/website/bun.lock +++ b/apps/website/bun.lock @@ -1,25 +1,28 @@ { "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 +162,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 +174,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=="], @@ -809,6 +816,8 @@ "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 +840,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 +918,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 +1014,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 +1148,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 +1166,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 +1224,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=="], diff --git a/apps/website/components/dashboard-icon-pixel.tsx b/apps/website/components/dashboard-icon-pixel.tsx index 561b1e362..3de2f60dc 100644 --- a/apps/website/components/dashboard-icon-pixel.tsx +++ b/apps/website/components/dashboard-icon-pixel.tsx @@ -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(null); - const tlRef = useRef(null); + const tlRef = useRef(null); useImperativeHandle(ref, () => ({ restart: () => tlRef.current?.play(), @@ -16,47 +24,40 @@ export const DashboardIconPixel = forwardRef< })); useEffect(() => { - const pixelEls = - iconRef.current?.querySelectorAll(".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(".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(); }; }, []); diff --git a/apps/website/components/elastic-footer.tsx b/apps/website/components/elastic-footer.tsx index c12119a04..5e56dc323 100644 --- a/apps/website/components/elastic-footer.tsx +++ b/apps/website/components/elastic-footer.tsx @@ -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"; diff --git a/apps/website/components/faq.tsx b/apps/website/components/faq.tsx index 3ced942fc..3afeffcc3 100644 --- a/apps/website/components/faq.tsx +++ b/apps/website/components/faq.tsx @@ -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"; diff --git a/apps/website/components/hero.tsx b/apps/website/components/hero.tsx index 3a27d910c..bcf8e03c5 100755 --- a/apps/website/components/hero.tsx +++ b/apps/website/components/hero.tsx @@ -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,85 +87,44 @@ 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", - }); + 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 }); - gsap.set(".hero-cta", { opacity: 0, scale: 0.95 }); + const tl = gsap.timeline({ defaults: { overwrite: "auto" } }); - const tl = gsap.timeline({ - defaults: { overwrite: "auto" }, - }); + // 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); + }); - tl.to(".hero-root", { opacity: 1, duration: 0.3, ease: "none" }) - - .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 (
-
+

@@ -305,6 +263,7 @@ 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" /> diff --git a/apps/website/components/navbar.tsx b/apps/website/components/navbar.tsx index 5d63cf272..9e531bfed 100644 --- a/apps/website/components/navbar.tsx +++ b/apps/website/components/navbar.tsx @@ -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( function NavIconPixel({ Icon }, ref) { const iconRef = useRef(null); - const tlRef = useRef(null); + const tlRef = useRef(null); useImperativeHandle(ref, () => ({ restart: () => tlRef.current?.play(), @@ -53,46 +60,39 @@ const NavIconPixel = forwardRef( })); useEffect(() => { - const pixelEls = - iconRef.current?.querySelectorAll(".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(".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(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(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 (
{/* Nav items */} diff --git a/apps/website/lib/lazyGsap.ts b/apps/website/lib/lazyGsap.ts new file mode 100644 index 000000000..735d3dbc8 --- /dev/null +++ b/apps/website/lib/lazyGsap.ts @@ -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 | null = null; + +export const getGsap = (): Promise => { + if (!promise) promise = import("gsap").then((m) => m.default); + return promise; +}; diff --git a/apps/website/next.config.mjs b/apps/website/next.config.mjs index 09dfd8978..c01b86ed3 100644 --- a/apps/website/next.config.mjs +++ b/apps/website/next.config.mjs @@ -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 []; diff --git a/apps/website/package.json b/apps/website/package.json index 66cae08e1..e0fbf0228 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -16,11 +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",