Pārlūkot izejas kodu

chore(component-library): extract app shell from insights hub

This PR makes a few related changes:

- Extract the app shell from insights hub to component library
- Merge `next-root`, `app-logger`, and `fonts` into the component library (the
  separation here didn't end up to be as clean as I'd originally hoped)
- Fix storybook and handle dark/light mode and primary/secondary backgrounds
  more correctly
Connor Prussin 6 mēneši atpakaļ
vecāks
revīzija
8efb31638d
100 mainītis faili ar 989 papildinājumiem un 766 dzēšanām
  1. 0 3
      .github/CODEOWNERS
  2. 0 3
      apps/insights/package.json
  3. 1 1
      apps/insights/src/app/global-error.tsx
  4. 1 1
      apps/insights/src/components/CopyButton/index.tsx
  5. 1 1
      apps/insights/src/components/Error/index.tsx
  6. 1 1
      apps/insights/src/components/PriceComponentDrawer/index.tsx
  7. 5 0
      apps/insights/src/components/PriceComponentsCard/index.module.scss
  8. 2 3
      apps/insights/src/components/PriceComponentsCard/index.tsx
  9. 1 1
      apps/insights/src/components/PriceFeed/chart.tsx
  10. 4 0
      apps/insights/src/components/PriceFeed/header.module.scss
  11. 3 1
      apps/insights/src/components/PriceFeed/header.tsx
  12. 1 1
      apps/insights/src/components/PriceFeed/publishers-card.tsx
  13. 1 1
      apps/insights/src/components/PriceFeeds/asset-class-table.tsx
  14. 5 0
      apps/insights/src/components/PriceFeeds/price-feeds-card.module.scss
  15. 2 3
      apps/insights/src/components/PriceFeeds/price-feeds-card.tsx
  16. 2 3
      apps/insights/src/components/Publishers/index.module.scss
  17. 5 0
      apps/insights/src/components/Publishers/publishers-card.module.scss
  18. 2 3
      apps/insights/src/components/Publishers/publishers-card.tsx
  19. 0 70
      apps/insights/src/components/Root/header.tsx
  20. 0 47
      apps/insights/src/components/Root/index.module.scss
  21. 25 42
      apps/insights/src/components/Root/index.tsx
  22. 0 4
      apps/insights/src/components/Root/logo.svg
  23. 0 30
      apps/insights/src/components/Root/mobile-menu.module.scss
  24. 0 57
      apps/insights/src/components/Root/mobile-menu.tsx
  25. 17 7
      apps/insights/src/components/Root/search-button.module.scss
  26. 57 47
      apps/insights/src/components/Root/search-button.tsx
  27. 0 73
      apps/insights/src/components/Root/support-drawer.module.scss
  28. 0 33
      apps/insights/src/components/Root/tabs.tsx
  29. 1 1
      apps/insights/src/components/TokenIcon/index.tsx
  30. 4 0
      apps/insights/src/components/TokenIcon/logo.svg
  31. 1 1
      apps/insights/src/hooks/use-data.ts
  32. 1 1
      apps/insights/src/hooks/use-live-price-data.tsx
  33. 1 1
      apps/insights/src/hooks/use-query-param-filter-pagination.ts
  34. 0 2
      packages/app-logger/.prettierignore
  35. 0 1
      packages/app-logger/README.md
  36. 0 1
      packages/app-logger/eslint.config.js
  37. 0 1
      packages/app-logger/jest.config.js
  38. 0 36
      packages/app-logger/package.json
  39. 0 1
      packages/app-logger/prettier.config.js
  40. 0 8
      packages/app-logger/src/context.ts
  41. 0 19
      packages/app-logger/src/index.tsx
  42. 0 26
      packages/app-logger/src/provider.tsx
  43. 0 3
      packages/app-logger/tsconfig.json
  44. 36 6
      packages/component-library/.storybook/main.ts
  45. 74 23
      packages/component-library/.storybook/preview.tsx
  46. 23 15
      packages/component-library/.storybook/storybook.module.scss
  47. 11 2
      packages/component-library/package.json
  48. 0 0
      packages/component-library/src/AppShell/amplitude.tsx
  49. 1 0
      packages/component-library/src/AppShell/base.scss
  50. 34 0
      packages/component-library/src/AppShell/body-providers.tsx
  51. 0 0
      packages/component-library/src/AppShell/fonts.tsx
  52. 1 2
      packages/component-library/src/AppShell/html-with-lang.tsx
  53. 0 0
      packages/component-library/src/AppShell/i18n-provider.tsx
  54. 66 0
      packages/component-library/src/AppShell/index.module.scss
  55. 51 0
      packages/component-library/src/AppShell/index.stories.tsx
  56. 107 0
      packages/component-library/src/AppShell/index.tsx
  57. 2 1
      packages/component-library/src/AppShell/report-accessibility.ts
  58. 0 0
      packages/component-library/src/AppShell/router-provider.tsx
  59. 22 0
      packages/component-library/src/AppShell/tabs.tsx
  60. 6 0
      packages/component-library/src/Card/index.stories.tsx
  61. 1 1
      packages/component-library/src/CrossfadeTabPanels/index.tsx
  62. 1 2
      packages/component-library/src/Footer/index.module.scss
  63. 16 0
      packages/component-library/src/Footer/index.stories.tsx
  64. 6 6
      packages/component-library/src/Footer/index.tsx
  65. 0 0
      packages/component-library/src/Footer/wordmark.svg
  66. 103 15
      packages/component-library/src/Header/index.module.scss
  67. 36 0
      packages/component-library/src/Header/index.stories.tsx
  68. 120 9
      packages/component-library/src/Header/index.tsx
  69. 4 0
      packages/component-library/src/Header/logo.svg
  70. 1 1
      packages/component-library/src/Header/theme-switch.module.scss
  71. 0 0
      packages/component-library/src/Header/theme-switch.tsx
  72. 0 19
      packages/component-library/src/Html/base.scss
  73. 0 9
      packages/component-library/src/Html/index.tsx
  74. 0 12
      packages/component-library/src/MainContent/index.module.scss
  75. 0 30
      packages/component-library/src/MainContent/index.tsx
  76. 5 9
      packages/component-library/src/MainNavTabs/index.stories.tsx
  77. 15 5
      packages/component-library/src/MainNavTabs/index.tsx
  78. 1 1
      packages/component-library/src/MobileNavTabs/index.module.scss
  79. 28 0
      packages/component-library/src/MobileNavTabs/index.stories.tsx
  80. 11 14
      packages/component-library/src/MobileNavTabs/index.tsx
  81. 3 0
      packages/component-library/src/Paginator/index.stories.tsx
  82. 3 0
      packages/component-library/src/StatCard/index.stories.tsx
  83. 3 0
      packages/component-library/src/Table/index.stories.tsx
  84. 0 0
      packages/component-library/src/compose-providers.tsx
  85. 0 0
      packages/component-library/src/social-links.ts
  86. 2 0
      packages/component-library/src/theme.scss
  87. 3 3
      packages/component-library/src/useDrawer/index.module.scss
  88. 43 0
      packages/component-library/src/useLogger/index.tsx
  89. 6 0
      packages/component-library/svg.d.ts
  90. 0 2
      packages/fonts/.prettierignore
  91. 0 1
      packages/fonts/README.md
  92. 0 1
      packages/fonts/eslint.config.js
  93. 0 1
      packages/fonts/jest.config.js
  94. 0 29
      packages/fonts/package.json
  95. 0 1
      packages/fonts/prettier.config.js
  96. 0 3
      packages/fonts/tsconfig.json
  97. 0 2
      packages/next-root/.prettierignore
  98. 0 1
      packages/next-root/README.md
  99. 0 1
      packages/next-root/eslint.config.js
  100. 0 1
      packages/next-root/jest.config.js

+ 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

+ 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,

+ 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";
+  }
+}

+ 6 - 0
packages/component-library/svg.d.ts

@@ -0,0 +1,6 @@
+declare module "*.svg" {
+  import type { ReactElement, SVGProps } from "react";
+
+  const content: (props: SVGProps<SVGElement>) => ReactElement;
+  export default content;
+}

+ 0 - 2
packages/fonts/.prettierignore

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

+ 0 - 1
packages/fonts/README.md

@@ -1 +0,0 @@
-# @pythnetwork/fonts

+ 0 - 1
packages/fonts/eslint.config.js

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

+ 0 - 1
packages/fonts/jest.config.js

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

+ 0 - 29
packages/fonts/package.json

@@ -1,29 +0,0 @@
-{
-  "name": "@pythnetwork/fonts",
-  "version": "0.0.0",
-  "private": true,
-  "type": "module",
-  "main": "./src/index.ts",
-  "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": {
-    "next": "catalog:"
-  },
-  "devDependencies": {
-    "@cprussin/eslint-config": "catalog:",
-    "@cprussin/jest-config": "catalog:",
-    "@cprussin/prettier-config": "catalog:",
-    "@cprussin/tsconfig": "catalog:",
-    "@types/jest": "catalog:",
-    "eslint": "catalog:",
-    "jest": "catalog:",
-    "next": "catalog:",
-    "prettier": "catalog:",
-    "typescript": "catalog:"
-  }
-}

+ 0 - 1
packages/fonts/prettier.config.js

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

+ 0 - 3
packages/fonts/tsconfig.json

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

+ 0 - 2
packages/next-root/.prettierignore

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

+ 0 - 1
packages/next-root/README.md

@@ -1 +0,0 @@
-# @pythnetwork/next-root

+ 0 - 1
packages/next-root/eslint.config.js

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

+ 0 - 1
packages/next-root/jest.config.js

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

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels