فهرست منبع

chore: more UI performance improvements

This PR contains a few related performance improvements:

1. Enable react compiler for IH, Entropy Explorer, and component library
2. Refactor Drawer state management to reduce rerenders drastically
3. Rev nextjs patch version
4. Disable turbopack for IH as it's causing major issues with webinspector
Connor Prussin 6 ماه پیش
والد
کامیت
988dd16f6c
55فایلهای تغییر یافته به همراه697 افزوده شده و 255 حذف شده
  1. 4 0
      apps/entropy-explorer/next.config.js
  2. 1 0
      apps/entropy-explorer/package.json
  3. 1 1
      apps/entropy-explorer/src/components/Home/chain-select.tsx
  4. 1 2
      apps/entropy-explorer/src/components/Home/request-drawer.tsx
  5. 1 0
      apps/entropy-explorer/src/components/Home/results.tsx
  6. 5 4
      apps/insights/next.config.js
  7. 2 1
      apps/insights/package.json
  8. 1 1
      apps/insights/src/components/Explain/index.tsx
  9. 1 1
      apps/insights/src/components/PriceComponentDrawer/index.tsx
  10. 1 1
      apps/insights/src/components/PriceComponentsCard/index.tsx
  11. 1 1
      apps/insights/src/components/PriceFeed/header.tsx
  12. 1 1
      apps/insights/src/components/PriceFeed/reference-data.tsx
  13. 1 1
      apps/insights/src/components/PriceFeeds/asset-class-table.tsx
  14. 0 1
      apps/insights/src/components/PriceFeeds/coming-soon-list.module.scss
  15. 1 1
      apps/insights/src/components/PriceFeeds/coming-soon-list.tsx
  16. 4 0
      apps/insights/src/components/PriceFeeds/index.module.scss
  17. 1 1
      apps/insights/src/components/PriceFeeds/index.tsx
  18. 1 1
      apps/insights/src/components/PriceFeeds/price-feeds-card.tsx
  19. 2 2
      apps/insights/src/components/Publisher/layout.tsx
  20. 1 1
      apps/insights/src/components/Publishers/index.tsx
  21. 2 2
      apps/insights/src/components/Publishers/publishers-card.tsx
  22. 3 3
      apps/insights/src/components/Root/search-button.tsx
  23. 44 30
      apps/insights/src/hooks/use-live-price-data.tsx
  24. 6 0
      packages/component-library/babel.config.js
  25. 6 1
      packages/component-library/package.json
  26. 2 0
      packages/component-library/src/AppShell/index.tsx
  27. 2 0
      packages/component-library/src/Badge/index.tsx
  28. 1 1
      packages/component-library/src/Breadcrumbs/index.tsx
  29. 5 2
      packages/component-library/src/Button/index.module.scss
  30. 14 7
      packages/component-library/src/Button/index.stories.tsx
  31. 13 18
      packages/component-library/src/Button/index.tsx
  32. 2 0
      packages/component-library/src/DropdownCaretDown/index.tsx
  33. 2 2
      packages/component-library/src/ErrorPage/index.tsx
  34. 1 1
      packages/component-library/src/Footer/index.tsx
  35. 6 6
      packages/component-library/src/Header/index.tsx
  36. 2 0
      packages/component-library/src/Header/theme-switch.module.scss
  37. 12 7
      packages/component-library/src/Header/theme-switch.tsx
  38. 2 0
      packages/component-library/src/InfoBox/index.tsx
  39. 2 0
      packages/component-library/src/Link/index.tsx
  40. 172 47
      packages/component-library/src/ModalDialog/index.tsx
  41. 3 1
      packages/component-library/src/NotFoundPage/index.tsx
  42. 2 2
      packages/component-library/src/Paginator/index.tsx
  43. 1 3
      packages/component-library/src/Select/index.tsx
  44. 2 0
      packages/component-library/src/Skeleton/index.tsx
  45. 2 0
      packages/component-library/src/StatCard/index.tsx
  46. 9 1
      packages/component-library/src/Table/index.module.scss
  47. 4 5
      packages/component-library/src/Table/index.tsx
  48. 5 5
      packages/component-library/src/social-links.tsx
  49. 3 3
      packages/component-library/src/useAlert/index.tsx
  50. 5 0
      packages/component-library/src/useDrawer/index.stories.tsx
  51. 3 3
      packages/component-library/src/useDrawer/index.tsx
  52. 2 1
      packages/component-library/tsconfig.build.json
  53. 6 0
      packages/component-library/turbo.json
  54. 314 81
      pnpm-lock.yaml
  55. 6 2
      pnpm-workspace.yaml

+ 4 - 0
apps/entropy-explorer/next.config.js

@@ -1,4 +1,8 @@
 const config = {
+  experimental: {
+    reactCompiler: true,
+  },
+
   reactStrictMode: true,
 
   pageExtensions: ["ts", "tsx", "mdx"],

+ 1 - 0
apps/entropy-explorer/package.json

@@ -45,6 +45,7 @@
     "@types/react": "catalog:",
     "@types/react-dom": "catalog:",
     "autoprefixer": "catalog:",
+    "babel-plugin-react-compiler": "catalog:",
     "eslint": "catalog:",
     "jest": "catalog:",
     "postcss": "catalog:",

+ 1 - 1
apps/entropy-explorer/src/components/Home/chain-select.tsx

@@ -96,7 +96,7 @@ const useResolvedProps = () => {
     textValue: chainTextValue,
     buttonLabel: viemChain?.name ?? "Chain",
     ...(viemChain && {
-      icon: () => <ChainIcon id={viemChain.id} />,
+      icon: <ChainIcon id={viemChain.id} />,
     }),
   };
 };

+ 1 - 2
apps/entropy-explorer/src/components/Home/request-drawer.tsx

@@ -65,7 +65,6 @@ const RequestDrawerBody = ({ request }: { request: Request }) => {
         label="Details"
         fill
         className={styles.details ?? ""}
-        stickyHeader
         columns={[
           {
             id: "field",
@@ -254,7 +253,7 @@ const CallbackFailedInfo = ({ request }: { request: CallbackErrorRequest }) => {
           <Button
             size="sm"
             variant="ghost"
-            beforeIcon={Question}
+            beforeIcon={<Question />}
             rounded
             hideText
             href="https://docs.pyth.network/entropy/debug-callback-failures"

+ 1 - 0
apps/entropy-explorer/src/components/Home/results.tsx

@@ -207,6 +207,7 @@ const defaultProps = {
   label: "Requests",
   rounded: true,
   fill: true,
+  stickyHeader: "appHeader",
   columns: [
     {
       id: "chain" as const,

+ 5 - 4
apps/insights/next.config.js

@@ -1,12 +1,13 @@
 const config = {
-  reactStrictMode: true,
-
-  pageExtensions: ["ts", "tsx", "mdx"],
-
   experimental: {
     useCache: true,
+    reactCompiler: true,
   },
 
+  reactStrictMode: true,
+
+  pageExtensions: ["ts", "tsx", "mdx"],
+
   logging: {
     fetches: {
       fullUrl: true,

+ 2 - 1
apps/insights/package.json

@@ -12,7 +12,7 @@
     "fix:lint:eslint": "eslint --fix .",
     "fix:lint:stylelint": "stylelint --fix 'src/**/*.scss'",
     "pull:env": "[ $CI ] || VERCEL_ORG_ID=team_BKQrg3JJFLxZyTqpuYtIY0rj VERCEL_PROJECT_ID=prj_TBkf9EyQjQF37gs4Vk0sQKJj97kE vercel env pull",
-    "start:dev": "next dev --port 3003 --turbopack",
+    "start:dev": "next dev --port 3003",
     "start:prod": "next start --port 3003",
     "test:format": "prettier --check .",
     "test:lint:eslint": "eslint . --max-warnings 0",
@@ -58,6 +58,7 @@
     "@types/react": "catalog:",
     "@types/react-dom": "catalog:",
     "autoprefixer": "catalog:",
+    "babel-plugin-react-compiler": "catalog:",
     "eslint": "catalog:",
     "jest": "catalog:",
     "postcss": "catalog:",

+ 1 - 1
apps/insights/src/components/Explain/index.tsx

@@ -17,7 +17,7 @@ export const Explain = ({ size, title, children }: Props) => (
       className={styles.trigger ?? ""}
       variant="ghost"
       size={size}
-      beforeIcon={(props) => <Info weight="fill" {...props} />}
+      beforeIcon={<Info weight="fill" />}
       rounded
       hideText
       alert={{

+ 1 - 1
apps/insights/src/components/PriceComponentDrawer/index.tsx

@@ -270,7 +270,7 @@ const HeadingExtra = ({ status, ...props }: HeadingExtraProps) => {
       <OpenButton
         variant="ghost"
         hideText
-        beforeIcon={ArrowSquareOut}
+        beforeIcon={<ArrowSquareOut />}
         rounded
         className={styles.ghostOpenButton ?? ""}
         {...props}

+ 1 - 1
apps/insights/src/components/PriceComponentsCard/index.tsx

@@ -489,7 +489,7 @@ export const PriceComponentsCardContents = <
         label={label}
         fill
         rounded
-        stickyHeader
+        stickyHeader="appHeader"
         className={styles.table ?? ""}
         columns={[
           {

+ 1 - 1
apps/insights/src/components/PriceFeed/header.tsx

@@ -135,7 +135,7 @@ const PriceFeedHeaderImpl = (props: PriceFeedHeaderImplProps) => (
         <Button
           variant="outline"
           size="sm"
-          beforeIcon={ListDashes}
+          beforeIcon={<ListDashes />}
           isPending={props.isLoading}
           {...(!props.isLoading && {
             drawer: {

+ 1 - 1
apps/insights/src/components/PriceFeed/reference-data.tsx

@@ -105,7 +105,7 @@ export const ReferenceData = ({ feed }: Props) => {
     <Table
       label="Reference Data"
       fill
-      stickyHeader
+      stickyHeader="top"
       className={styles.referenceData ?? ""}
       columns={[
         {

+ 1 - 1
apps/insights/src/components/PriceFeeds/asset-class-table.tsx

@@ -66,7 +66,7 @@ export const AssetClassTable = ({ numFeedsByAssetClass }: Props) => {
   return (
     <Table
       fill
-      stickyHeader
+      stickyHeader="top"
       label="Asset Classes"
       columns={[
         {

+ 0 - 1
apps/insights/src/components/PriceFeeds/coming-soon-list.module.scss

@@ -4,7 +4,6 @@
   display: flex;
   flex-flow: column nowrap;
   overflow: hidden;
-  height: 100%;
 
   .searchBar {
     width: 100%;

+ 1 - 1
apps/insights/src/components/PriceFeeds/coming-soon-list.tsx

@@ -110,7 +110,7 @@ export const ComingSoonList = ({ comingSoonFeeds }: Props) => {
       </div>
       <Table
         fill
-        stickyHeader
+        stickyHeader="top"
         label="Coming Soon"
         className={styles.priceFeeds ?? ""}
         emptyState={

+ 4 - 0
apps/insights/src/components/PriceFeeds/index.module.scss

@@ -87,3 +87,7 @@
     }
   }
 }
+
+.comingSoonCard {
+  grid-template-rows: 1fr;
+}

+ 1 - 1
apps/insights/src/components/PriceFeeds/index.tsx

@@ -195,7 +195,7 @@ const FeaturedFeeds = ({
           variant="outline"
           drawer={{
             fill: true,
-            className: styles.comingSoonCard ?? "",
+            bodyClassName: styles.comingSoonCard ?? "",
             title: (
               <>
                 <span>Coming Soon</span>

+ 1 - 1
apps/insights/src/components/PriceFeeds/price-feeds-card.tsx

@@ -319,7 +319,7 @@ const PriceFeedsCardContents = ({ id, ...props }: PriceFeedsCardContents) => (
       rounded
       fill
       label="Price Feeds"
-      stickyHeader
+      stickyHeader="appHeader"
       className={styles.table ?? ""}
       columns={[
         {

+ 2 - 2
apps/insights/src/components/Publisher/layout.tsx

@@ -494,7 +494,7 @@ const OisPoolCardImpl = (props: OisPoolCardImplProps) => (
             size="sm"
             href="https://staking.pyth.network"
             target="_blank"
-            beforeIcon={Browsers}
+            beforeIcon={<Browsers />}
           >
             Open Staking App
           </Button>
@@ -503,7 +503,7 @@ const OisPoolCardImpl = (props: OisPoolCardImplProps) => (
             size="sm"
             href="https://docs.pyth.network/home/oracle-integrity-staking"
             target="_blank"
-            beforeIcon={BookOpenText}
+            beforeIcon={<BookOpenText />}
           >
             Documentation
           </Button>

+ 1 - 1
apps/insights/src/components/Publishers/index.tsx

@@ -91,7 +91,7 @@ export const Publishers = async () => {
               target="_blank"
               size="sm"
               variant="outline"
-              afterIcon={ArrowSquareOut}
+              afterIcon={<ArrowSquareOut />}
             >
               Staking App
             </Button>

+ 2 - 2
apps/insights/src/components/Publishers/publishers-card.tsx

@@ -270,7 +270,7 @@ const PublishersCardContents = ({
           variant="outline"
           hideLabel
           options={CLUSTER_NAMES.map((id) => ({ id }))}
-          icon={Database}
+          icon={<Database />}
           {...(props.isLoading
             ? { isPending: true, buttonLabel: "Cluster" }
             : {
@@ -323,7 +323,7 @@ const PublishersCardContents = ({
       rounded
       fill
       label="Publishers"
-      stickyHeader
+      stickyHeader="appHeader"
       className={styles.table ?? ""}
       columns={[
         {

+ 3 - 3
apps/insights/src/components/Root/search-button.tsx

@@ -77,7 +77,7 @@ const SearchButtonImpl = (
     <Button
       className={styles.largeScreenSearchButton ?? ""}
       variant="outline"
-      beforeIcon={MagnifyingGlass}
+      beforeIcon={<MagnifyingGlass />}
       size="sm"
       rounded
       {...props}
@@ -88,7 +88,7 @@ const SearchButtonImpl = (
       className={styles.smallScreenSearchButton ?? ""}
       hideText
       variant="ghost"
-      beforeIcon={MagnifyingGlass}
+      beforeIcon={<MagnifyingGlass />}
       size="sm"
       rounded
       {...props}
@@ -249,7 +249,7 @@ const SearchDialogContents = ({
         </div>
         <Button
           className={styles.closeButton ?? ""}
-          beforeIcon={(props) => <XCircle weight="fill" {...props} />}
+          beforeIcon={<XCircle weight="fill" />}
           slot="close"
           hideText
           rounded

+ 44 - 30
apps/insights/src/hooks/use-live-price-data.tsx

@@ -2,7 +2,6 @@
 
 import type { PriceData } from "@pythnetwork/client";
 import { useLogger } from "@pythnetwork/component-library/useLogger";
-import { useMap } from "@react-hookz/web";
 import { PublicKey } from "@solana/web3.js";
 import type { ComponentProps } from "react";
 import {
@@ -12,6 +11,7 @@ import {
   useCallback,
   useState,
   useMemo,
+  useRef,
 } from "react";
 
 import {
@@ -20,8 +20,6 @@ import {
   getAssetPricesFromAccounts,
 } from "../services/pyth";
 
-export const SKELETON_WIDTH = 20;
-
 const LivePriceDataContext = createContext<
   ReturnType<typeof usePriceData> | undefined
 >(undefined);
@@ -38,20 +36,22 @@ export const LivePriceDataProvider = (props: LivePriceDataProviderProps) => {
 };
 
 export const useLivePriceData = (cluster: Cluster, feedKey: string) => {
-  const { priceData, prevPriceData, addSubscription, removeSubscription } =
+  const { addSubscription, removeSubscription } =
     useLivePriceDataContext()[cluster];
 
+  const [data, setData] = useState<{
+    current: PriceData | undefined;
+    prev: PriceData | undefined;
+  }>({ current: undefined, prev: undefined });
+
   useEffect(() => {
-    addSubscription(feedKey);
+    addSubscription(feedKey, setData);
     return () => {
-      removeSubscription(feedKey);
+      removeSubscription(feedKey, setData);
     };
   }, [addSubscription, removeSubscription, feedKey]);
 
-  const current = priceData.get(feedKey);
-  const prev = prevPriceData.get(feedKey);
-
-  return { current, prev };
+  return data;
 };
 
 export const useLivePriceComponent = (
@@ -85,11 +85,16 @@ const usePriceData = () => {
   };
 };
 
+type Subscription = (value: {
+  current: PriceData;
+  prev: PriceData | undefined;
+}) => void;
+
 const usePriceDataForCluster = (cluster: Cluster) => {
-  const feedSubscriptions = useMap<string, number>([]);
   const [feedKeys, setFeedKeys] = useState<string[]>([]);
-  const prevPriceData = useMap<string, PriceData>([]);
-  const priceData = useMap<string, PriceData>([]);
+  const feedSubscriptions = useRef<Map<string, Set<Subscription>>>(new Map());
+  const priceData = useRef<Map<string, PriceData>>(new Map());
+  const prevPriceData = useRef<Map<string, PriceData>>(new Map());
   const logger = useLogger();
 
   useEffect(() => {
@@ -97,7 +102,9 @@ const usePriceDataForCluster = (cluster: Cluster) => {
     // there's any symbol that isn't currently publishing prices (e.g. the
     // markets are closed), we will still display the last published price for
     // that symbol.
-    const uninitializedFeedKeys = feedKeys.filter((key) => !priceData.has(key));
+    const uninitializedFeedKeys = feedKeys.filter(
+      (key) => !priceData.current.has(key),
+    );
     if (uninitializedFeedKeys.length > 0) {
       getAssetPricesFromAccounts(
         cluster,
@@ -106,8 +113,8 @@ const usePriceDataForCluster = (cluster: Cluster) => {
         .then((initialPrices) => {
           for (const [i, price] of initialPrices.entries()) {
             const key = uninitializedFeedKeys[i];
-            if (key && !priceData.has(key)) {
-              priceData.set(key, price);
+            if (key && !priceData.current.has(key)) {
+              priceData.current.set(key, price);
             }
           }
         })
@@ -122,11 +129,16 @@ const usePriceDataForCluster = (cluster: Cluster) => {
       feedKeys.map((key) => new PublicKey(key)),
       ({ price_account }, data) => {
         if (price_account) {
-          const prevData = priceData.get(price_account);
+          const prevData = priceData.current.get(price_account);
           if (prevData) {
-            prevPriceData.set(price_account, prevData);
+            prevPriceData.current.set(price_account, prevData);
+          }
+          priceData.current.set(price_account, data);
+          for (const subscription of feedSubscriptions.current.get(
+            price_account,
+          ) ?? []) {
+            subscription({ current: data, prev: prevData });
           }
-          priceData.set(price_account, data);
         }
       },
     );
@@ -139,26 +151,30 @@ const usePriceDataForCluster = (cluster: Cluster) => {
         logger.error("Failed to unsubscribe from price updates", error);
       });
     };
-  }, [feedKeys, logger, priceData, prevPriceData, cluster]);
+  }, [feedKeys, logger, cluster]);
 
   const addSubscription = useCallback(
-    (key: string) => {
-      const current = feedSubscriptions.get(key) ?? 0;
-      feedSubscriptions.set(key, current + 1);
-      if (current === 0) {
+    (key: string, subscription: Subscription) => {
+      const current = feedSubscriptions.current.get(key);
+      if (current === undefined) {
+        feedSubscriptions.current.set(key, new Set([subscription]));
         setFeedKeys((prev) => [...new Set([...prev, key])]);
+      } else {
+        current.add(subscription);
       }
     },
     [feedSubscriptions],
   );
 
   const removeSubscription = useCallback(
-    (key: string) => {
-      const current = feedSubscriptions.get(key);
+    (key: string, subscription: Subscription) => {
+      const current = feedSubscriptions.current.get(key);
       if (current) {
-        feedSubscriptions.set(key, current - 1);
-        if (current === 1) {
+        if (current.size === 0) {
+          feedSubscriptions.current.delete(key);
           setFeedKeys((prev) => prev.filter((elem) => elem !== key));
+        } else {
+          current.delete(subscription);
         }
       }
     },
@@ -166,8 +182,6 @@ const usePriceDataForCluster = (cluster: Cluster) => {
   );
 
   return {
-    priceData: new Map(priceData),
-    prevPriceData: new Map(prevPriceData),
     addSubscription,
     removeSubscription,
   };

+ 6 - 0
packages/component-library/babel.config.js

@@ -0,0 +1,6 @@
+const config = {
+  presets: ["@babel/preset-typescript"],
+  plugins: ["babel-plugin-react-compiler"],
+};
+
+export default config;

+ 6 - 1
packages/component-library/package.json

@@ -18,7 +18,8 @@
     "./theme": "./dist/esm/theme.scss"
   },
   "scripts": {
-    "build:esm": "tsc --project tsconfig.build.json --outDir ./dist/esm && echo '{\"type\":\"module\"}' > dist/esm/package.json",
+    "build:declarations": "tsc --project tsconfig.build.json --outDir ./dist/esm",
+    "build:esm": "babel src --out-dir ./dist/esm --extensions .ts && babel src --out-dir ./dist/esm --extensions .tsx --out-file-extension .jsx",
     "build:scss": "copyfiles -u 1 \"src/**/*.scss\" dist/esm",
     "build:storybook": "storybook build",
     "build:svg": "copyfiles -u 1 \"src/**/*.svg\" dist/esm",
@@ -53,6 +54,9 @@
     "swr": "catalog:"
   },
   "devDependencies": {
+    "@babel/cli": "catalog:",
+    "@babel/core": "catalog:",
+    "@babel/preset-typescript": "catalog:",
     "@cprussin/eslint-config": "catalog:",
     "@cprussin/jest-config": "catalog:",
     "@cprussin/prettier-config": "catalog:",
@@ -69,6 +73,7 @@
     "@types/react": "catalog:",
     "@types/react-dom": "catalog:",
     "autoprefixer": "catalog:",
+    "babel-plugin-react-compiler": "catalog:",
     "copyfiles": "catalog:",
     "css-loader": "catalog:",
     "eslint": "catalog:",

+ 2 - 0
packages/component-library/src/AppShell/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import { GoogleAnalytics } from "@next/third-parties/google";
 import clsx from "clsx";
 import dynamic from "next/dynamic";

+ 2 - 0
packages/component-library/src/Badge/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import clsx from "clsx";
 import type { ComponentProps } from "react";
 

+ 1 - 1
packages/component-library/src/Breadcrumbs/index.tsx

@@ -41,7 +41,7 @@ export const Breadcrumbs = ({ label, className, items, ...props }: Props) => (
                 <Button
                   size="xs"
                   variant="outline"
-                  beforeIcon={House}
+                  beforeIcon={<House />}
                   hideText
                   href="/"
                 >

+ 5 - 2
packages/component-library/src/Button/index.module.scss

@@ -19,6 +19,10 @@
   line-height: normal;
   -webkit-tap-highlight-color: transparent;
 
+  .icon {
+    display: grid;
+  }
+
   @each $size, $values in theme.$button-sizes {
     &[data-size="#{$size}"] {
       height: theme.map-get-strict($values, "height");
@@ -27,8 +31,7 @@
       font-size: theme.map-get-strict($values, "font-size");
 
       .icon {
-        width: theme.map-get-strict($values, "icon-size");
-        height: theme.map-get-strict($values, "icon-size");
+        font-size: theme.map-get-strict($values, "icon-size");
       }
 
       .text {

+ 14 - 7
packages/component-library/src/Button/index.stories.tsx

@@ -1,8 +1,19 @@
-import * as Icon from "@phosphor-icons/react/dist/ssr";
+import * as icons from "@phosphor-icons/react/dist/ssr";
 import type { Meta, StoryObj } from "@storybook/react";
 
 import { Button as ButtonComponent, VARIANTS, SIZES } from "./index.jsx";
 
+const iconControl = {
+  control: "select",
+  options: Object.keys(icons),
+  mapping: Object.fromEntries(
+    Object.entries(icons).map(([iconName, Icon]) => [
+      iconName,
+      <Icon key={iconName} weights={new Map()} />,
+    ]),
+  ),
+} as const;
+
 const meta = {
   component: ButtonComponent,
   argTypes: {
@@ -45,17 +56,13 @@ const meta = {
       },
     },
     beforeIcon: {
-      control: "select",
-      options: Object.keys(Icon),
-      mapping: Icon,
+      ...iconControl,
       table: {
         category: "Contents",
       },
     },
     afterIcon: {
-      control: "select",
-      options: Object.keys(Icon),
-      mapping: Icon,
+      ...iconControl,
       table: {
         category: "Contents",
       },

+ 13 - 18
packages/component-library/src/Button/index.tsx

@@ -1,10 +1,7 @@
+"use client";
+
 import clsx from "clsx";
-import type {
-  ComponentProps,
-  ElementType,
-  ComponentType,
-  ReactNode,
-} from "react";
+import type { ComponentProps, ElementType, ReactNode } from "react";
 
 import styles from "./index.module.scss";
 import { Button as UnstyledButton } from "../unstyled/Button/index.jsx";
@@ -27,8 +24,8 @@ type OwnProps = {
   rounded?: boolean | undefined;
   hideText?: boolean | undefined;
   children: ReactNode;
-  beforeIcon?: Icon | undefined;
-  afterIcon?: Icon | undefined;
+  beforeIcon?: ReactNode | undefined;
+  afterIcon?: ReactNode | undefined;
 };
 
 export type Props<T extends ElementType> = Omit<
@@ -52,8 +49,8 @@ const buttonProps = ({
   rounded = false,
   className,
   children,
-  beforeIcon: BeforeIcon,
-  afterIcon: AfterIcon,
+  beforeIcon,
+  afterIcon,
   hideText = false,
   ...otherProps
 }: OwnProps & { className?: Parameters<typeof clsx>[0] }) => ({
@@ -65,15 +62,13 @@ const buttonProps = ({
   className: clsx(styles.button, className),
   children: (
     <>
-      {BeforeIcon !== undefined && <BeforeIcon className={styles.icon} />}
+      {beforeIcon !== undefined && (
+        <div className={styles.icon}>{beforeIcon}</div>
+      )}
       <span className={styles.text}>{children}</span>
-      {AfterIcon !== undefined && <AfterIcon className={styles.icon} />}
+      {afterIcon !== undefined && (
+        <div className={styles.icon}>{afterIcon}</div>
+      )}
     </>
   ),
 });
-
-const Icon = ({ icon: IconComponent }: { icon: Icon }) => (
-  <IconComponent className={styles.icon} />
-);
-
-type Icon = ComponentType<{ className?: string | undefined }>;

+ 2 - 0
packages/component-library/src/DropdownCaretDown/index.tsx

@@ -6,6 +6,8 @@ export const DropdownCaretDown = (
   <svg
     xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 20 20"
+    width="1em"
+    height="1em"
     fill="currentColor"
     {...props}
   >

+ 2 - 2
packages/component-library/src/ErrorPage/index.tsx

@@ -21,8 +21,8 @@ export const ErrorPage = ({ error, reset }: Props) => {
     <div className={styles.errorPage}>
       <Warning className={styles.errorIcon} />
       <div className={styles.text}>
-        <h1 className={styles.header}>Uh oh!</h1>
-        <h2 className={styles.subheader}>Something went wrong</h2>
+        <h2 className={styles.header}>Uh oh!</h2>
+        <p className={styles.subheader}>Something went wrong</p>
         <code className={styles.details}>{error.digest ?? error.message}</code>
       </div>
       {reset && (

+ 1 - 1
packages/component-library/src/Footer/index.tsx

@@ -7,7 +7,7 @@ import type { Props as ButtonProps } from "../Button/index.jsx";
 import { Button } from "../Button/index.jsx";
 import { SupportDrawer } from "../Header/index.jsx";
 import { Link } from "../Link/index.jsx";
-import { socialLinks } from "../social-links.js";
+import { socialLinks } from "../social-links.jsx";
 
 export const Footer = ({ className, ...props }: ComponentProps<"footer">) => (
   <footer className={clsx(styles.footer, className)} {...props}>

+ 6 - 6
packages/component-library/src/Header/index.tsx

@@ -10,7 +10,7 @@ import { ShieldChevron } from "@phosphor-icons/react/dist/ssr/ShieldChevron";
 import clsx from "clsx";
 import type { ComponentProps, ReactNode } from "react";
 
-import { socialLinks } from "../social-links.js";
+import { socialLinks } from "../social-links.jsx";
 import styles from "./index.module.scss";
 import Logo from "./logo.svg";
 import { ThemeSwitch } from "./theme-switch.jsx";
@@ -57,7 +57,7 @@ export const Header = ({
           variant="ghost"
           size="sm"
           rounded
-          beforeIcon={Lifebuoy}
+          beforeIcon={<Lifebuoy />}
           drawer={SupportDrawer}
           className={styles.supportButton ?? ""}
         >
@@ -83,7 +83,7 @@ export const Header = ({
 const MobileMenu = ({ className }: { className?: string | undefined }) => (
   <Button
     className={className ?? ""}
-    beforeIcon={List}
+    beforeIcon={<List />}
     variant="ghost"
     size="sm"
     rounded
@@ -105,7 +105,7 @@ const MobileMenuContents = () => (
         variant="ghost"
         size="md"
         rounded
-        beforeIcon={Lifebuoy}
+        beforeIcon={<Lifebuoy />}
         drawer={SupportDrawer}
       >
         Support
@@ -218,12 +218,12 @@ export const SupportDrawer = {
       />
       <LinkList
         title="Community"
-        links={socialLinks.map(({ icon: Icon, href, name }) => ({
+        links={socialLinks.map(({ icon, href, name }) => ({
           href,
           target: "_blank",
           title: name,
           description: href,
-          icon: <Icon />,
+          icon,
         }))}
       />
     </>

+ 2 - 0
packages/component-library/src/Header/theme-switch.module.scss

@@ -5,6 +5,8 @@
 
   .iconPath {
     position: relative;
+    width: 1em;
+    height: 1em;
 
     .iconPlaceholder,
     .iconMovement {

+ 12 - 7
packages/component-library/src/Header/theme-switch.tsx

@@ -37,7 +37,7 @@ export const ThemeSwitch = <T extends ElementType>({
       size="sm"
       hideText
       onPress={toggleTheme}
-      beforeIcon={IconPath}
+      beforeIcon={<IconPath />}
       className={clsx(styles.themeSwitch, className)}
       rounded
       {...props}
@@ -51,13 +51,18 @@ const IconPath = ({ className, ...props }: Omit<IconProps, "offset">) => {
   const offsets = useOffsets();
   const isSSR = useIsSSR();
 
-  return isSSR ? (
-    <div className={className} />
-  ) : (
+  return (
     <div className={clsx(styles.iconPath, className)}>
-      <IconMovement icon={<Desktop {...props} />} offset={offsets.desktop} />
-      <IconMovement icon={<Sun {...props} />} offset={offsets.sun} />
-      <IconMovement icon={<Moon {...props} />} offset={offsets.moon} />
+      {!isSSR && (
+        <>
+          <IconMovement
+            icon={<Desktop {...props} />}
+            offset={offsets.desktop}
+          />
+          <IconMovement icon={<Sun {...props} />} offset={offsets.sun} />
+          <IconMovement icon={<Moon {...props} />} offset={offsets.moon} />
+        </>
+      )}
     </div>
   );
 };

+ 2 - 0
packages/component-library/src/InfoBox/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import clsx from "clsx";
 import type { ComponentProps, ReactNode } from "react";
 

+ 2 - 0
packages/component-library/src/Link/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import clsx from "clsx";
 import type { ComponentProps, ElementType } from "react";
 

+ 172 - 47
packages/component-library/src/ModalDialog/index.tsx

@@ -16,6 +16,8 @@ import {
   useState,
   useEffect,
   useRef,
+  useMemo,
+  useReducer,
 } from "react";
 import type { ModalRenderProps } from "react-aria-components";
 import {
@@ -106,6 +108,7 @@ type OwnProps = Pick<ComponentProps<typeof Modal>, "children"> &
       | undefined;
     onClose?: (() => void) | undefined;
     onCloseFinish?: (() => void) | undefined;
+    onOpenFinish?: (() => void) | undefined;
     onDragEnd?: (
       e: MouseEvent | TouchEvent | PointerEvent,
       panInfo: PanInfo,
@@ -121,6 +124,7 @@ export const ModalDialog = ({
   onOpenChange,
   onClose,
   onCloseFinish,
+  onOpenFinish,
   overlayClassName,
   overlayVariants,
   children,
@@ -145,6 +149,8 @@ export const ModalDialog = ({
     if (animation === "hidden") {
       hideOverlay();
       onCloseFinish?.();
+    } else if (animation === "visible") {
+      onOpenFinish?.();
     }
     setAnimation((a) => {
       return animation === "hidden" && a === "hidden" ? "unmounted" : a;
@@ -204,77 +210,196 @@ export const createModalDialogContext = <
 ) => {
   type ContextType = {
     close: () => Promise<void>;
-    open: (modalDialogProps: OpenArgs<T, U>) => void;
+    open: (modalDialogProps: ModalDialogProps<T, U>) => void;
   };
 
   const Context = createContext<ContextType | undefined>(undefined);
 
+  enum StateType {
+    Closed,
+    Opening,
+    Open,
+    Closing,
+    Replacing,
+  }
+  const State = {
+    Closed: () => ({ type: StateType.Closed as const }),
+    Opening: (props: ModalDialogProps<T, U>) => ({
+      type: StateType.Opening as const,
+      props,
+    }),
+    Open: (props: ModalDialogProps<T, U>) => ({
+      type: StateType.Open as const,
+      props,
+    }),
+    Closing: (props: ModalDialogProps<T, U>) => ({
+      type: StateType.Closing as const,
+      props,
+    }),
+    Replacing: (
+      oldProps: ModalDialogProps<T, U>,
+      newProps: ModalDialogProps<T, U>,
+    ) => ({ type: StateType.Replacing as const, oldProps, newProps }),
+  };
+  type State = ReturnType<(typeof State)[keyof typeof State]>;
+
+  enum ActionType {
+    Open,
+    OpenFinish,
+    Close,
+    CloseFinish,
+  }
+  const Action = {
+    Open: (props: ModalDialogProps<T, U>) => ({
+      type: ActionType.Open as const,
+      props,
+    }),
+    OpenFinish: () => ({ type: ActionType.OpenFinish as const }),
+    Close: () => ({ type: ActionType.Close as const }),
+    CloseFinish: () => ({ type: ActionType.CloseFinish as const }),
+  };
+  type Action = ReturnType<(typeof Action)[keyof typeof Action]>;
+
+  const reducer = (state: State, action: Action) => {
+    switch (action.type) {
+      case ActionType.Open: {
+        switch (state.type) {
+          case StateType.Closed: {
+            return State.Opening(action.props);
+          }
+          case StateType.Closing:
+          case StateType.Open:
+          case StateType.Opening: {
+            return State.Replacing(state.props, action.props);
+          }
+          case StateType.Replacing: {
+            return State.Replacing(state.oldProps, action.props);
+          }
+        }
+      }
+      // This rule is a false positive because typescript ensures we never
+      // fallthough, and adding a `break` above triggers an unreachable code
+      // error
+      // eslint-disable-next-line no-fallthrough
+      case ActionType.OpenFinish: {
+        switch (state.type) {
+          case StateType.Opening: {
+            return State.Open(state.props);
+          }
+          case StateType.Closed:
+          case StateType.Closing:
+          case StateType.Replacing:
+          case StateType.Open: {
+            return state;
+          }
+        }
+      }
+      // This rule is a false positive because typescript ensures we never
+      // fallthough, and adding a `break` above triggers an unreachable code
+      // error
+      // eslint-disable-next-line no-fallthrough
+      case ActionType.Close: {
+        switch (state.type) {
+          case StateType.Open:
+          case StateType.Opening: {
+            return State.Closing(state.props);
+          }
+          case StateType.Replacing: {
+            return State.Closing(state.oldProps);
+          }
+          case StateType.Closed:
+          case StateType.Closing: {
+            return state;
+          }
+        }
+      }
+      // This rule is a false positive because typescript ensures we never
+      // fallthough, and adding a `break` above triggers an unreachable code
+      // error
+      // eslint-disable-next-line no-fallthrough
+      case ActionType.CloseFinish: {
+        switch (state.type) {
+          case StateType.Closing: {
+            return State.Closed();
+          }
+          case StateType.Replacing: {
+            return State.Opening(state.newProps);
+          }
+          case StateType.Closed:
+          case StateType.Open:
+          case StateType.Opening: {
+            return state;
+          }
+        }
+      }
+    }
+  };
+
   return {
     Provider: ({ children, ...ctxProps }: U & { children: ReactNode }) => {
-      const promiseCloseResolvers = useRef<(() => void)[]>([]);
-      const [isOpen, setIsOpen] = useState(false);
-      const [currentModalDialog, setModalDialog] = useState<
-        OpenArgs<T, U> | undefined
-      >(undefined);
-      const close = useCallback(() => {
-        setIsOpen(false);
-        return new Promise<void>((resolve) => {
-          promiseCloseResolvers.current.push(resolve);
-        });
-      }, []);
-      const open = useCallback(
-        (props: OpenArgs<T, U>) => {
-          if (currentModalDialog && currentModalDialog !== props) {
-            close()
-              .then(() => {
-                setTimeout(() => {
-                  setModalDialog(props);
-                  setIsOpen(true);
-                });
-              })
-              .catch((error: unknown) => {
-                throw error;
-              });
-          } else if (!currentModalDialog) {
-            setModalDialog(props);
-            setIsOpen(true);
-          }
-        },
-        [currentModalDialog, setModalDialog, close],
+      const closeResolvers = useRef<(() => void)[]>([]);
+      const [state, dispatch] = useReducer<State, [Action]>(
+        reducer,
+        State.Closed(),
       );
-      const handleOpenChange = useCallback(
-        (newValue: boolean) => {
-          if (!newValue) {
-            setIsOpen(false);
-          }
+      const open = useCallback(
+        (props: ModalDialogProps<T, U>) => {
+          dispatch(Action.Open(props));
         },
-        [setIsOpen],
+        [dispatch],
       );
+      const close = useCallback(() => {
+        dispatch(Action.Close());
+        return new Promise<void>((resolve) =>
+          closeResolvers.current.push(resolve),
+        );
+      }, [dispatch]);
+      const value = useMemo(() => ({ open, close }), [open, close]);
+      const handleOpenFinish = useCallback(() => {
+        dispatch(Action.OpenFinish());
+      }, [dispatch]);
       const handleCloseFinish = useCallback(() => {
-        const onCloseFinished = currentModalDialog?.onCloseFinished;
-        setModalDialog(undefined);
+        let onCloseFinished;
+        if (state.type === StateType.Closing) {
+          onCloseFinished = state.props.onCloseFinished;
+        }
+        dispatch(Action.CloseFinish());
         onCloseFinished?.();
-        for (const resolver of promiseCloseResolvers.current) {
+        for (const resolver of closeResolvers.current) {
           resolver();
         }
-        promiseCloseResolvers.current = [];
-      }, [setModalDialog, currentModalDialog]);
+        closeResolvers.current = [];
+      }, [dispatch, state]);
+      const handleOpenChange = useCallback(
+        (isOpen: boolean) => {
+          if (!isOpen) {
+            dispatch(Action.Close());
+          }
+        },
+        [dispatch],
+      );
 
       return (
-        <Context value={{ open, close }}>
+        <Context value={value}>
           {children}
-          {currentModalDialog !== undefined && (
+          {state.type !== StateType.Closed && (
             // @ts-expect-error TODO typescript isn't validating this type
             // properly.  To be honest, I'm not sure why, but the code for
             // `createModalDialogContext` is pretty messy and I think
             // simplifying this would probably resolve the issue.  I'll come
             // back and refactor this eventually and see if this goes away...
             <Component
-              isOpen={isOpen}
+              isOpen={
+                state.type === StateType.Open ||
+                state.type === StateType.Opening
+              }
               onOpenChange={handleOpenChange}
+              onOpenFinish={handleOpenFinish}
               onCloseFinish={handleCloseFinish}
               {...ctxProps}
-              {...currentModalDialog}
+              {...(state.type === StateType.Replacing
+                ? state.oldProps
+                : state.props)}
             />
           )}
         </Context>
@@ -292,7 +417,7 @@ export const createModalDialogContext = <
   };
 };
 
-export type OpenArgs<T, U = undefined> = Omit<
+export type ModalDialogProps<T, U = undefined> = Omit<
   T,
   "isOpen" | "onOpenChange" | "onCloseFinish" | keyof U
 > & {

+ 3 - 1
packages/component-library/src/NotFoundPage/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import { MagnifyingGlass } from "@phosphor-icons/react/dist/ssr/MagnifyingGlass";
 
 import { Button } from "../Button";
@@ -9,7 +11,7 @@ export const NotFoundPage = () => (
       <MagnifyingGlass />
     </div>
     <div className={styles.text}>
-      <h1 className={styles.header}>Not Found</h1>
+      <h2 className={styles.header}>Not Found</h2>
       <p className={styles.subheader}>
         {"The page you're looking for isn't here"}
       </p>

+ 2 - 2
packages/component-library/src/Paginator/index.tsx

@@ -107,7 +107,7 @@ const PaginatorToolbar = ({
     <Toolbar aria-label="Page" className={styles.paginatorToolbar ?? ""}>
       <PageSelector
         hideText
-        beforeIcon={CaretLeft}
+        beforeIcon={<CaretLeft />}
         isDisabled={currentPage === 1}
         page={1}
         onPageChange={onPageChange}
@@ -132,7 +132,7 @@ const PaginatorToolbar = ({
       })}
       <PageSelector
         hideText
-        beforeIcon={CaretRight}
+        beforeIcon={<CaretRight />}
         isDisabled={currentPage === numPages}
         page={numPages}
         onPageChange={onPageChange}

+ 1 - 3
packages/component-library/src/Select/index.tsx

@@ -88,9 +88,7 @@ export const Select = <T extends { id: string | number }>({
   >
     <Label className={styles.label}>{label}</Label>
     <Button
-      afterIcon={({ className }) => (
-        <DropdownCaretDown className={clsx(styles.caret, className)} />
-      )}
+      afterIcon={<DropdownCaretDown className={styles.caret} />}
       variant={variant}
       size={size}
       rounded={rounded}

+ 2 - 0
packages/component-library/src/Skeleton/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import clsx from "clsx";
 import type { ComponentProps, CSSProperties } from "react";
 

+ 2 - 0
packages/component-library/src/StatCard/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import clsx from "clsx";
 import type { ReactNode, ElementType } from "react";
 

+ 9 - 1
packages/component-library/src/Table/index.module.scss

@@ -105,10 +105,17 @@
 
         &[data-sticky-header] {
           position: sticky;
-          top: theme.$header-height;
           z-index: 1;
         }
 
+        &[data-sticky-header="top"] {
+          top: 0;
+        }
+
+        &[data-sticky-header="appHeader"] {
+          top: theme.$header-height;
+        }
+
         &[data-sticky] {
           z-index: 2;
         }
@@ -178,6 +185,7 @@
         .cell {
           transition: background-color 100ms linear;
           border-bottom: 1px solid theme.color("background", "secondary");
+          isolation: isolate;
         }
 
         &[data-hovered] .cell {

+ 4 - 5
packages/component-library/src/Table/index.tsx

@@ -25,7 +25,7 @@ export type { SortDescriptor } from "../unstyled/Table/index.jsx";
 type TableProps<T extends string> = ComponentProps<typeof UnstyledTable> & {
   className?: string | undefined;
   headerCellClassName?: string | undefined;
-  stickyHeader?: boolean | undefined;
+  stickyHeader?: "top" | "appHeader" | undefined;
   fill?: boolean | undefined;
   rounded?: boolean | undefined;
   label: string;
@@ -110,7 +110,7 @@ export const Table = <T extends string>({
         <TableHeader columns={columns} className={styles.tableHeader ?? ""}>
           {(columnConfig: ColumnConfig<T>) => (
             <Column
-              data-sticky-header={stickyHeader === undefined ? undefined : ""}
+              data-sticky-header={stickyHeader}
               {...columnConfig}
               {...cellProps(columnConfig, headerCellClassName)}
             >
@@ -129,12 +129,11 @@ export const Table = <T extends string>({
                             : "ascending",
                         );
                       }}
-                      beforeIcon={(props) => (
+                      beforeIcon={
                         <svg
                           xmlns="http://www.w3.org/2000/svg"
                           viewBox="0 0 16 16"
                           fill="currentColor"
-                          {...props}
                         >
                           <path
                             className={styles.ascending}
@@ -145,7 +144,7 @@ export const Table = <T extends string>({
                             d="m10.677 9.927-2.5 2.5a.25.25 0 0 1-.354 0l-2.5-2.5A.25.25 0 0 1 5.5 9.5h5a.25.25 0 0 1 .177.427Z"
                           />
                         </svg>
-                      )}
+                      }
                       hideText
                     >
                       Sort

+ 5 - 5
packages/component-library/src/social-links.ts → packages/component-library/src/social-links.tsx

@@ -7,27 +7,27 @@ import { YoutubeLogo } from "@phosphor-icons/react/dist/ssr/YoutubeLogo";
 export const socialLinks = [
   {
     name: "Discord",
-    icon: DiscordLogo,
+    icon: <DiscordLogo />,
     href: "https://discord.gg/invite/PythNetwork",
   },
   {
     name: "X",
-    icon: XLogo,
+    icon: <XLogo />,
     href: "https://x.com/PythNetwork",
   },
   {
     name: "Telegram",
-    icon: TelegramLogo,
+    icon: <TelegramLogo />,
     href: "https://t.me/Pyth_Network",
   },
   {
     name: "GitHub",
-    icon: GithubLogo,
+    icon: <GithubLogo />,
     href: "https://github.com/pyth-network",
   },
   {
     name: "Youtube",
-    icon: YoutubeLogo,
+    icon: <YoutubeLogo />,
     href: "https://www.youtube.com/channel/UCjCkvPN9ohl0UDvldfn1neg",
   },
 ];

+ 3 - 3
packages/component-library/src/useAlert/index.tsx

@@ -7,7 +7,7 @@ import { Heading } from "react-aria-components";
 
 import styles from "./index.module.scss";
 import { Button } from "../Button/index.jsx";
-import type { OpenArgs } from "../ModalDialog/index.jsx";
+import type { ModalDialogProps } from "../ModalDialog/index.jsx";
 import {
   ModalDialog,
   createModalDialogContext,
@@ -54,7 +54,7 @@ const Alert = ({
   >
     <Button
       className={styles.closeButton ?? ""}
-      beforeIcon={(props) => <XCircle weight="fill" {...props} />}
+      beforeIcon={<XCircle weight="fill" />}
       slot="close"
       hideText
       rounded
@@ -75,4 +75,4 @@ const { Provider, useValue } = createModalDialogContext<Props>(Alert);
 
 export const AlertProvider = Provider;
 export const useAlert = useValue;
-export type OpenAlertArgs = OpenArgs<Props>;
+export type OpenAlertArgs = ModalDialogProps<Props>;

+ 5 - 0
packages/component-library/src/useDrawer/index.stories.tsx

@@ -39,6 +39,11 @@ const meta = {
         category: "Behavior",
       },
     },
+    onCloseFinished: {
+      table: {
+        category: "Behavior",
+      },
+    },
   },
 } satisfies Meta<typeof OpenButton>;
 export default meta;

+ 3 - 3
packages/component-library/src/useDrawer/index.tsx

@@ -9,7 +9,7 @@ import { Heading } from "react-aria-components";
 
 import styles from "./index.module.scss";
 import { Button } from "../Button/index.jsx";
-import type { OpenArgs } from "../ModalDialog/index.jsx";
+import type { ModalDialogProps } from "../ModalDialog/index.jsx";
 import {
   ModalDialog,
   createModalDialogContext,
@@ -115,7 +115,7 @@ const Drawer = ({
             {headingExtra}
             <Button
               className={styles.closeButton ?? ""}
-              beforeIcon={(props) => <XCircle weight="fill" {...props} />}
+              beforeIcon={<XCircle weight="fill" />}
               slot="close"
               hideText
               rounded
@@ -270,7 +270,7 @@ const { Provider, useValue } = createModalDialogContext<
 
 export const DrawerProvider = Provider;
 export const useDrawer = useValue;
-export type OpenDrawerArgs = OpenArgs<
+export type OpenDrawerArgs = ModalDialogProps<
   Props,
   Pick<Props, "setMainContentOffset">
 >;

+ 2 - 1
packages/component-library/tsconfig.build.json

@@ -3,7 +3,8 @@
   "compilerOptions": {
     "noEmit": false,
     "incremental": false,
-    "declaration": true
+    "declaration": true,
+    "emitDeclarationOnly": true
   },
   "exclude": ["node_modules", "dist", ".storybook", "**/*.stories.tsx"]
 }

+ 6 - 0
packages/component-library/turbo.json

@@ -6,11 +6,17 @@
       "dependsOn": [
         "//#install:modules",
         "^build",
+        "build:declarations",
         "build:esm",
         "build:scss",
         "build:svg"
       ]
     },
+    "build:declarations": {
+      "dependsOn": ["//#install:modules", "^build"],
+      "inputs": ["src/**/*.ts", "src/**/*.tsx"],
+      "outputs": ["dist/**/*.d.ts"]
+    },
     "build:scss": {
       "dependsOn": ["//#install:modules"],
       "inputs": ["src/**/*.scss"],

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 314 - 81
pnpm-lock.yaml


+ 6 - 2
pnpm-workspace.yaml

@@ -48,6 +48,9 @@ catalog:
   "@amplitude/analytics-browser": ^2.13.0
   "@amplitude/plugin-autocapture-browser": ^1.0.0
   "@axe-core/react": ^4.10.1
+  "@babel/cli": ^7.27.2
+  "@babel/core": ^7.27.1
+  "@babel/preset-typescript": ^7.27.1
   "@bonfida/spl-name-service": ^3.0.10
   "@clickhouse/client": ^1.11.0
   "@coral-xyz/anchor": ^0.30.1
@@ -58,7 +61,7 @@ catalog:
   "@floating-ui/react": ^0.27.6
   "@headlessui/react": ^2.2.0
   "@heroicons/react": ^2.2.0
-  "@next/third-parties": ^15.3.1
+  "@next/third-parties": ^15.3.2
   "@phosphor-icons/react": ^2.1.7
   "@pythnetwork/client": ^2.22.1
   "@pythnetwork/pyth-sdk-solidity": ^4.0.0
@@ -86,6 +89,7 @@ catalog:
   "@types/react-dom": ^19.1.1
   "@vercel/functions": ^2.0.0
   autoprefixer: ^10.4.21
+  babel-plugin-react-compiler: 19.1.0-rc.1
   bcp-47: ^2.1.0
   bs58: ^6.0.0
   class-variance-authority: ^0.7.1
@@ -104,7 +108,7 @@ catalog:
   lucide-react: ^0.487.0
   modern-normalize: ^3.0.1
   motion: ^12.9.2
-  next: ^15.3.1
+  next: ^15.3.2
   next-themes: ^0.4.6
   nuqs: ^2.4.1
   pino: ^9.6.0

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است