ソースを参照

Merge branch 'main' of github.com:pyth-network/pyth-crosschain into tb/argus/internal-interfaces

Tejas Badadare 6 ヶ月 前
コミット
12b5998cb7
100 ファイル変更1072 行追加803 行削除
  1. 0 3
      .github/CODEOWNERS
  2. 6 0
      apps/api-reference/src/evm-networks.ts
  3. 0 3
      apps/fortuna/src/config.rs
  4. 1 9
      apps/fortuna/src/keeper.rs
  5. 3 45
      apps/fortuna/src/keeper/block.rs
  6. 0 3
      apps/insights/package.json
  7. 1 1
      apps/insights/src/app/global-error.tsx
  8. 1 1
      apps/insights/src/components/CopyButton/index.tsx
  9. 1 1
      apps/insights/src/components/Error/index.tsx
  10. 1 1
      apps/insights/src/components/PriceComponentDrawer/index.tsx
  11. 5 0
      apps/insights/src/components/PriceComponentsCard/index.module.scss
  12. 2 3
      apps/insights/src/components/PriceComponentsCard/index.tsx
  13. 1 1
      apps/insights/src/components/PriceFeed/chart.tsx
  14. 4 0
      apps/insights/src/components/PriceFeed/header.module.scss
  15. 3 1
      apps/insights/src/components/PriceFeed/header.tsx
  16. 1 1
      apps/insights/src/components/PriceFeed/publishers-card.tsx
  17. 1 1
      apps/insights/src/components/PriceFeeds/asset-class-table.tsx
  18. 5 0
      apps/insights/src/components/PriceFeeds/price-feeds-card.module.scss
  19. 2 3
      apps/insights/src/components/PriceFeeds/price-feeds-card.tsx
  20. 2 3
      apps/insights/src/components/Publishers/index.module.scss
  21. 5 0
      apps/insights/src/components/Publishers/publishers-card.module.scss
  22. 2 3
      apps/insights/src/components/Publishers/publishers-card.tsx
  23. 0 70
      apps/insights/src/components/Root/header.tsx
  24. 0 47
      apps/insights/src/components/Root/index.module.scss
  25. 25 42
      apps/insights/src/components/Root/index.tsx
  26. 0 4
      apps/insights/src/components/Root/logo.svg
  27. 0 30
      apps/insights/src/components/Root/mobile-menu.module.scss
  28. 0 57
      apps/insights/src/components/Root/mobile-menu.tsx
  29. 17 7
      apps/insights/src/components/Root/search-button.module.scss
  30. 57 47
      apps/insights/src/components/Root/search-button.tsx
  31. 0 73
      apps/insights/src/components/Root/support-drawer.module.scss
  32. 0 33
      apps/insights/src/components/Root/tabs.tsx
  33. 1 1
      apps/insights/src/components/TokenIcon/index.tsx
  34. 4 0
      apps/insights/src/components/TokenIcon/logo.svg
  35. 1 1
      apps/insights/src/hooks/use-data.ts
  36. 1 1
      apps/insights/src/hooks/use-live-price-data.tsx
  37. 1 1
      apps/insights/src/hooks/use-query-param-filter-pagination.ts
  38. 4 0
      apps/insights/src/static-data/stats.tsx
  39. 3 2
      apps/price_pusher/package.json
  40. 10 13
      apps/price_pusher/src/aptos/balance-tracker.ts
  41. 2 8
      apps/price_pusher/src/sui/balance-tracker.ts
  42. 25 0
      contract_manager/store/chains/EvmChains.yaml
  43. 15 0
      contract_manager/store/contracts/EvmPriceFeedContracts.yaml
  44. 15 0
      contract_manager/store/contracts/EvmWormholeContracts.yaml
  45. 5 0
      governance/xc_admin/packages/xc_admin_common/src/chains.ts
  46. 0 2
      packages/app-logger/.prettierignore
  47. 0 1
      packages/app-logger/README.md
  48. 0 1
      packages/app-logger/eslint.config.js
  49. 0 1
      packages/app-logger/jest.config.js
  50. 0 36
      packages/app-logger/package.json
  51. 0 1
      packages/app-logger/prettier.config.js
  52. 0 8
      packages/app-logger/src/context.ts
  53. 0 19
      packages/app-logger/src/index.tsx
  54. 0 26
      packages/app-logger/src/provider.tsx
  55. 0 3
      packages/app-logger/tsconfig.json
  56. 36 6
      packages/component-library/.storybook/main.ts
  57. 74 23
      packages/component-library/.storybook/preview.tsx
  58. 23 15
      packages/component-library/.storybook/storybook.module.scss
  59. 11 2
      packages/component-library/package.json
  60. 0 0
      packages/component-library/src/AppShell/amplitude.tsx
  61. 1 0
      packages/component-library/src/AppShell/base.scss
  62. 34 0
      packages/component-library/src/AppShell/body-providers.tsx
  63. 0 0
      packages/component-library/src/AppShell/fonts.tsx
  64. 1 2
      packages/component-library/src/AppShell/html-with-lang.tsx
  65. 0 0
      packages/component-library/src/AppShell/i18n-provider.tsx
  66. 66 0
      packages/component-library/src/AppShell/index.module.scss
  67. 51 0
      packages/component-library/src/AppShell/index.stories.tsx
  68. 107 0
      packages/component-library/src/AppShell/index.tsx
  69. 2 1
      packages/component-library/src/AppShell/report-accessibility.ts
  70. 0 0
      packages/component-library/src/AppShell/router-provider.tsx
  71. 22 0
      packages/component-library/src/AppShell/tabs.tsx
  72. 6 0
      packages/component-library/src/Card/index.stories.tsx
  73. 1 1
      packages/component-library/src/CrossfadeTabPanels/index.tsx
  74. 1 2
      packages/component-library/src/Footer/index.module.scss
  75. 16 0
      packages/component-library/src/Footer/index.stories.tsx
  76. 6 6
      packages/component-library/src/Footer/index.tsx
  77. 0 0
      packages/component-library/src/Footer/wordmark.svg
  78. 103 15
      packages/component-library/src/Header/index.module.scss
  79. 36 0
      packages/component-library/src/Header/index.stories.tsx
  80. 120 9
      packages/component-library/src/Header/index.tsx
  81. 4 0
      packages/component-library/src/Header/logo.svg
  82. 1 1
      packages/component-library/src/Header/theme-switch.module.scss
  83. 0 0
      packages/component-library/src/Header/theme-switch.tsx
  84. 0 19
      packages/component-library/src/Html/base.scss
  85. 0 9
      packages/component-library/src/Html/index.tsx
  86. 0 12
      packages/component-library/src/MainContent/index.module.scss
  87. 0 30
      packages/component-library/src/MainContent/index.tsx
  88. 5 9
      packages/component-library/src/MainNavTabs/index.stories.tsx
  89. 15 5
      packages/component-library/src/MainNavTabs/index.tsx
  90. 1 1
      packages/component-library/src/MobileNavTabs/index.module.scss
  91. 28 0
      packages/component-library/src/MobileNavTabs/index.stories.tsx
  92. 11 14
      packages/component-library/src/MobileNavTabs/index.tsx
  93. 3 0
      packages/component-library/src/Paginator/index.stories.tsx
  94. 3 0
      packages/component-library/src/StatCard/index.stories.tsx
  95. 3 0
      packages/component-library/src/Table/index.stories.tsx
  96. 0 0
      packages/component-library/src/compose-providers.tsx
  97. 0 0
      packages/component-library/src/social-links.ts
  98. 2 0
      packages/component-library/src/theme.scss
  99. 3 3
      packages/component-library/src/useDrawer/index.module.scss
  100. 43 0
      packages/component-library/src/useLogger/index.tsx

+ 0 - 3
.github/CODEOWNERS

@@ -2,11 +2,8 @@ apps/api-reference @pyth-network/web-team
 apps/entropy-debugger @pyth-network/web-team
 apps/insights @pyth-network/web-team
 apps/staking @pyth-network/web-team
-packages/app-logger @pyth-network/web-team
 packages/component-library @pyth-network/web-team
-packages/fonts @pyth-network/web-team
 packages/known-publishers @pyth-network/web-team
-packages/next-root @pyth-network/web-team
 Dockerfile.node @pyth-network/web-team
 package.json @pyth-network/web-team
 pnpm-workspace.yaml @pyth-network/web-team

+ 6 - 0
apps/api-reference/src/evm-networks.ts

@@ -893,6 +893,12 @@ export const NETWORK_INFO = {
     isMainnet: true,
     contractAddress: "0x2880aB155794e7179c9eE2e38200202908C17B43",
   },
+  [146]: {
+    name: "sonic_mainnet",
+    rpcUrl: " https://rpc.soniclabs.com",
+    isMainnet: true,
+    contractAddress: "0x2880aB155794e7179c9eE2e38200202908C17B43",
+  },
 } satisfies Record<number, NetworkInfo>;
 
 export const NETWORK_IDS = Object.keys(NETWORK_INFO).map((key) =>

+ 0 - 3
apps/fortuna/src/config.rs

@@ -110,9 +110,6 @@ pub struct EthereumConfig {
     /// TODO: Change type from String to Url
     pub geth_rpc_addr: String,
 
-    /// URL of a Geth RPC wss endpoint to use for subscribing to blockchain events.
-    pub geth_rpc_wss: Option<String>,
-
     /// Address of a Pyth Randomness contract to interact with.
     pub contract_addr: Address,
 

+ 1 - 9
apps/fortuna/src/keeper.rs

@@ -99,15 +99,7 @@ pub async fn run_keeper_threads(
 
     let (tx, rx) = mpsc::channel::<BlockRange>(1000);
     // Spawn a thread to watch for new blocks and send the range of blocks for which events has not been handled to the `tx` channel.
-    spawn(
-        watch_blocks_wrapper(
-            chain_state.clone(),
-            latest_safe_block,
-            tx,
-            chain_eth_config.geth_rpc_wss.clone(),
-        )
-        .in_current_span(),
-    );
+    spawn(watch_blocks_wrapper(chain_state.clone(), latest_safe_block, tx).in_current_span());
 
     // Spawn a thread for block processing with configured delays
     spawn(

+ 3 - 45
apps/fortuna/src/keeper/block.rs

@@ -6,12 +6,8 @@ use {
         keeper::keeper_metrics::KeeperMetrics,
         keeper::process_event::process_event_with_backoff,
     },
-    anyhow::{anyhow, Result},
-    ethers::{
-        providers::{Middleware, Provider, Ws},
-        types::U256,
-    },
-    futures::StreamExt,
+    anyhow::Result,
+    ethers::types::U256,
     std::{collections::HashSet, sync::Arc},
     tokio::{
         spawn,
@@ -176,7 +172,6 @@ pub async fn watch_blocks_wrapper(
     chain_state: BlockchainState,
     latest_safe_block: BlockNumber,
     tx: mpsc::Sender<BlockRange>,
-    geth_rpc_wss: Option<String>,
 ) {
     let mut last_safe_block_processed = latest_safe_block;
     loop {
@@ -184,7 +179,6 @@ pub async fn watch_blocks_wrapper(
             chain_state.clone(),
             &mut last_safe_block_processed,
             tx.clone(),
-            geth_rpc_wss.clone(),
         )
         .in_current_span()
         .await
@@ -203,47 +197,11 @@ pub async fn watch_blocks(
     chain_state: BlockchainState,
     last_safe_block_processed: &mut BlockNumber,
     tx: mpsc::Sender<BlockRange>,
-    geth_rpc_wss: Option<String>,
 ) -> Result<()> {
     tracing::info!("Watching blocks to handle new events");
 
-    let provider_option = match geth_rpc_wss {
-        Some(wss) => Some(match Provider::<Ws>::connect(wss.clone()).await {
-            Ok(provider) => provider,
-            Err(e) => {
-                tracing::error!("Error while connecting to wss: {}. error: {:?}", wss, e);
-                return Err(e.into());
-            }
-        }),
-        None => {
-            tracing::info!("No wss provided");
-            None
-        }
-    };
-
-    let mut stream_option = match provider_option {
-        Some(ref provider) => Some(match provider.subscribe_blocks().await {
-            Ok(client) => client,
-            Err(e) => {
-                tracing::error!("Error while subscribing to blocks. error {:?}", e);
-                return Err(e.into());
-            }
-        }),
-        None => None,
-    };
-
     loop {
-        match stream_option {
-            Some(ref mut stream) => {
-                if stream.next().await.is_none() {
-                    tracing::error!("Error blocks subscription stream ended");
-                    return Err(anyhow!("Error blocks subscription stream ended"));
-                }
-            }
-            None => {
-                time::sleep(POLL_INTERVAL).await;
-            }
-        }
+        time::sleep(POLL_INTERVAL).await;
 
         let latest_safe_block = get_latest_safe_block(&chain_state).in_current_span().await;
         if latest_safe_block > *last_safe_block_processed {

+ 0 - 3
apps/insights/package.json

@@ -22,13 +22,10 @@
   "dependencies": {
     "@clickhouse/client": "catalog:",
     "@phosphor-icons/react": "catalog:",
-    "@pythnetwork/app-logger": "workspace:*",
     "@pythnetwork/client": "catalog:",
     "@pythnetwork/component-library": "workspace:*",
-    "@pythnetwork/fonts": "workspace:*",
     "@pythnetwork/hermes-client": "workspace:*",
     "@pythnetwork/known-publishers": "workspace:*",
-    "@pythnetwork/next-root": "workspace:*",
     "@react-hookz/web": "catalog:",
     "@solana/web3.js": "catalog:",
     "bs58": "catalog:",

+ 1 - 1
apps/insights/src/app/global-error.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import { LoggerProvider } from "@pythnetwork/app-logger/provider";
+import { LoggerProvider } from "@pythnetwork/component-library/useLogger";
 import type { ComponentProps } from "react";
 
 import { Error } from "../components/Error";

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

@@ -2,8 +2,8 @@
 
 import { Check } from "@phosphor-icons/react/dist/ssr/Check";
 import { Copy } from "@phosphor-icons/react/dist/ssr/Copy";
-import { useLogger } from "@pythnetwork/app-logger";
 import { Button } from "@pythnetwork/component-library/unstyled/Button";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import clsx from "clsx";
 import type { ComponentProps } from "react";
 import { useCallback, useEffect, useState } from "react";

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

@@ -1,6 +1,6 @@
 import { Warning } from "@phosphor-icons/react/dist/ssr/Warning";
-import { useLogger } from "@pythnetwork/app-logger";
 import { Button } from "@pythnetwork/component-library/Button";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import { useEffect } from "react";
 
 import styles from "./index.module.scss";

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

@@ -1,6 +1,5 @@
 import { ArrowSquareOut } from "@phosphor-icons/react/dist/ssr/ArrowSquareOut";
 import { Flask } from "@phosphor-icons/react/dist/ssr/Flask";
-import { useLogger } from "@pythnetwork/app-logger";
 import type { Props as ButtonProps } from "@pythnetwork/component-library/Button";
 import { Button } from "@pythnetwork/component-library/Button";
 import { Card } from "@pythnetwork/component-library/Card";
@@ -11,6 +10,7 @@ import { StatCard } from "@pythnetwork/component-library/StatCard";
 import { Table } from "@pythnetwork/component-library/Table";
 import type { Button as UnstyledButton } from "@pythnetwork/component-library/unstyled/Button";
 import { useDrawer } from "@pythnetwork/component-library/useDrawer";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import { useMountEffect } from "@react-hookz/web";
 import dynamic from "next/dynamic";
 import { useRouter } from "next/navigation";

+ 5 - 0
apps/insights/src/components/PriceComponentsCard/index.module.scss

@@ -87,3 +87,8 @@
     }
   }
 }
+
+:export {
+  // stylelint-disable-next-line property-no-unknown
+  headerHeight: theme.$header-height;
+}

+ 2 - 3
apps/insights/src/components/PriceComponentsCard/index.tsx

@@ -1,6 +1,5 @@
 "use client";
 
-import { useLogger } from "@pythnetwork/app-logger";
 import { Badge } from "@pythnetwork/component-library/Badge";
 import { Button } from "@pythnetwork/component-library/Button";
 import { Card } from "@pythnetwork/component-library/Card";
@@ -14,6 +13,7 @@ import type {
   SortDescriptor,
 } from "@pythnetwork/component-library/Table";
 import { Table } from "@pythnetwork/component-library/Table";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import clsx from "clsx";
 import { useQueryState, parseAsStringEnum, parseAsBoolean } from "nuqs";
 import type { ReactNode } from "react";
@@ -37,7 +37,6 @@ import { LivePrice, LiveConfidence, LiveComponentValue } from "../LivePrices";
 import { NoResults } from "../NoResults";
 import { usePriceComponentDrawer } from "../PriceComponentDrawer";
 import { PriceName } from "../PriceName";
-import rootStyles from "../Root/index.module.scss";
 import { Score } from "../Score";
 import { Status as StatusComponent } from "../Status";
 
@@ -490,7 +489,7 @@ export const PriceComponentsCardContents = <
         label={label}
         fill
         rounded
-        stickyHeader={rootStyles.headerHeight}
+        stickyHeader={styles.headerHeight}
         className={styles.table ?? ""}
         columns={[
           {

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

@@ -1,6 +1,6 @@
 "use client";
 
-import { useLogger } from "@pythnetwork/app-logger";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import { useResizeObserver } from "@react-hookz/web";
 import type { IChartApi, ISeriesApi, UTCTimestamp } from "lightweight-charts";
 import { LineSeries, LineStyle, createChart } from "lightweight-charts";

+ 4 - 0
apps/insights/src/components/PriceFeed/header.module.scss

@@ -19,6 +19,10 @@
     gap: theme.spacing(2);
     justify-content: space-between;
 
+    .assetClassBadge {
+      align-self: start;
+    }
+
     @include theme.breakpoint("sm") {
       flex-flow: row nowrap;
       align-items: center;

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

@@ -65,7 +65,9 @@ const PriceFeedHeaderImpl = (props: PriceFeedHeaderImplProps) => (
       {props.isLoading ? (
         <Skeleton width={15} />
       ) : (
-        <AssetClassBadge>{props.feed.product.asset_type}</AssetClassBadge>
+        <AssetClassBadge className={styles.assetClassBadge}>
+          {props.feed.product.asset_type}
+        </AssetClassBadge>
       )}
     </div>
     <div className={styles.headerRow}>

+ 1 - 1
apps/insights/src/components/PriceFeed/publishers-card.tsx

@@ -1,7 +1,7 @@
 "use client";
 
-import { useLogger } from "@pythnetwork/app-logger";
 import { Switch } from "@pythnetwork/component-library/Switch";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import { useQueryState, parseAsBoolean } from "nuqs";
 import { Suspense, useCallback, useMemo } from "react";
 

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

@@ -1,9 +1,9 @@
 "use client";
 
-import { useLogger } from "@pythnetwork/app-logger";
 import { Badge } from "@pythnetwork/component-library/Badge";
 import { Table } from "@pythnetwork/component-library/Table";
 import { useDrawer } from "@pythnetwork/component-library/useDrawer";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import { usePathname } from "next/navigation";
 import {
   parseAsString,

+ 5 - 0
apps/insights/src/components/PriceFeeds/price-feeds-card.module.scss

@@ -21,3 +21,8 @@
     }
   }
 }
+
+:export {
+  // stylelint-disable-next-line property-no-unknown
+  headerHeight: theme.$header-height;
+}

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

@@ -1,7 +1,6 @@
 "use client";
 
 import { ChartLine } from "@phosphor-icons/react/dist/ssr/ChartLine";
-import { useLogger } from "@pythnetwork/app-logger";
 import { Badge } from "@pythnetwork/component-library/Badge";
 import { Card } from "@pythnetwork/component-library/Card";
 import { Paginator } from "@pythnetwork/component-library/Paginator";
@@ -12,6 +11,7 @@ import type {
   SortDescriptor,
 } from "@pythnetwork/component-library/Table";
 import { Table } from "@pythnetwork/component-library/Table";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import { useQueryState, parseAsString } from "nuqs";
 import type { ReactNode } from "react";
 import { Suspense, useCallback, useMemo } from "react";
@@ -32,7 +32,6 @@ import {
 import { NoResults } from "../NoResults";
 import { PriceFeedTag } from "../PriceFeedTag";
 import { PriceName } from "../PriceName";
-import rootStyles from "../Root/index.module.scss";
 
 type Props = {
   id: string;
@@ -317,7 +316,7 @@ const PriceFeedsCardContents = ({ id, ...props }: PriceFeedsCardContents) => (
       rounded
       fill
       label="Price Feeds"
-      stickyHeader={rootStyles.headerHeight}
+      stickyHeader={styles.headerHeight}
       className={styles.table ?? ""}
       columns={[
         {

+ 2 - 3
apps/insights/src/components/Publishers/index.module.scss

@@ -1,5 +1,4 @@
 @use "@pythnetwork/component-library/theme";
-@use "../Root/index.module.scss" as root;
 
 $gap: theme.spacing(4);
 
@@ -53,7 +52,7 @@ $gap: theme.spacing(4);
     .statCard {
       @include theme.breakpoint("2xl") {
         position: sticky;
-        top: root.$header-height;
+        top: theme.$header-height;
       }
     }
 
@@ -83,7 +82,7 @@ $gap: theme.spacing(4);
         $card-wrapper-p: (2 * theme.spacing(1));
         $card-height: $card-content + $card-pt + $card-pb + $card-wrapper-p;
 
-        top: calc(root.$header-height + $gap + $card-height);
+        top: calc(theme.$header-height + $gap + $card-height);
       }
 
       .oisPool {

+ 5 - 0
apps/insights/src/components/Publishers/publishers-card.module.scss

@@ -39,3 +39,8 @@
     }
   }
 }
+
+:export {
+  // stylelint-disable-next-line property-no-unknown
+  headerHeight: theme.$header-height;
+}

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

@@ -2,7 +2,6 @@
 
 import { Broadcast } from "@phosphor-icons/react/dist/ssr/Broadcast";
 import { Database } from "@phosphor-icons/react/dist/ssr/Database";
-import { useLogger } from "@pythnetwork/app-logger";
 import { Badge } from "@pythnetwork/component-library/Badge";
 import { Card } from "@pythnetwork/component-library/Card";
 import { Link } from "@pythnetwork/component-library/Link";
@@ -14,6 +13,7 @@ import type {
   SortDescriptor,
 } from "@pythnetwork/component-library/Table";
 import { Table } from "@pythnetwork/component-library/Table";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import clsx from "clsx";
 import { useQueryState, parseAsStringEnum } from "nuqs";
 import type { ReactNode } from "react";
@@ -32,7 +32,6 @@ import {
 import { NoResults } from "../NoResults";
 import { PublisherTag } from "../PublisherTag";
 import { Ranking } from "../Ranking";
-import rootStyles from "../Root/index.module.scss";
 import { Score } from "../Score";
 
 const PUBLISHER_SCORE_WIDTH = 38;
@@ -324,7 +323,7 @@ const PublishersCardContents = ({
       rounded
       fill
       label="Publishers"
-      stickyHeader={rootStyles.headerHeight}
+      stickyHeader={styles.headerHeight}
       className={styles.table ?? ""}
       columns={[
         {

+ 0 - 70
apps/insights/src/components/Root/header.tsx

@@ -1,70 +0,0 @@
-import { Lifebuoy } from "@phosphor-icons/react/dist/ssr/Lifebuoy";
-import { Button } from "@pythnetwork/component-library/Button";
-import { Link } from "@pythnetwork/component-library/Link";
-import clsx from "clsx";
-import type { ComponentProps } from "react";
-
-import styles from "./header.module.scss";
-import Logo from "./logo.svg";
-import { MobileMenu } from "./mobile-menu";
-import { SearchButton, SearchShortcutText } from "./search-button";
-import { SupportDrawer } from "./support-drawer";
-import { MainNavTabs } from "./tabs";
-import { ThemeSwitch } from "./theme-switch";
-
-type Props = ComponentProps<"header"> & {
-  tabs: ComponentProps<typeof MainNavTabs>["items"];
-};
-
-export const Header = ({ className, tabs, ...props }: Props) => (
-  <header className={clsx(styles.header, className)} {...props}>
-    <div className={styles.content}>
-      <div className={styles.leftMenu}>
-        <Link href="/" className={styles.logoLink ?? ""}>
-          <div className={styles.logoWrapper}>
-            <Logo className={styles.logo} />
-          </div>
-          <div className={styles.logoLabel}>Pyth Homepage</div>
-        </Link>
-        <div className={styles.appName}>Insights</div>
-        <MainNavTabs className={styles.mainNavTabs ?? ""} items={tabs} />
-      </div>
-      <div className={styles.rightMenu}>
-        <Button
-          variant="ghost"
-          size="sm"
-          rounded
-          beforeIcon={Lifebuoy}
-          drawer={SupportDrawer}
-          className={styles.supportButton ?? ""}
-        >
-          Support
-        </Button>
-        <SearchButton
-          className={styles.outlineSearchButton ?? ""}
-          variant="outline"
-        >
-          <SearchShortcutText />
-        </SearchButton>
-        <SearchButton
-          className={styles.ghostSearchButton ?? ""}
-          hideText
-          variant="ghost"
-        >
-          Search
-        </SearchButton>
-        <MobileMenu className={styles.mobileMenu} />
-        <Button
-          href="https://docs.pyth.network"
-          size="sm"
-          rounded
-          target="_blank"
-          className={styles.mainCta ?? ""}
-        >
-          Dev Docs
-        </Button>
-        <ThemeSwitch className={styles.themeSwitch ?? ""} />
-      </div>
-    </div>
-  </header>
-);

+ 0 - 47
apps/insights/src/components/Root/index.module.scss

@@ -1,47 +0,0 @@
-@use "@pythnetwork/component-library/theme";
-
-$header-height: var(--header-height);
-
-:export {
-  // stylelint-disable-next-line property-no-unknown
-  headerHeight: $header-height;
-}
-
-.root {
-  scroll-padding-top: $header-height;
-
-  --header-height: #{theme.spacing(18)};
-
-  @include theme.breakpoint("md") {
-    --header-height: #{theme.spacing(20)};
-  }
-
-  .tabRoot {
-    display: grid;
-    min-height: 100dvh;
-    grid-template-rows: auto 1fr auto;
-    grid-template-columns: 100%;
-
-    .main {
-      isolation: isolate;
-      padding-top: theme.spacing(4);
-      min-height: calc(100svh - $header-height);
-
-      @include theme.breakpoint("sm") {
-        min-height: unset;
-        padding-top: theme.spacing(6);
-      }
-    }
-
-    .header {
-      z-index: 1;
-      height: $header-height;
-    }
-  }
-
-  .mobileNavTabs {
-    @include theme.breakpoint("sm") {
-      display: none;
-    }
-  }
-}

+ 25 - 42
apps/insights/src/components/Root/index.tsx

@@ -1,13 +1,9 @@
+import { AppShell } from "@pythnetwork/component-library/AppShell";
 import { lookup as lookupPublisher } from "@pythnetwork/known-publishers";
-import { Root as BaseRoot } from "@pythnetwork/next-root";
 import { NuqsAdapter } from "nuqs/adapters/next/app";
 import type { ReactNode } from "react";
+import { Suspense } from "react";
 
-import { Footer } from "./footer";
-import { Header } from "./header";
-import styles from "./index.module.scss";
-import { MobileNavTabs } from "./mobile-nav-tabs";
-import { TabRoot, TabPanel } from "./tabs";
 import {
   ENABLE_ACCESSIBILITY_REPORTING,
   GOOGLE_ANALYTICS_ID,
@@ -18,46 +14,37 @@ import { getPublishers } from "../../services/clickhouse";
 import { Cluster, getFeeds } from "../../services/pyth";
 import { PriceFeedIcon } from "../PriceFeedIcon";
 import { PublisherIcon } from "../PublisherIcon";
-import { SearchButtonProvider as SearchButtonProviderImpl } from "./search-button";
+import { SearchButton as SearchButtonImpl } from "./search-button";
 
 export const TABS = [
-  { href: "/", id: "", children: "Overview" },
-  { href: "/publishers", id: "publishers", children: "Publishers" },
-  {
-    href: "/price-feeds",
-    id: "price-feeds",
-    children: "Price Feeds",
-  },
+  { segment: "", children: "Overview" },
+  { segment: "publishers", children: "Publishers" },
+  { segment: "price-feeds", children: "Price Feeds" },
 ];
 
 type Props = {
   children: ReactNode;
 };
 
-export const Root = ({ children }: Props) => {
-  return (
-    <BaseRoot
-      amplitudeApiKey={AMPLITUDE_API_KEY}
-      googleAnalyticsId={GOOGLE_ANALYTICS_ID}
-      enableAccessibilityReporting={ENABLE_ACCESSIBILITY_REPORTING}
-      providers={[NuqsAdapter, LivePriceDataProvider]}
-      className={styles.root}
-    >
-      <SearchButtonProvider>
-        <TabRoot className={styles.tabRoot ?? ""}>
-          <Header className={styles.header} tabs={TABS} />
-          <main className={styles.main}>
-            <TabPanel>{children}</TabPanel>
-          </main>
-          <Footer />
-          <MobileNavTabs tabs={TABS} className={styles.mobileNavTabs} />
-        </TabRoot>
-      </SearchButtonProvider>
-    </BaseRoot>
-  );
-};
+export const Root = ({ children }: Props) => (
+  <AppShell
+    appName="Insights"
+    amplitudeApiKey={AMPLITUDE_API_KEY}
+    googleAnalyticsId={GOOGLE_ANALYTICS_ID}
+    enableAccessibilityReporting={ENABLE_ACCESSIBILITY_REPORTING}
+    providers={[NuqsAdapter, LivePriceDataProvider]}
+    tabs={TABS}
+    extraCta={
+      <Suspense fallback={<SearchButtonImpl isLoading />}>
+        <SearchButton />
+      </Suspense>
+    }
+  >
+    {children}
+  </AppShell>
+);
 
-const SearchButtonProvider = async ({ children }: { children: ReactNode }) => {
+const SearchButton = async () => {
   const [publishers, feeds] = await Promise.all([
     Promise.all([
       getPublishersForSearchDialog(Cluster.Pythnet),
@@ -66,11 +53,7 @@ const SearchButtonProvider = async ({ children }: { children: ReactNode }) => {
     getFeedsForSearchDialog(Cluster.Pythnet),
   ]);
 
-  return (
-    <SearchButtonProviderImpl publishers={publishers.flat()} feeds={feeds}>
-      {children}
-    </SearchButtonProviderImpl>
-  );
+  return <SearchButtonImpl publishers={publishers.flat()} feeds={feeds} />;
 };
 
 const getPublishersForSearchDialog = async (cluster: Cluster) => {

+ 0 - 4
apps/insights/src/components/Root/logo.svg

@@ -1,4 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32.3 41" fill="currentColor">
-<path d="M19.9998 16.5133C19.9998 18.7239 18.2087 20.5163 15.9998 20.5163V24.5193C20.4177 24.5193 23.9998 20.9346 23.9998 16.5133C23.9998 12.0921 20.4177 8.50732 15.9998 8.50732C14.5434 8.50732 13.1757 8.89658 11.9998 9.57914C9.60808 10.9624 7.99976 13.5496 7.99976 16.5133V36.5283L11.5963 40.1276L11.9998 40.5313V16.5133C11.9998 14.3027 13.7908 12.5103 15.9998 12.5103C18.2087 12.5103 19.9998 14.3027 19.9998 16.5133Z"/>
-<path d="M16 0.501953C13.0855 0.501953 10.3537 1.28228 8 2.64558C6.49299 3.51643 5.14337 4.62626 4 5.92438C1.51063 8.74694 0 12.4548 0 16.514V28.523L4 32.526V16.514C4 12.9582 5.545 9.76263 8 7.56288C9.15423 6.5309 10.5093 5.71618 12 5.19113C13.2501 4.74575 14.5979 4.50496 16 4.50496C22.6269 4.50496 28 9.88212 28 16.514C28 23.1458 22.6269 28.523 16 28.523V32.526C24.8376 32.526 32 25.3564 32 16.514C32 7.67151 24.8376 0.501953 16 0.501953Z"/>
-</svg>

+ 0 - 30
apps/insights/src/components/Root/mobile-menu.module.scss

@@ -1,30 +0,0 @@
-@use "@pythnetwork/component-library/theme";
-
-.mobileMenu {
-  display: flex;
-  flex-flow: column nowrap;
-  align-items: stretch;
-  gap: theme.spacing(6);
-  justify-content: space-between;
-
-  .buttons {
-    display: flex;
-    flex-flow: column nowrap;
-    align-items: stretch;
-    gap: theme.spacing(6);
-  }
-
-  .theme {
-    display: flex;
-    flex-flow: row nowrap;
-    justify-content: flex-end;
-    align-items: center;
-    gap: theme.spacing(2);
-
-    .themeLabel {
-      @include theme.text("sm", "normal");
-
-      color: theme.color("muted");
-    }
-  }
-}

+ 0 - 57
apps/insights/src/components/Root/mobile-menu.tsx

@@ -1,57 +0,0 @@
-import { Lifebuoy } from "@phosphor-icons/react/dist/ssr/Lifebuoy";
-import { List } from "@phosphor-icons/react/dist/ssr/List";
-import { Button } from "@pythnetwork/component-library/Button";
-
-import styles from "./mobile-menu.module.scss";
-import { SupportDrawer } from "./support-drawer";
-import { ThemeSwitch } from "./theme-switch";
-
-type Props = {
-  className?: string | undefined;
-};
-
-export const MobileMenu = ({ className }: Props) => (
-  <Button
-    className={className ?? ""}
-    beforeIcon={List}
-    variant="ghost"
-    size="sm"
-    rounded
-    hideText
-    drawer={{
-      hideHeading: true,
-      title: "Menu",
-      contents: <MobileMenuContents />,
-    }}
-  >
-    Menu
-  </Button>
-);
-
-const MobileMenuContents = () => (
-  <div className={styles.mobileMenu}>
-    <div className={styles.buttons}>
-      <Button
-        variant="ghost"
-        size="md"
-        rounded
-        beforeIcon={Lifebuoy}
-        drawer={SupportDrawer}
-      >
-        Support
-      </Button>
-      <Button
-        href="https://docs.pyth.network"
-        size="md"
-        rounded
-        target="_blank"
-      >
-        Dev Docs
-      </Button>
-    </div>
-    <div className={styles.theme}>
-      <span className={styles.themeLabel}>Theme</span>
-      <ThemeSwitch />
-    </div>
-  </div>
-);

+ 17 - 7
apps/insights/src/components/Root/search-button.module.scss

@@ -1,11 +1,27 @@
 @use "@pythnetwork/component-library/theme";
 
+.searchButton {
+  .largeScreenSearchButton {
+    display: none;
+
+    @include theme.breakpoint("md") {
+      display: unset;
+    }
+  }
+
+  .smallScreenSearchButton {
+    @include theme.breakpoint("md") {
+      display: none;
+    }
+  }
+}
+
 .searchDialogContents {
   gap: theme.spacing(1);
   display: flex;
   flex-flow: column nowrap;
   overflow: hidden;
-  max-height: 100%;
+  max-height: theme.spacing(120);
   min-height: 0;
 
   .searchBar,
@@ -178,9 +194,3 @@
     }
   }
 }
-
-// stylelint-disable property-no-unknown
-:export {
-  breakpoint-sm: theme.map-get-strict(theme.$breakpoints, "sm");
-}
-// stylelint-enable property-no-unknown

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

@@ -2,7 +2,6 @@
 
 import { MagnifyingGlass } from "@phosphor-icons/react/dist/ssr/MagnifyingGlass";
 import { XCircle } from "@phosphor-icons/react/dist/ssr/XCircle";
-import { useLogger } from "@pythnetwork/app-logger";
 import { Badge } from "@pythnetwork/component-library/Badge";
 import type { Props as ButtonProps } from "@pythnetwork/component-library/Button";
 import { Button } from "@pythnetwork/component-library/Button";
@@ -19,15 +18,9 @@ import {
   ListBoxItem,
 } from "@pythnetwork/component-library/unstyled/ListBox";
 import { useDrawer } from "@pythnetwork/component-library/useDrawer";
-import type { ReactNode, ComponentProps } from "react";
-import {
-  useMemo,
-  useCallback,
-  useEffect,
-  useState,
-  createContext,
-  use,
-} from "react";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
+import type { ReactNode } from "react";
+import { useMemo, useCallback, useEffect, useState } from "react";
 import { useIsSSR, useCollator, useFilter } from "react-aria";
 
 import styles from "./search-button.module.scss";
@@ -40,9 +33,18 @@ import { Score } from "../Score";
 
 const INPUTS = new Set(["input", "select", "button", "textarea"]);
 
-const SearchButtonContext = createContext<undefined | (() => void)>(undefined);
+type Props =
+  | { isLoading: true }
+  | (ResolvedSearchButtonProps & { isLoading?: false | undefined });
+
+export const SearchButton = (props: Props) =>
+  props.isLoading ? (
+    <SearchButtonImpl isPending />
+  ) : (
+    <ResolvedSearchButton {...props} />
+  );
 
-type Props = Omit<ComponentProps<typeof SearchButtonContext>, "value"> & {
+type ResolvedSearchButtonProps = {
   feeds: {
     symbol: string;
     displaySymbol: string;
@@ -60,11 +62,43 @@ type Props = Omit<ComponentProps<typeof SearchButtonContext>, "value"> & {
   ))[];
 };
 
-export const SearchButtonProvider = ({
-  feeds,
-  publishers,
-  ...props
-}: Props) => {
+const ResolvedSearchButton = (props: ResolvedSearchButtonProps) => {
+  const openSearchDrawer = useSearchDrawer(props);
+
+  useSearchHotkey(openSearchDrawer);
+
+  return <SearchButtonImpl onPress={openSearchDrawer} />;
+};
+
+const SearchButtonImpl = (
+  props: Omit<ButtonProps<typeof UnstyledButton>, "children">,
+) => (
+  <div className={styles.searchButton}>
+    <Button
+      className={styles.largeScreenSearchButton ?? ""}
+      variant="outline"
+      beforeIcon={MagnifyingGlass}
+      size="sm"
+      rounded
+      {...props}
+    >
+      <SearchShortcutText />
+    </Button>
+    <Button
+      className={styles.smallScreenSearchButton ?? ""}
+      hideText
+      variant="ghost"
+      beforeIcon={MagnifyingGlass}
+      size="sm"
+      rounded
+      {...props}
+    >
+      Search
+    </Button>
+  </div>
+);
+
+const useSearchDrawer = ({ feeds, publishers }: ResolvedSearchButtonProps) => {
   const drawer = useDrawer();
 
   const searchDrawer = useMemo(
@@ -82,6 +116,10 @@ export const SearchButtonProvider = ({
     drawer.open(searchDrawer);
   }, [drawer, searchDrawer]);
 
+  return openSearchDrawer;
+};
+
+const useSearchHotkey = (openSearchDrawer: () => void) => {
   const handleKeyDown = useCallback(
     (event: KeyboardEvent) => {
       const activeElement = document.activeElement;
@@ -110,34 +148,9 @@ export const SearchButtonProvider = ({
       globalThis.removeEventListener("keydown", handleKeyDown);
     };
   }, [handleKeyDown]);
-
-  return <SearchButtonContext value={openSearchDrawer} {...props} />;
-};
-
-export const SearchButton = (
-  props: Omit<
-    ButtonProps<typeof UnstyledButton>,
-    "beforeIcon" | "size" | "rounded" | "onPress"
-  >,
-) => {
-  const openSearchDrawer = use(SearchButtonContext);
-  if (openSearchDrawer) {
-    return (
-      <Button
-        className={styles.outlineSearchButton ?? ""}
-        beforeIcon={MagnifyingGlass}
-        size="sm"
-        rounded
-        onPress={openSearchDrawer}
-        {...props}
-      />
-    );
-  } else {
-    throw new Error("Search drawer context not initialized!");
-  }
 };
 
-export const SearchShortcutText = () => {
+const SearchShortcutText = () => {
   const isSSR = useIsSSR();
   return isSSR ? <Skeleton width={7} /> : <SearchTextImpl />;
 };
@@ -147,10 +160,7 @@ const SearchTextImpl = () => {
   return isMac ? "⌘ K" : "Ctrl K";
 };
 
-type SearchDialogContentsProps = {
-  feeds: Props["feeds"];
-  publishers: Props["publishers"];
-};
+type SearchDialogContentsProps = ResolvedSearchButtonProps;
 
 const SearchDialogContents = ({
   feeds,

+ 0 - 73
apps/insights/src/components/Root/support-drawer.module.scss

@@ -1,73 +0,0 @@
-@use "@pythnetwork/component-library/theme";
-
-.supportDrawer {
-  display: flex;
-  flex-flow: column nowrap;
-  gap: theme.spacing(8);
-
-  & > * {
-    flex: none;
-  }
-
-  .linkList {
-    display: flex;
-    flex-flow: column nowrap;
-    gap: theme.spacing(4);
-
-    .title {
-      @include theme.text("lg", "medium");
-
-      color: theme.color("heading");
-    }
-
-    .items {
-      list-style-type: none;
-      padding: 0;
-      margin: 0;
-      display: flex;
-      flex-flow: column nowrap;
-      gap: theme.spacing(2);
-
-      .link {
-        padding: theme.spacing(3);
-        display: grid;
-        grid-template-columns: max-content 1fr max-content;
-        grid-template-rows: max-content max-content;
-        text-align: left;
-        gap: theme.spacing(2) theme.spacing(4);
-        align-items: center;
-        width: 100%;
-
-        .icon {
-          font-size: theme.spacing(8);
-          color: theme.color("states", "data", "normal");
-          grid-row: span 2 / span 2;
-          display: grid;
-          place-content: center;
-        }
-
-        .header {
-          @include theme.text("sm", "medium");
-
-          color: theme.color("heading");
-        }
-
-        .description {
-          @include theme.text("xs", "normal");
-
-          color: theme.color("muted");
-          grid-column: 2;
-          grid-row: 2;
-          text-overflow: ellipsis;
-          overflow: hidden;
-        }
-
-        .caret {
-          color: theme.color("states", "data", "normal");
-          font-size: theme.spacing(4);
-          grid-row: span 2 / span 2;
-        }
-      }
-    }
-  }
-}

+ 0 - 33
apps/insights/src/components/Root/tabs.tsx

@@ -1,33 +0,0 @@
-"use client";
-
-import { MainNavTabs as MainNavTabsComponent } from "@pythnetwork/component-library/MainNavTabs";
-import {
-  TabPanel as UnstyledTabPanel,
-  Tabs,
-} from "@pythnetwork/component-library/unstyled/Tabs";
-import { useSelectedLayoutSegment, usePathname } from "next/navigation";
-import type { ComponentProps } from "react";
-
-export const TabRoot = (
-  props: Omit<ComponentProps<typeof Tabs>, "selectedKey">,
-) => {
-  const tabId = useSelectedLayoutSegment() ?? "";
-
-  return <Tabs selectedKey={tabId} {...props} />;
-};
-
-export const MainNavTabs = (
-  props: Omit<ComponentProps<typeof MainNavTabsComponent>, "pathname">,
-) => {
-  const pathname = usePathname();
-
-  return <MainNavTabsComponent pathname={pathname} {...props} />;
-};
-
-export const TabPanel = (
-  props: Omit<ComponentProps<typeof UnstyledTabPanel>, "id">,
-) => {
-  const tabId = useSelectedLayoutSegment() ?? "";
-
-  return <UnstyledTabPanel key="tabpanel" id={tabId} {...props} />;
-};

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

@@ -4,7 +4,7 @@ import clsx from "clsx";
 import type { ComponentProps } from "react";
 
 import styles from "./index.module.scss";
-import Logo from "../Root/logo.svg";
+import Logo from "./logo.svg";
 
 type Props = Omit<ComponentProps<"span">, "children">;
 

+ 4 - 0
apps/insights/src/components/TokenIcon/logo.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32.3 41" fill="currentColor">
+  <path d="M19.9998 16.5133C19.9998 18.7239 18.2087 20.5163 15.9998 20.5163V24.5193C20.4177 24.5193 23.9998 20.9346 23.9998 16.5133C23.9998 12.0921 20.4177 8.50732 15.9998 8.50732C14.5434 8.50732 13.1757 8.89658 11.9998 9.57914C9.60808 10.9624 7.99976 13.5496 7.99976 16.5133V36.5283L11.5963 40.1276L11.9998 40.5313V16.5133C11.9998 14.3027 13.7908 12.5103 15.9998 12.5103C18.2087 12.5103 19.9998 14.3027 19.9998 16.5133Z"/>
+  <path d="M16 0.501953C13.0855 0.501953 10.3537 1.28228 8 2.64558C6.49299 3.51643 5.14337 4.62626 4 5.92438C1.51063 8.74694 0 12.4548 0 16.514V28.523L4 32.526V16.514C4 12.9582 5.545 9.76263 8 7.56288C9.15423 6.5309 10.5093 5.71618 12 5.19113C13.2501 4.74575 14.5979 4.50496 16 4.50496C22.6269 4.50496 28 9.88212 28 16.514C28 23.1458 22.6269 28.523 16 28.523V32.526C24.8376 32.526 32 25.3564 32 16.514C32 7.67151 24.8376 0.501953 16 0.501953Z"/>
+</svg>

+ 1 - 1
apps/insights/src/hooks/use-data.ts

@@ -1,4 +1,4 @@
-import { useLogger } from "@pythnetwork/app-logger";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import { useCallback } from "react";
 import type { KeyedMutator } from "swr";
 import useSWR from "swr";

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

@@ -1,7 +1,7 @@
 "use client";
 
-import { useLogger } from "@pythnetwork/app-logger";
 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";

+ 1 - 1
apps/insights/src/hooks/use-query-param-filter-pagination.ts

@@ -1,7 +1,7 @@
 "use client";
 
-import { useLogger } from "@pythnetwork/app-logger";
 import type { SortDescriptor } from "@pythnetwork/component-library/unstyled/Table";
+import { useLogger } from "@pythnetwork/component-library/useLogger";
 import { usePathname } from "next/navigation";
 import {
   parseAsString,

+ 4 - 0
apps/insights/src/static-data/stats.tsx

@@ -39,6 +39,7 @@ export const totalVolumeTraded = [
   { date: new Date("30 Jan '25"), volume: 132_060_000_000 },
   { date: new Date("27 Feb '25"), volume: 92_160_000_000 },
   { date: new Date("28 Feb '25"), volume: 75_250_000_000 },
+  { date: new Date("29 Apr '25"), volume: 67_140_000_000 },
 ];
 
 export const activeChains = [
@@ -72,6 +73,7 @@ export const activeChains = [
   { date: new Date("30 Dec '24"), chains: 93 },
   { date: new Date("30 Jan '25"), chains: 97 },
   { date: new Date("27 Feb '25"), chains: 100 },
+  { date: new Date("29 Apr '25"), chains: 101 },
 ];
 
 export const activePublishers = [
@@ -120,6 +122,7 @@ export const activePublishers = [
   { date: new Date("31 Dec '24"), numPublishers: 122 },
   { date: new Date("31 Jan '25"), numPublishers: 123 },
   { date: new Date("28 Feb '25"), numPublishers: 124 },
+  { date: new Date("30 Apr '25"), numPublishers: 125 },
 ];
 
 export const activeFeeds = [
@@ -162,4 +165,5 @@ export const activeFeeds = [
   { date: new Date("30 Jan '25"), numFeeds: 613 },
   { date: new Date("27 Feb '25"), numFeeds: 1296 },
   { date: new Date("30 Mar '25"), numFeeds: 1321 },
+  { date: new Date("29 Apr '25"), numFeeds: 1418 },
 ];

+ 3 - 2
apps/price_pusher/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/price-pusher",
-  "version": "9.3.1",
+  "version": "9.3.3",
   "description": "Pyth Price Pusher",
   "homepage": "https://pyth.network",
   "main": "lib/index.js",
@@ -59,10 +59,11 @@
     "typescript": "catalog:"
   },
   "dependencies": {
+    "@aptos-labs/ts-sdk": "^1.39.0",
     "@coral-xyz/anchor": "^0.30.0",
     "@injectivelabs/networks": "1.14.47",
-    "@injectivelabs/utils": "^1.14.48",
     "@injectivelabs/sdk-ts": "1.14.50",
+    "@injectivelabs/utils": "^1.14.48",
     "@mysten/sui": "^1.3.0",
     "@pythnetwork/hermes-client": "^1.3.1",
     "@pythnetwork/price-service-sdk": "workspace:^",

+ 10 - 13
apps/price_pusher/src/aptos/balance-tracker.ts

@@ -1,4 +1,4 @@
-import { AptosClient } from "aptos";
+import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";
 import {
   BaseBalanceTracker,
   BaseBalanceTrackerConfig,
@@ -24,7 +24,7 @@ export interface AptosBalanceTrackerConfig extends BaseBalanceTrackerConfig {
  * Aptos-specific implementation of the balance tracker
  */
 export class AptosBalanceTracker extends BaseBalanceTracker {
-  private client: AptosClient;
+  private client: Aptos;
   private aptosAddress: string;
   private decimals: number;
 
@@ -33,8 +33,9 @@ export class AptosBalanceTracker extends BaseBalanceTracker {
       ...config,
       logger: config.logger.child({ module: "AptosBalanceTracker" }),
     });
-
-    this.client = new AptosClient(config.endpoint);
+    this.client = new Aptos(
+      new AptosConfig({ network: Network.CUSTOM, fullnode: config.endpoint }),
+    );
     this.aptosAddress = config.address;
     // APT has 8 decimal places by default
     this.decimals = config.decimals ?? 8;
@@ -47,16 +48,12 @@ export class AptosBalanceTracker extends BaseBalanceTracker {
   protected async updateBalance(): Promise<void> {
     try {
       // Get account resource to check the balance
-      const accountResource = await this.client.getAccountResource(
-        this.aptosAddress,
-        "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
-      );
-
-      // Extract the balance value from the account resource
-      const rawBalance = (accountResource.data as any).coin.value;
+      const accountAPTAmount = await this.client.getAccountAPTAmount({
+        accountAddress: this.aptosAddress,
+      });
 
-      // Convert the balance to a bigint
-      const balance = BigInt(rawBalance);
+      // Convert the amount to a bigint
+      const balance = BigInt(accountAPTAmount);
 
       // Calculate the normalized balance for display
       const normalizedBalance = Number(balance) / Math.pow(10, this.decimals);

+ 2 - 8
apps/price_pusher/src/sui/balance-tracker.ts

@@ -36,18 +36,12 @@ export class SuiBalanceTracker extends BaseBalanceTracker {
    */
   protected async updateBalance(): Promise<void> {
     try {
-      // Get all coins owned by the address
-      const { data: coins } = await this.client.getCoins({
+      const balance = await this.client.getBalance({
         owner: this.address,
       });
 
-      // Sum up all coin balances
-      const totalBalance = coins.reduce((acc, coin) => {
-        return acc + BigInt(coin.balance);
-      }, BigInt(0));
-
       // Convert to a normalized number for reporting (SUI has 9 decimals)
-      const normalizedBalance = Number(totalBalance) / 1e9;
+      const normalizedBalance = Number(balance.totalBalance) / 1e9;
 
       this.metrics.updateWalletBalance(
         this.address,

+ 25 - 0
contract_manager/store/chains/EvmChains.yaml

@@ -884,3 +884,28 @@
   rpcUrl: https://carrot.megaeth.com/rpc
   networkId: 6342
   type: EvmChain
+- id: converge_testnet
+  mainnet: false
+  rpcUrl: https://rpc-converge-testnet-0.t.conduit.xyz
+  networkId: 52085144
+  type: EvmChain
+- id: worldchain
+  mainnet: true
+  rpcUrl: https://worldchain-mainnet.g.alchemy.com/public
+  networkId: 480
+  type: EvmChain
+- id: worldchain_testnet
+  mainnet: false
+  rpcUrl: https://worldchain-sepolia.g.alchemy.com/public
+  networkId: 4801
+  type: EvmChain
+- id: swellchain_testnet
+  mainnet: false
+  rpcUrl: https://swell-testnet.alt.technology
+  networkId: 1924
+  type: EvmChain
+- id: swellchain
+  mainnet: true
+  rpcUrl: https://swell-mainnet.alt.technology
+  networkId: 1923
+  type: EvmChain

+ 15 - 0
contract_manager/store/contracts/EvmPriceFeedContracts.yaml

@@ -496,3 +496,18 @@
 - chain: megaeth_testnet
   address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
   type: EvmPriceFeedContract
+- chain: converge_testnet
+  address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
+  type: EvmPriceFeedContract
+- chain: worldchain
+  address: "0xe9d69CdD6Fe41e7B621B4A688C5D1a68cB5c8ADc"
+  type: EvmPriceFeedContract
+- chain: swellchain_testnet
+  address: "0x26DD80569a8B23768A1d80869Ed7339e07595E85"
+  type: EvmPriceFeedContract
+- chain: swellchain
+  address: "0xDd24F84d36BF92C65F92307595335bdFab5Bbd21"
+  type: EvmPriceFeedContract
+- chain: worldchain_testnet
+  address: "0x2880aB155794e7179c9eE2e38200202908C17B43"
+  type: EvmPriceFeedContract

+ 15 - 0
contract_manager/store/contracts/EvmWormholeContracts.yaml

@@ -490,3 +490,18 @@
 - chain: megaeth_testnet
   address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
   type: EvmWormholeContract
+- chain: converge_testnet
+  address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
+  type: EvmWormholeContract
+- chain: worldchain
+  address: "0x66E9cBa5529824a03B5Bc9931d9c63637101D0F7"
+  type: EvmWormholeContract
+- chain: swellchain
+  address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509"
+  type: EvmWormholeContract
+- chain: swellchain_testnet
+  address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320"
+  type: EvmWormholeContract
+- chain: worldchain_testnet
+  address: "0xb27e5ca259702f209a29225d0eDdC131039C9933"
+  type: EvmWormholeContract

+ 5 - 0
governance/xc_admin/packages/xc_admin_common/src/chains.ts

@@ -109,6 +109,8 @@ export const RECEIVER_CHAINS = {
   hyperevm: 60079,
   bittensor_mainnet: 60080,
   xion: 60081,
+  worldchain: 60082,
+  swellchain: 60083,
 
   // Testnets as a separate chain ids (to use stable data sources and governance for them)
   injective_testnet: 60013,
@@ -236,6 +238,9 @@ export const RECEIVER_CHAINS = {
   iota_sui_testnet: 50118,
   berachain_bepolia: 50119,
   megaeth_testnet: 50120,
+  converge_testnet: 50121,
+  swellchain_testnet: 50122,
+  worldchain_testnet: 50123,
 };
 
 // If there is any overlapping value the receiver chain will replace the wormhole

+ 0 - 2
packages/app-logger/.prettierignore

@@ -1,2 +0,0 @@
-coverage/
-node_modules/

+ 0 - 1
packages/app-logger/README.md

@@ -1 +0,0 @@
-# @pythnetwork/app-logger

+ 0 - 1
packages/app-logger/eslint.config.js

@@ -1 +0,0 @@
-export { react as default } from "@cprussin/eslint-config";

+ 0 - 1
packages/app-logger/jest.config.js

@@ -1 +0,0 @@
-export { base as default } from "@cprussin/jest-config";

+ 0 - 36
packages/app-logger/package.json

@@ -1,36 +0,0 @@
-{
-  "name": "@pythnetwork/app-logger",
-  "version": "0.0.0",
-  "private": true,
-  "type": "module",
-  "exports": {
-    ".": "./src/index.tsx",
-    "./provider": "./src/provider.tsx"
-  },
-  "scripts": {
-    "fix:format": "prettier --write .",
-    "fix:lint": "eslint --fix . --max-warnings 0",
-    "test:format": "prettier --check .",
-    "test:lint": "eslint . --max-warnings 0",
-    "test:types": "tsc"
-  },
-  "peerDependencies": {
-    "react": "catalog:"
-  },
-  "dependencies": {
-    "pino": "catalog:"
-  },
-  "devDependencies": {
-    "@cprussin/eslint-config": "catalog:",
-    "@cprussin/jest-config": "catalog:",
-    "@cprussin/prettier-config": "catalog:",
-    "@cprussin/tsconfig": "catalog:",
-    "@types/jest": "catalog:",
-    "@types/react": "catalog:",
-    "eslint": "catalog:",
-    "jest": "catalog:",
-    "prettier": "catalog:",
-    "react": "catalog:",
-    "typescript": "catalog:"
-  }
-}

+ 0 - 1
packages/app-logger/prettier.config.js

@@ -1 +0,0 @@
-export { base as default } from "@cprussin/prettier-config";

+ 0 - 8
packages/app-logger/src/context.ts

@@ -1,8 +0,0 @@
-"use client";
-
-import type { Logger } from "pino";
-import { createContext } from "react";
-
-export const LoggerContext = createContext<undefined | Logger<string>>(
-  undefined,
-);

+ 0 - 19
packages/app-logger/src/index.tsx

@@ -1,19 +0,0 @@
-import { useContext } from "react";
-
-import { LoggerContext } from "./context.js";
-
-export const useLogger = () => {
-  const logger = useContext(LoggerContext);
-  if (logger) {
-    return logger;
-  } else {
-    throw new LoggerNotInitializedError();
-  }
-};
-
-class LoggerNotInitializedError extends Error {
-  constructor() {
-    super("This component must be contained within a <LoggerProvider>");
-    this.name = "LoggerNotInitializedError";
-  }
-}

+ 0 - 26
packages/app-logger/src/provider.tsx

@@ -1,26 +0,0 @@
-"use client";
-
-import { pino } from "pino";
-import type { ComponentProps } from "react";
-import { useMemo } from "react";
-
-import { LoggerContext } from "./context.js";
-
-type LoggerProviderProps = Omit<
-  ComponentProps<typeof LoggerContext.Provider>,
-  "config" | "value"
-> & {
-  config?: Parameters<typeof pino>[0] | undefined;
-};
-
-export const LoggerProvider = ({ config, ...props }: LoggerProviderProps) => {
-  const logger = useMemo(
-    () =>
-      pino({
-        ...config,
-        browser: { ...config?.browser },
-      }),
-    [config],
-  );
-  return <LoggerContext.Provider value={logger} {...props} />;
-};

+ 0 - 3
packages/app-logger/tsconfig.json

@@ -1,3 +0,0 @@
-{
-  "extends": "@cprussin/tsconfig/react.json"
-}

+ 36 - 6
packages/component-library/.storybook/main.ts

@@ -18,7 +18,13 @@ const config = {
   },
 
   addons: [
-    "@storybook/addon-essentials",
+    {
+      name: "@storybook/addon-essentials",
+      options: {
+        backgrounds: false,
+        measure: false,
+      },
+    },
     "@storybook/addon-themes",
     {
       name: "@storybook/addon-styling-webpack",
@@ -51,14 +57,38 @@ const config = {
     },
   ],
 
-  webpackFinal: (config) => ({
-    ...config,
-    resolve: {
+  webpackFinal: (config) => {
+    config.resolve = {
       ...config.resolve,
       extensionAlias: {
+        ...config.resolve?.extensionAlias,
         ".js": [".js", ".ts", ".tsx"],
       },
-    },
-  }),
+    };
+
+    for (const rule of config.module?.rules ?? []) {
+      if (
+        typeof rule === "object" &&
+        rule !== null &&
+        rule.test instanceof RegExp &&
+        rule.test.test(".svg")
+      ) {
+        rule.exclude = /\.svg$/i;
+      }
+    }
+
+    config.module = {
+      ...config.module,
+      rules: [
+        ...(config.module?.rules ?? []),
+        {
+          test: /\.svg$/i,
+          use: ["@svgr/webpack"],
+        },
+      ],
+    };
+
+    return config;
+  },
 } satisfies StorybookConfig;
 export default config;

+ 74 - 23
packages/component-library/.storybook/preview.tsx

@@ -1,37 +1,88 @@
-import { sans } from "@pythnetwork/fonts";
-import { withThemeByClassName } from "@storybook/addon-themes";
 import type { Preview, Decorator } from "@storybook/react";
-import clsx from "clsx";
+import { useEffect } from "react";
 
-import "../src/Html/base.scss";
 import styles from "./storybook.module.scss";
-import { MainContent } from "../src/MainContent";
+import { BodyProviders } from "../src/AppShell/body-providers.js";
+import { sans } from "../src/AppShell/fonts";
+import { RootProviders } from "../src/AppShell/index.js";
+import shellStyles from "../src/AppShell/index.module.scss";
 
 const preview = {
-  parameters: {
-    layout: "fullscreen",
-    backgrounds: {
-      disable: true,
+  globalTypes: {
+    theme: {
+      description: "Theme",
+      toolbar: {
+        title: "Theme",
+        icon: "sun",
+        items: [
+          { value: "light", title: "Light", icon: "sun" },
+          { value: "dark", title: "Dark", icon: "moon" },
+        ],
+        dynamicTitle: true,
+      },
+    },
+    background: {
+      description: "Background",
+      toolbar: {
+        title: "Background",
+        icon: "switchalt",
+        items: [
+          { value: "primary", title: "Primary", icon: "switchalt" },
+          { value: "secondary", title: "Secondary", icon: "contrast" },
+        ],
+        dynamicTitle: true,
+      },
     },
+  },
+  initialGlobals: {
+    background: "primary",
+    theme: "light",
+  },
+  parameters: {
+    layout: "centered",
     actions: { argTypesRegex: "^on[A-Z].*" },
+    nextjs: {
+      appDirectory: true,
+      navigation: {
+        segments: [],
+      },
+    },
   },
 } satisfies Preview;
 
 export default preview;
 
 export const decorators: Decorator[] = [
-  (Story) => (
-    <MainContent className={clsx(sans.className, styles.mainContent)}>
-      <Story />
-    </MainContent>
-  ),
-  withThemeByClassName({
-    themes: {
-      Light: styles.light ?? "",
-      "Light (Secondary Background)": clsx(styles.light, styles.secondary),
-      Dark: styles.dark ?? "",
-      "Dark (Secondary Background)": clsx(styles.dark, styles.secondary),
-    },
-    defaultTheme: "Light",
-  }),
+  (Story, { globals, parameters }) => {
+    useEffect(() => {
+      document.documentElement.classList.add(
+        sans.className,
+        shellStyles.html ?? "",
+      );
+      document.body.classList.add(shellStyles.body ?? "");
+    }, []);
+    return (
+      <RootProviders>
+        {globals.bare ? (
+          <Story />
+        ) : (
+          <BodyProviders
+            className={styles.contents ?? ""}
+            {...(isValidTheme(globals.theme) && { theme: globals.theme })}
+            {...(typeof parameters.layout === "string" && {
+              "data-layout": parameters.layout,
+            })}
+            {...(typeof globals.background === "string" && {
+              "data-background": globals.background,
+            })}
+          >
+            <Story />
+          </BodyProviders>
+        )}
+      </RootProviders>
+    );
+  },
 ];
+
+const isValidTheme = (theme: unknown): theme is "light" | "dark" =>
+  theme === "light" || theme === "dark";

+ 23 - 15
packages/component-library/.storybook/storybook.module.scss

@@ -1,23 +1,31 @@
 @use "../src/theme.scss";
 
-html,
-body {
-  height: 100%;
-  width: 100%;
+body,
+:global(#storybook-root) {
+  padding: 0 !important;
 }
 
-.light {
-  color-scheme: light;
-}
+.contents {
+  height: 100vh;
+  width: 100vw;
+  color: theme.color("foreground");
+  background: theme.color("background", "primary");
+  display: grid;
+  place-content: center;
+  isolation: isolate;
 
-.dark {
-  color-scheme: dark;
-}
+  &[data-background="secondary"] {
+    background: theme.color("background", "secondary");
+  }
 
-.secondary .mainContent {
-  background: theme.color("background", "secondary");
-}
+  &[data-layout="padded"] {
+    padding: theme.spacing(10);
+    display: block;
+    place-content: unset;
+  }
 
-.mainContent {
-  padding: theme.spacing(10);
+  &[data-layout="fullscreen"] {
+    display: block;
+    place-content: unset;
+  }
 }

+ 11 - 2
packages/component-library/package.json

@@ -26,13 +26,20 @@
     "react": "catalog:"
   },
   "dependencies": {
-    "@pythnetwork/fonts": "workspace:*",
+    "@amplitude/analytics-browser": "catalog:",
+    "@amplitude/plugin-autocapture-browser": "catalog:",
+    "@axe-core/react": "catalog:",
+    "@next/third-parties": "catalog:",
     "@react-hookz/web": "catalog:",
+    "bcp-47": "catalog:",
     "clsx": "catalog:",
     "modern-normalize": "catalog:",
     "motion": "catalog:",
+    "next-themes": "catalog:",
+    "pino": "catalog:",
     "react-aria": "catalog:",
-    "react-aria-components": "catalog:"
+    "react-aria-components": "catalog:",
+    "react-dom": "catalog:"
   },
   "devDependencies": {
     "@cprussin/eslint-config": "catalog:",
@@ -46,8 +53,10 @@
     "@storybook/blocks": "catalog:",
     "@storybook/nextjs": "catalog:",
     "@storybook/react": "catalog:",
+    "@svgr/webpack": "catalog:",
     "@types/jest": "catalog:",
     "@types/react": "catalog:",
+    "@types/react-dom": "catalog:",
     "autoprefixer": "catalog:",
     "css-loader": "catalog:",
     "eslint": "catalog:",

+ 0 - 0
packages/next-root/src/amplitude.tsx → packages/component-library/src/AppShell/amplitude.tsx


+ 1 - 0
packages/component-library/src/AppShell/base.scss

@@ -0,0 +1 @@
+@use "modern-normalize";

+ 34 - 0
packages/component-library/src/AppShell/body-providers.tsx

@@ -0,0 +1,34 @@
+"use client";
+
+import { ThemeProvider } from "next-themes";
+import type { ComponentProps, CSSProperties } from "react";
+import { useState } from "react";
+
+import { OverlayVisibleContext } from "../overlay-visible-context.js";
+import { AlertProvider } from "../useAlert/index.js";
+import { DrawerProvider } from "../useDrawer/index.js";
+
+type TabRootProps = ComponentProps<"div"> & {
+  theme?: "dark" | "light" | undefined;
+};
+
+export const BodyProviders = ({ theme, ...props }: TabRootProps) => {
+  const overlayVisibleState = useState(false);
+  const [offset, setOffset] = useState(0);
+
+  return (
+    <ThemeProvider {...(theme && { forcedTheme: theme })}>
+      <OverlayVisibleContext value={overlayVisibleState}>
+        <AlertProvider>
+          <DrawerProvider setMainContentOffset={setOffset}>
+            <div
+              style={{ "--offset": offset / 100 } as CSSProperties}
+              data-overlay-visible={overlayVisibleState[0] ? "" : undefined}
+              {...props}
+            />
+          </DrawerProvider>
+        </AlertProvider>
+      </OverlayVisibleContext>
+    </ThemeProvider>
+  );
+};

+ 0 - 0
packages/fonts/src/index.ts → packages/component-library/src/AppShell/fonts.tsx


+ 1 - 2
packages/next-root/src/html-with-lang.tsx → packages/component-library/src/AppShell/html-with-lang.tsx

@@ -1,6 +1,5 @@
 "use client";
 
-import { Html } from "@pythnetwork/component-library/Html";
 import type { ComponentProps } from "react";
 import { useLocale } from "react-aria";
 
@@ -8,5 +7,5 @@ type HtmlWithLangProps = Omit<ComponentProps<"html">, "lang">;
 
 export const HtmlWithLang = (props: HtmlWithLangProps) => {
   const locale = useLocale();
-  return <Html lang={locale.locale} dir={locale.direction} {...props} />;
+  return <html lang={locale.locale} dir={locale.direction} {...props} />;
 };

+ 0 - 0
packages/next-root/src/i18n-provider.tsx → packages/component-library/src/AppShell/i18n-provider.tsx


+ 66 - 0
packages/component-library/src/AppShell/index.module.scss

@@ -0,0 +1,66 @@
+@use "../theme";
+
+.html {
+  padding-right: 0 !important;
+
+  --header-height: #{theme.spacing(18)};
+
+  @include theme.breakpoint("md") {
+    --header-height: #{theme.spacing(20)};
+  }
+
+  .body {
+    background: black;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    scroll-behavior: smooth;
+    line-height: 1;
+
+    *::selection {
+      color: theme.color("selection", "foreground");
+      background: theme.color("selection", "background");
+    }
+
+    .appShell {
+      display: grid;
+      grid-template-rows: auto 1fr auto;
+      grid-template-columns: 100%;
+      color: theme.color("foreground");
+      background: theme.color("background", "primary");
+      border-top-left-radius: calc(var(--offset) * theme.border-radius("xl"));
+      border-top-right-radius: calc(var(--offset) * theme.border-radius("xl"));
+      overflow: hidden auto;
+      transform: scale(calc(100% - (var(--offset) * 5%)));
+      height: 100dvh;
+      scrollbar-gutter: stable;
+
+      .header {
+        z-index: 1;
+      }
+
+      .main {
+        isolation: isolate;
+        padding-top: theme.spacing(4);
+
+        @include theme.breakpoint("sm") {
+          min-height: unset;
+          padding-top: theme.spacing(6);
+        }
+      }
+
+      .mainNavTabs {
+        display: none;
+
+        @include theme.breakpoint("sm") {
+          display: flex;
+        }
+      }
+
+      .mobileNavTabs {
+        @include theme.breakpoint("sm") {
+          display: none;
+        }
+      }
+    }
+  }
+}

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

@@ -0,0 +1,51 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { AppBody as AppShellComponent } from "./index.js";
+
+const meta = {
+  component: AppShellComponent,
+  globals: {
+    bare: true,
+    theme: {
+      disable: true,
+    },
+  },
+  parameters: {
+    layout: "fullscreen",
+    themes: {
+      disable: true,
+    },
+  },
+  argTypes: {
+    tabs: {
+      table: {
+        disable: true,
+      },
+    },
+    appName: {
+      control: "text",
+      table: {
+        category: "Contents",
+      },
+    },
+    children: {
+      control: "text",
+      table: {
+        category: "Contents",
+      },
+    },
+  },
+} satisfies Meta<typeof AppShellComponent>;
+export default meta;
+
+export const AppShell = {
+  args: {
+    appName: "Component Library",
+    children: "Hello world!",
+    tabs: [
+      { children: "Home", segment: "" },
+      { children: "Foo", segment: "foo" },
+      { children: "Bar", segment: "bar" },
+    ],
+  },
+} satisfies StoryObj<typeof AppShellComponent>;

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

@@ -0,0 +1,107 @@
+import { GoogleAnalytics } from "@next/third-parties/google";
+import clsx from "clsx";
+import dynamic from "next/dynamic";
+import type { ComponentProps, ReactNode } from "react";
+
+import { Amplitude } from "./amplitude.js";
+import { BodyProviders } from "./body-providers.js";
+import { sans } from "./fonts.js";
+import { HtmlWithLang } from "./html-with-lang.js";
+import { I18nProvider } from "./i18n-provider.js";
+import styles from "./index.module.scss";
+import { TabRoot, TabPanel } from "./tabs";
+import { Footer } from "../Footer/index.js";
+import { Header } from "../Header/index.js";
+import { MainNavTabs } from "../MainNavTabs/index.js";
+import { MobileNavTabs } from "../MobileNavTabs/index.js";
+import { ComposeProviders } from "../compose-providers.js";
+import { RouterProvider } from "./router-provider.js";
+import { LoggerProvider } from "../useLogger/index.js";
+
+import "./base.scss";
+
+const ReportAccessibility = dynamic(() =>
+  import("./report-accessibility.js").then((mod) => mod.ReportAccessibility),
+);
+
+type Tab = ComponentProps<typeof MainNavTabs>["tabs"][number] & {
+  children: ReactNode;
+};
+
+type Props = AppBodyProps & {
+  enableAccessibilityReporting: boolean;
+  amplitudeApiKey?: string | undefined;
+  googleAnalyticsId?: string | undefined;
+  providers?: ComponentProps<typeof ComposeProviders>["providers"] | undefined;
+};
+
+export const AppShell = ({
+  enableAccessibilityReporting,
+  amplitudeApiKey,
+  googleAnalyticsId,
+  providers,
+  ...props
+}: Props) => (
+  <RootProviders providers={providers}>
+    <HtmlWithLang
+      // See https://github.com/pacocoursey/next-themes?tab=readme-ov-file#with-app
+      suppressHydrationWarning
+      className={clsx(sans.className, styles.html)}
+    >
+      <body className={styles.body}>
+        <AppBody {...props} />
+      </body>
+      {googleAnalyticsId && <GoogleAnalytics gaId={googleAnalyticsId} />}
+      {amplitudeApiKey && <Amplitude apiKey={amplitudeApiKey} />}
+      {enableAccessibilityReporting && <ReportAccessibility />}
+    </HtmlWithLang>
+  </RootProviders>
+);
+
+type AppBodyProps = Pick<
+  ComponentProps<typeof Header>,
+  "appName" | "mainCta" | "extraCta"
+> & {
+  tabs?: Tab[] | undefined;
+  children: ReactNode;
+};
+
+export const AppBody = ({ tabs, children, ...props }: AppBodyProps) => (
+  <BodyProviders>
+    <TabRoot className={styles.appShell ?? ""}>
+      <Header
+        className={styles.header}
+        mainMenu={
+          tabs && (
+            <MainNavTabs className={styles.mainNavTabs ?? ""} tabs={tabs} />
+          )
+        }
+        {...props}
+      />
+      <main className={styles.main}>
+        <TabPanel>{children}</TabPanel>
+      </main>
+      <Footer />
+      {tabs && <MobileNavTabs tabs={tabs} className={styles.mobileNavTabs} />}
+    </TabRoot>
+  </BodyProviders>
+);
+
+type RootProvidersProps = Omit<
+  ComponentProps<typeof ComposeProviders>,
+  "providers"
+> & {
+  providers?: ComponentProps<typeof ComposeProviders>["providers"] | undefined;
+};
+
+export const RootProviders = ({ providers, ...props }: RootProvidersProps) => (
+  <ComposeProviders
+    providers={[
+      ...(providers ?? []),
+      LoggerProvider,
+      I18nProvider,
+      RouterProvider,
+    ]}
+    {...props}
+  />
+);

+ 2 - 1
packages/next-root/src/report-accessibility.tsx → packages/component-library/src/AppShell/report-accessibility.ts

@@ -1,10 +1,11 @@
 "use client";
 
-import { useLogger } from "@pythnetwork/app-logger";
 import { useEffect } from "react";
 import * as React from "react";
 import * as ReactDOM from "react-dom";
 
+import { useLogger } from "../useLogger/index.js";
+
 const AXE_TIMEOUT = 1000;
 
 export const ReportAccessibility = () => {

+ 0 - 0
packages/next-root/src/router-provider.tsx → packages/component-library/src/AppShell/router-provider.tsx


+ 22 - 0
packages/component-library/src/AppShell/tabs.tsx

@@ -0,0 +1,22 @@
+"use client";
+
+import { useSelectedLayoutSegment } from "next/navigation";
+import type { ComponentProps } from "react";
+
+import { TabPanel as UnstyledTabPanel, Tabs } from "../unstyled/Tabs/index.js";
+
+export const TabRoot = (
+  props: Omit<ComponentProps<typeof Tabs>, "selectedKey">,
+) => {
+  const tabId = useSelectedLayoutSegment() ?? "";
+
+  return <Tabs selectedKey={tabId} {...props} />;
+};
+
+export const TabPanel = (
+  props: Omit<ComponentProps<typeof UnstyledTabPanel>, "id">,
+) => {
+  const tabId = useSelectedLayoutSegment() ?? "";
+
+  return <UnstyledTabPanel key="tabpanel" id={tabId} {...props} />;
+};

+ 6 - 0
packages/component-library/src/Card/index.stories.tsx

@@ -5,6 +5,12 @@ import { Card as CardComponent, VARIANTS } from "./index.js";
 
 const meta = {
   component: CardComponent,
+  globals: {
+    background: "primary",
+  },
+  parameters: {
+    layout: "padded",
+  },
   argTypes: {
     href: {
       control: "text",

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

@@ -7,7 +7,7 @@ import { TabListStateContext } from "react-aria-components";
 
 import { TabPanel as UnstyledTabPanel } from "../unstyled/Tabs/index.js";
 
-const AnimatedPanel = motion(UnstyledTabPanel);
+const AnimatedPanel = motion.create(UnstyledTabPanel);
 
 type Props = {
   items: {

+ 1 - 2
apps/insights/src/components/Root/footer.module.scss → packages/component-library/src/Footer/index.module.scss

@@ -1,7 +1,6 @@
-@use "@pythnetwork/component-library/theme";
+@use "../theme";
 
 .footer {
-  background: theme.color("background", "primary");
   padding: theme.spacing(8) 0;
 
   .topContent {

+ 16 - 0
packages/component-library/src/Footer/index.stories.tsx

@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { Footer as FooterComponent } from "./index.js";
+
+const meta = {
+  component: FooterComponent,
+  parameters: {
+    layout: "fullscreen",
+  },
+  argTypes: {},
+} satisfies Meta<typeof FooterComponent>;
+export default meta;
+
+export const Footer = {
+  args: {},
+} satisfies StoryObj<typeof FooterComponent>;

+ 6 - 6
apps/insights/src/components/Root/footer.tsx → packages/component-library/src/Footer/index.tsx

@@ -1,12 +1,12 @@
-import type { Props as ButtonProps } from "@pythnetwork/component-library/Button";
-import { Button } from "@pythnetwork/component-library/Button";
-import { Link } from "@pythnetwork/component-library/Link";
 import type { ComponentProps, ElementType } from "react";
 
-import styles from "./footer.module.scss";
-import { socialLinks } from "./social-links";
-import { SupportDrawer } from "./support-drawer";
+import styles from "./index.module.scss";
 import Wordmark from "./wordmark.svg";
+import type { Props as ButtonProps } from "../Button/index.js";
+import { Button } from "../Button/index.js";
+import { SupportDrawer } from "../Header/index.js";
+import { Link } from "../Link/index.js";
+import { socialLinks } from "../social-links.js";
 
 export const Footer = () => (
   <footer className={styles.footer}>

+ 0 - 0
apps/insights/src/components/Root/wordmark.svg → packages/component-library/src/Footer/wordmark.svg


+ 103 - 15
apps/insights/src/components/Root/header.module.scss → packages/component-library/src/Header/index.module.scss

@@ -1,6 +1,7 @@
-@use "@pythnetwork/component-library/theme";
+@use "../theme";
 
 .header {
+  height: theme.$header-height;
   position: sticky;
   top: 0;
   width: 100%;
@@ -88,20 +89,6 @@
         }
       }
 
-      .outlineSearchButton {
-        display: none;
-
-        @include theme.breakpoint("md") {
-          display: unset;
-        }
-      }
-
-      .ghostSearchButton {
-        @include theme.breakpoint("md") {
-          display: none;
-        }
-      }
-
       .mobileMenu {
         @include theme.breakpoint("lg") {
           display: none;
@@ -132,3 +119,104 @@
     }
   }
 }
+
+.mobileMenuContents {
+  display: flex;
+  flex-flow: column nowrap;
+  align-items: stretch;
+  gap: theme.spacing(6);
+  justify-content: space-between;
+
+  .buttons {
+    display: flex;
+    flex-flow: column nowrap;
+    align-items: stretch;
+    gap: theme.spacing(6);
+  }
+
+  .theme {
+    display: flex;
+    flex-flow: row nowrap;
+    justify-content: flex-end;
+    align-items: center;
+    gap: theme.spacing(2);
+
+    .themeLabel {
+      @include theme.text("sm", "normal");
+
+      color: theme.color("muted");
+    }
+  }
+}
+
+.supportDrawer {
+  display: flex;
+  flex-flow: column nowrap;
+  gap: theme.spacing(8);
+
+  & > * {
+    flex: none;
+  }
+
+  .linkList {
+    display: flex;
+    flex-flow: column nowrap;
+    gap: theme.spacing(4);
+
+    .title {
+      @include theme.text("lg", "medium");
+
+      color: theme.color("heading");
+    }
+
+    .items {
+      list-style-type: none;
+      padding: 0;
+      margin: 0;
+      display: flex;
+      flex-flow: column nowrap;
+      gap: theme.spacing(2);
+
+      .link {
+        padding: theme.spacing(3);
+        display: grid;
+        grid-template-columns: max-content 1fr max-content;
+        grid-template-rows: max-content max-content;
+        text-align: left;
+        gap: theme.spacing(2) theme.spacing(4);
+        align-items: center;
+        width: 100%;
+
+        .icon {
+          font-size: theme.spacing(8);
+          color: theme.color("states", "data", "normal");
+          grid-row: span 2 / span 2;
+          display: grid;
+          place-content: center;
+        }
+
+        .linkTitle {
+          @include theme.text("sm", "medium");
+
+          color: theme.color("heading");
+        }
+
+        .description {
+          @include theme.text("xs", "normal");
+
+          color: theme.color("muted");
+          grid-column: 2;
+          grid-row: 2;
+          text-overflow: ellipsis;
+          overflow: hidden;
+        }
+
+        .caret {
+          color: theme.color("states", "data", "normal");
+          font-size: theme.spacing(4);
+          grid-row: span 2 / span 2;
+        }
+      }
+    }
+  }
+}

+ 36 - 0
packages/component-library/src/Header/index.stories.tsx

@@ -0,0 +1,36 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { ThemeProvider } from "next-themes";
+
+import { Header as HeaderComponent } from "./index.js";
+
+const meta = {
+  component: HeaderComponent,
+  decorators: [
+    (Story) => (
+      <ThemeProvider>
+        <Story />
+      </ThemeProvider>
+    ),
+  ],
+  globals: {
+    background: "primary",
+  },
+  parameters: {
+    layout: "fullscreen",
+  },
+  argTypes: {
+    appName: {
+      control: "text",
+      table: {
+        category: "Contents",
+      },
+    },
+  },
+} satisfies Meta<typeof HeaderComponent>;
+export default meta;
+
+export const Header = {
+  args: {
+    appName: "Component Library",
+  },
+} satisfies StoryObj<typeof HeaderComponent>;

+ 120 - 9
apps/insights/src/components/Root/support-drawer.tsx → packages/component-library/src/Header/index.tsx

@@ -1,19 +1,130 @@
-"use client";
-
 import { BookOpenText } from "@phosphor-icons/react/dist/ssr/BookOpenText";
 import { CaretRight } from "@phosphor-icons/react/dist/ssr/CaretRight";
 import { Code } from "@phosphor-icons/react/dist/ssr/Code";
 import { Coins } from "@phosphor-icons/react/dist/ssr/Coins";
 import { Gavel } from "@phosphor-icons/react/dist/ssr/Gavel";
+import { Lifebuoy } from "@phosphor-icons/react/dist/ssr/Lifebuoy";
+import { List } from "@phosphor-icons/react/dist/ssr/List";
 import { Plug } from "@phosphor-icons/react/dist/ssr/Plug";
 import { ShieldChevron } from "@phosphor-icons/react/dist/ssr/ShieldChevron";
-import type { Props as CardProps } from "@pythnetwork/component-library/Card";
-import { Card } from "@pythnetwork/component-library/Card";
-import type { Link as UnstyledLink } from "@pythnetwork/component-library/unstyled/Link";
-import type { ReactNode } from "react";
+import clsx from "clsx";
+import type { ComponentProps, ReactNode } from "react";
+
+import { socialLinks } from "../social-links.js";
+import styles from "./index.module.scss";
+import Logo from "./logo.svg";
+import { ThemeSwitch } from "./theme-switch.js";
+import { Button } from "../Button/index.js";
+import type { Props as CardProps } from "../Card/index.js";
+import { Card } from "../Card/index.js";
+import { Link } from "../Link/index.js";
+import type { Link as UnstyledLink } from "../unstyled/Link/index.js";
+
+type Props = ComponentProps<"header"> & {
+  appName: string;
+  mainCta?:
+    | {
+        label: string;
+        href: string;
+      }
+    | undefined;
+  mainMenu?: ReactNode | undefined;
+  extraCta?: ReactNode | undefined;
+};
 
-import { socialLinks } from "./social-links";
-import styles from "./support-drawer.module.scss";
+export const Header = ({
+  className,
+  appName,
+  mainCta,
+  mainMenu,
+  extraCta,
+  ...props
+}: Props) => (
+  <header className={clsx(styles.header, className)} {...props}>
+    <div className={styles.content}>
+      <div className={styles.leftMenu}>
+        <Link href="/" className={styles.logoLink ?? ""}>
+          <div className={styles.logoWrapper}>
+            <Logo className={styles.logo} />
+          </div>
+          <div className={styles.logoLabel}>Pyth Homepage</div>
+        </Link>
+        <div className={styles.appName}>{appName}</div>
+        {mainMenu}
+      </div>
+      <div className={styles.rightMenu}>
+        <Button
+          variant="ghost"
+          size="sm"
+          rounded
+          beforeIcon={Lifebuoy}
+          drawer={SupportDrawer}
+          className={styles.supportButton ?? ""}
+        >
+          Support
+        </Button>
+        {extraCta}
+        <MobileMenu className={styles.mobileMenu} />
+        <Button
+          href={mainCta?.href ?? "https://docs.pyth.network"}
+          size="sm"
+          rounded
+          target="_blank"
+          className={styles.mainCta ?? ""}
+        >
+          {mainCta?.label ?? "Dev Docs"}
+        </Button>
+        <ThemeSwitch className={styles.themeSwitch ?? ""} />
+      </div>
+    </div>
+  </header>
+);
+
+const MobileMenu = ({ className }: { className?: string | undefined }) => (
+  <Button
+    className={className ?? ""}
+    beforeIcon={List}
+    variant="ghost"
+    size="sm"
+    rounded
+    hideText
+    drawer={{
+      hideHeading: true,
+      title: "Menu",
+      contents: <MobileMenuContents />,
+    }}
+  >
+    Menu
+  </Button>
+);
+
+const MobileMenuContents = () => (
+  <div className={styles.mobileMenuContents}>
+    <div className={styles.buttons}>
+      <Button
+        variant="ghost"
+        size="md"
+        rounded
+        beforeIcon={Lifebuoy}
+        drawer={SupportDrawer}
+      >
+        Support
+      </Button>
+      <Button
+        href="https://docs.pyth.network"
+        size="md"
+        rounded
+        target="_blank"
+      >
+        Dev Docs
+      </Button>
+    </div>
+    <div className={styles.theme}>
+      <span className={styles.themeLabel}>Theme</span>
+      <ThemeSwitch />
+    </div>
+  </div>
+);
 
 type LinkListProps = {
   title: ReactNode;
@@ -35,7 +146,7 @@ const LinkList = ({ title, links }: LinkListProps) => (
         <Card key={i} {...link}>
           <div className={styles.link}>
             <div className={styles.icon}>{icon}</div>
-            <h4 className={styles.header}>{title}</h4>
+            <h4 className={styles.linkTitle}>{title}</h4>
             {description && <p className={styles.description}>{description}</p>}
             <CaretRight className={styles.caret} />
           </div>

+ 4 - 0
packages/component-library/src/Header/logo.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32.3 41" fill="currentColor">
+  <path d="M19.9998 16.5133C19.9998 18.7239 18.2087 20.5163 15.9998 20.5163V24.5193C20.4177 24.5193 23.9998 20.9346 23.9998 16.5133C23.9998 12.0921 20.4177 8.50732 15.9998 8.50732C14.5434 8.50732 13.1757 8.89658 11.9998 9.57914C9.60808 10.9624 7.99976 13.5496 7.99976 16.5133V36.5283L11.5963 40.1276L11.9998 40.5313V16.5133C11.9998 14.3027 13.7908 12.5103 15.9998 12.5103C18.2087 12.5103 19.9998 14.3027 19.9998 16.5133Z"/>
+  <path d="M16 0.501953C13.0855 0.501953 10.3537 1.28228 8 2.64558C6.49299 3.51643 5.14337 4.62626 4 5.92438C1.51063 8.74694 0 12.4548 0 16.514V28.523L4 32.526V16.514C4 12.9582 5.545 9.76263 8 7.56288C9.15423 6.5309 10.5093 5.71618 12 5.19113C13.2501 4.74575 14.5979 4.50496 16 4.50496C22.6269 4.50496 28 9.88212 28 16.514C28 23.1458 22.6269 28.523 16 28.523V32.526C24.8376 32.526 32 25.3564 32 16.514C32 7.67151 24.8376 0.501953 16 0.501953Z"/>
+</svg>

+ 1 - 1
apps/insights/src/components/Root/theme-switch.module.scss → packages/component-library/src/Header/theme-switch.module.scss

@@ -1,4 +1,4 @@
-@use "@pythnetwork/component-library/theme";
+@use "../theme";
 
 .themeSwitch {
   overflow: hidden;

+ 0 - 0
apps/insights/src/components/Root/theme-switch.tsx → packages/component-library/src/Header/theme-switch.tsx


+ 0 - 19
packages/component-library/src/Html/base.scss

@@ -1,19 +0,0 @@
-@use "modern-normalize";
-@use "../theme";
-
-:root {
-  background: black;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  scroll-behavior: smooth;
-  line-height: 1;
-}
-
-html {
-  padding-right: 0 !important;
-}
-
-*::selection {
-  color: theme.color("selection", "foreground");
-  background: theme.color("selection", "background");
-}

+ 0 - 9
packages/component-library/src/Html/index.tsx

@@ -1,9 +0,0 @@
-import { sans } from "@pythnetwork/fonts";
-import clsx from "clsx";
-import type { ComponentProps } from "react";
-
-import "./base.scss";
-
-export const Html = ({ className, lang, ...props }: ComponentProps<"html">) => (
-  <html lang={lang} className={clsx(sans.className, className)} {...props} />
-);

+ 0 - 12
packages/component-library/src/MainContent/index.module.scss

@@ -1,12 +0,0 @@
-@use "../theme";
-
-.mainContent {
-  color: theme.color("foreground");
-  background: theme.color("background", "primary");
-  border-top-left-radius: calc(var(--offset) * theme.border-radius("xl"));
-  border-top-right-radius: calc(var(--offset) * theme.border-radius("xl"));
-  overflow: hidden auto;
-  transform: scale(calc(100% - (var(--offset) * 5%)));
-  height: 100dvh;
-  scrollbar-gutter: stable;
-}

+ 0 - 30
packages/component-library/src/MainContent/index.tsx

@@ -1,30 +0,0 @@
-"use client";
-
-import clsx from "clsx";
-import type { ComponentProps, CSSProperties } from "react";
-import { useState } from "react";
-
-import styles from "./index.module.scss";
-import { OverlayVisibleContext } from "../overlay-visible-context.js";
-import { AlertProvider } from "../useAlert/index.js";
-import { DrawerProvider } from "../useDrawer/index.js";
-
-export const MainContent = ({ className, ...props }: ComponentProps<"div">) => {
-  const overlayVisibleState = useState(false);
-  const [offset, setOffset] = useState(0);
-
-  return (
-    <OverlayVisibleContext value={overlayVisibleState}>
-      <AlertProvider>
-        <DrawerProvider setMainContentOffset={setOffset}>
-          <div
-            className={clsx(styles.mainContent, className)}
-            style={{ "--offset": offset / 100 } as CSSProperties}
-            data-overlay-visible={overlayVisibleState[0] ? "" : undefined}
-            {...props}
-          />
-        </DrawerProvider>
-      </AlertProvider>
-    </OverlayVisibleContext>
-  );
-};

+ 5 - 9
packages/component-library/src/MainNavTabs/index.stories.tsx

@@ -6,12 +6,7 @@ import { Tabs } from "../unstyled/Tabs/index.js";
 const meta = {
   component: MainNavTabsComponent,
   argTypes: {
-    items: {
-      table: {
-        disable: true,
-      },
-    },
-    pathname: {
+    tabs: {
       table: {
         disable: true,
       },
@@ -29,9 +24,10 @@ export const MainNavTabs = {
     ),
   ],
   args: {
-    items: [
-      { id: "foo", children: "Foo" },
-      { id: "bar", children: "Bar" },
+    tabs: [
+      { children: "Home", segment: "" },
+      { children: "Foo", segment: "foo" },
+      { children: "Bar", segment: "bar" },
     ],
   },
 } satisfies StoryObj<typeof MainNavTabsComponent>;

+ 15 - 5
packages/component-library/src/MainNavTabs/index.tsx

@@ -2,6 +2,7 @@
 
 import clsx from "clsx";
 import { motion } from "motion/react";
+import { usePathname } from "next/navigation";
 import type { ComponentProps } from "react";
 import { useId } from "react";
 
@@ -9,13 +10,19 @@ import styles from "./index.module.scss";
 import buttonStyles from "../Button/index.module.scss";
 import { Tab, TabList } from "../unstyled/Tabs/index.js";
 
+type Tab = Omit<ComponentProps<typeof Tab>, "id" | "href"> & {
+  segment: string;
+};
+
 type OwnProps = {
-  pathname?: string | undefined;
-  items: ComponentProps<typeof Tab>[];
+  tabs: Tab[];
 };
-type Props = Omit<ComponentProps<typeof TabList>, keyof OwnProps> & OwnProps;
 
-export const MainNavTabs = ({ className, pathname, ...props }: Props) => {
+type Props = Omit<ComponentProps<typeof TabList>, keyof OwnProps | "items"> &
+  OwnProps;
+
+export const MainNavTabs = ({ className, tabs, ...props }: Props) => {
+  const pathname = usePathname();
   const id = useId();
   return (
     <TabList
@@ -23,8 +30,9 @@ export const MainNavTabs = ({ className, pathname, ...props }: Props) => {
       className={clsx(styles.mainNavTabs, className)}
       dependencies={[pathname]}
       data-selectable={
-        props.items.every((tab) => tab.href !== pathname) ? "" : undefined
+        tabs.every((tab) => pathname !== `/${tab.segment}`) ? "" : undefined
       }
+      items={tabs}
       {...props}
     >
       {({ className: tabClassName, children, ...tab }) => (
@@ -33,6 +41,8 @@ export const MainNavTabs = ({ className, pathname, ...props }: Props) => {
           data-size="sm"
           data-variant="ghost"
           data-rounded
+          id={tab.segment}
+          href={`/${tab.segment}`}
           {...tab}
         >
           {(args) => (

+ 1 - 1
apps/insights/src/components/Root/mobile-nav-tabs.module.scss → packages/component-library/src/MobileNavTabs/index.module.scss

@@ -1,4 +1,4 @@
-@use "@pythnetwork/component-library/theme";
+@use "../theme";
 
 .mobileNavTabs {
   background: theme.color("background", "primary");

+ 28 - 0
packages/component-library/src/MobileNavTabs/index.stories.tsx

@@ -0,0 +1,28 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { MobileNavTabs as MobileNavTabsComponent } from "./index.js";
+
+const meta = {
+  component: MobileNavTabsComponent,
+  parameters: {
+    layout: "padded",
+  },
+  argTypes: {
+    tabs: {
+      table: {
+        disable: true,
+      },
+    },
+  },
+} satisfies Meta<typeof MobileNavTabsComponent>;
+export default meta;
+
+export const MobileNavTabs = {
+  args: {
+    tabs: [
+      { children: "Home", segment: "" },
+      { children: "Foo", segment: "foo" },
+      { children: "Bar", segment: "bar" },
+    ],
+  },
+} satisfies StoryObj<typeof MobileNavTabsComponent>;

+ 11 - 14
apps/insights/src/components/Root/mobile-nav-tabs.tsx → packages/component-library/src/MobileNavTabs/index.tsx

@@ -7,16 +7,11 @@ import { usePathname } from "next/navigation";
 import type { ReactNode } from "react";
 import { useId, useMemo } from "react";
 
-import styles from "./mobile-nav-tabs.module.scss";
+import styles from "./index.module.scss";
 
 type Props = {
   className?: string | undefined;
-  tabs: Tab[];
-};
-
-type Tab = {
-  href: string;
-  children: ReactNode;
+  tabs: Omit<TabProps, "bubbleId">[];
 };
 
 export const MobileNavTabs = ({ tabs, className }: Props) => {
@@ -25,31 +20,33 @@ export const MobileNavTabs = ({ tabs, className }: Props) => {
   return (
     <nav className={clsx(styles.mobileNavTabs, className)}>
       {tabs.map((tab) => (
-        <NavTab tab={tab} key={tab.href} bubbleId={bubbleId} />
+        <NavTab key={tab.segment} bubbleId={bubbleId} {...tab} />
       ))}
     </nav>
   );
 };
 
 type TabProps = {
-  tab: Tab;
+  segment: string;
+  children: ReactNode;
   bubbleId: string;
 };
 
-const NavTab = ({ tab, bubbleId }: TabProps) => {
+const NavTab = ({ segment, bubbleId, children }: TabProps) => {
   const pathname = usePathname();
   const isSelected = useMemo(
-    () => (tab.href === "/" ? pathname === "/" : pathname.startsWith(tab.href)),
-    [tab.href, pathname],
+    () =>
+      segment === "" ? pathname === "/" : pathname.startsWith(`/${segment}`),
+    [segment, pathname],
   );
 
   return (
     <Link
-      href={tab.href}
+      href={`/${segment}`}
       className={styles.mobileTab ?? ""}
       data-is-selected={isSelected ? "" : undefined}
     >
-      {tab.children}
+      {children}
       {isSelected && (
         <motion.span
           layoutId={`${bubbleId}-bubble`}

+ 3 - 0
packages/component-library/src/Paginator/index.stories.tsx

@@ -4,6 +4,9 @@ import { Paginator as PaginatorComponent } from "./index.js";
 
 const meta = {
   component: PaginatorComponent,
+  parameters: {
+    layout: "padded",
+  },
   argTypes: {
     currentPage: {
       control: "number",

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

@@ -11,6 +11,9 @@ const cardMetaArgTypes = () => {
 
 const meta = {
   component: StatCardComponent,
+  parameters: {
+    layout: "padded",
+  },
   argTypes: {
     ...cardMetaArgTypes(),
     header: {

+ 3 - 0
packages/component-library/src/Table/index.stories.tsx

@@ -4,6 +4,9 @@ import { Table as TableComponent } from "./index.js";
 
 const meta = {
   component: TableComponent,
+  parameters: {
+    layout: "padded",
+  },
   argTypes: {
     columns: {
       table: {

+ 0 - 0
packages/next-root/src/compose-providers.tsx → packages/component-library/src/compose-providers.tsx


+ 0 - 0
apps/insights/src/components/Root/social-links.tsx → packages/component-library/src/social-links.ts


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

@@ -892,3 +892,5 @@ $breakpoints: (
     @content;
   }
 }
+
+$header-height: var(--header-height);

+ 3 - 3
packages/component-library/src/useDrawer/index.module.scss

@@ -26,6 +26,8 @@
     overflow-y: hidden;
 
     @include theme.breakpoint("sm") {
+      max-height: unset;
+
       &[data-variant="dialog"] {
         position: relative;
         top: theme.spacing(32);
@@ -37,7 +39,6 @@
         border: unset;
         border-radius: theme.border-radius("2xl");
         padding: theme.spacing(1);
-        max-height: theme.spacing(120);
         width: max-content;
       }
 
@@ -48,7 +49,6 @@
         right: theme.spacing(4);
         width: 60%;
         max-width: theme.spacing(180);
-        max-height: unset;
         border-radius: theme.border-radius("3xl");
         padding-bottom: theme.border-radius("3xl");
       }
@@ -128,7 +128,7 @@
       flex: 1;
       overflow-y: auto;
       padding: theme.spacing(4);
-      grid-auto-rows: minmax(0, max-content);
+      grid-auto-rows: minmax(min-content, max-content);
 
       @include theme.breakpoint("sm") {
         padding: theme.spacing(6);

+ 43 - 0
packages/component-library/src/useLogger/index.tsx

@@ -0,0 +1,43 @@
+"use client";
+
+import type { Logger } from "pino";
+import { pino } from "pino";
+import type { ComponentProps } from "react";
+import { createContext, useMemo, use } from "react";
+
+const LoggerContext = createContext<undefined | Logger<string>>(undefined);
+
+type LoggerProviderProps = Omit<
+  ComponentProps<typeof LoggerContext.Provider>,
+  "config" | "value"
+> & {
+  config?: Parameters<typeof pino>[0] | undefined;
+};
+
+export const LoggerProvider = ({ config, ...props }: LoggerProviderProps) => {
+  const logger = useMemo(
+    () =>
+      pino({
+        ...config,
+        browser: { ...config?.browser },
+      }),
+    [config],
+  );
+  return <LoggerContext value={logger} {...props} />;
+};
+
+export const useLogger = () => {
+  const logger = use(LoggerContext);
+  if (logger) {
+    return logger;
+  } else {
+    throw new LoggerNotInitializedError();
+  }
+};
+
+class LoggerNotInitializedError extends Error {
+  constructor() {
+    super("This component must be contained within a <LoggerProvider>");
+    this.name = "LoggerNotInitializedError";
+  }
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません