@@ -22,20 +28,41 @@ export const Header = ({ className, ...props }: ComponentProps<"header">) => (
Pyth Homepage
Insights
-
+
-
-
+
+
Support
-
-
+
+
+
+
+
+
+ Search
+
+
Dev Docs
diff --git a/apps/insights/src/components/Root/index.module.scss b/apps/insights/src/components/Root/index.module.scss
index cc2565df1f..311e7f37c3 100644
--- a/apps/insights/src/components/Root/index.module.scss
+++ b/apps/insights/src/components/Root/index.module.scss
@@ -1,6 +1,6 @@
@use "@pythnetwork/component-library/theme";
-$header-height: theme.spacing(20);
+$header-height: var(--header-height);
:export {
// stylelint-disable-next-line property-no-unknown
@@ -9,16 +9,28 @@ $header-height: theme.spacing(20);
.root {
scroll-padding-top: $header-height;
- overflow-x: hidden;
+
+ --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(6);
+ padding-top: theme.spacing(4);
+ min-height: calc(100svh - $header-height);
+
+ @include theme.breakpoint("sm") {
+ min-height: unset;
+ padding-top: theme.spacing(6);
+ }
}
.header {
@@ -26,4 +38,10 @@ $header-height: theme.spacing(20);
height: $header-height;
}
}
+
+ .mobileNavTabs {
+ @include theme.breakpoint("sm") {
+ display: none;
+ }
+ }
}
diff --git a/apps/insights/src/components/Root/index.tsx b/apps/insights/src/components/Root/index.tsx
index f9a72a7ea4..803b8e5a2b 100644
--- a/apps/insights/src/components/Root/index.tsx
+++ b/apps/insights/src/components/Root/index.tsx
@@ -5,8 +5,8 @@ import type { ReactNode } from "react";
import { Footer } from "./footer";
import { Header } from "./header";
-// import { MobileMenu } from "./mobile-menu";
import styles from "./index.module.scss";
+import { MobileNavTabs } from "./mobile-nav-tabs";
import { SearchDialogProvider } from "./search-dialog";
import { TabRoot, TabPanel } from "./tabs";
import {
@@ -14,7 +14,6 @@ import {
GOOGLE_ANALYTICS_ID,
AMPLITUDE_API_KEY,
} from "../../config/server";
-// import { toHex } from "../../hex";
import { LivePriceDataProvider } from "../../hooks/use-live-price-data";
import { PriceFeedsProvider as PriceFeedsProviderImpl } from "../../hooks/use-price-feeds";
import { getPublishers } from "../../services/clickhouse";
@@ -22,6 +21,16 @@ import { Cluster, getFeeds } from "../../services/pyth";
import { PriceFeedIcon } from "../PriceFeedIcon";
import { PublisherIcon } from "../PublisherIcon";
+export const TABS = [
+ { href: "/", id: "", children: "Overview" },
+ { href: "/publishers", id: "publishers", children: "Publishers" },
+ {
+ href: "/price-feeds",
+ id: "price-feeds",
+ children: "Price Feeds",
+ },
+];
+
type Props = {
children: ReactNode;
};
@@ -42,11 +51,12 @@ export const Root = async ({ children }: Props) => {
>
-
+
{children}
+
diff --git a/apps/insights/src/components/Root/mobile-menu.module.scss b/apps/insights/src/components/Root/mobile-menu.module.scss
new file mode 100644
index 0000000000..05cf4932c2
--- /dev/null
+++ b/apps/insights/src/components/Root/mobile-menu.module.scss
@@ -0,0 +1,30 @@
+@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");
+ }
+ }
+}
diff --git a/apps/insights/src/components/Root/mobile-menu.tsx b/apps/insights/src/components/Root/mobile-menu.tsx
index 1a760e5ace..c9958aa242 100644
--- a/apps/insights/src/components/Root/mobile-menu.tsx
+++ b/apps/insights/src/components/Root/mobile-menu.tsx
@@ -1,46 +1,78 @@
-import type { Icon } from "@phosphor-icons/react";
-import { Broadcast } from "@phosphor-icons/react/dist/ssr/Broadcast";
-import { ChartLine } from "@phosphor-icons/react/dist/ssr/ChartLine";
-import { List } from "@phosphor-icons/react/dist/ssr/List";
-import { MagnifyingGlass } from "@phosphor-icons/react/dist/ssr/MagnifyingGlass";
-import { PresentationChart } from "@phosphor-icons/react/dist/ssr/PresentationChart";
-import type { ComponentProps, ReactNode } from "react";
+"use client";
-import { NavLink } from "./nav-link";
+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 { Drawer, DrawerTrigger } from "@pythnetwork/component-library/Drawer";
+import { useCallback, useState, useRef } from "react";
-export const MobileMenu = () => (
-
-
-
-);
+import styles from "./mobile-menu.module.scss";
+import { SupportDrawer } from "./support-drawer";
+import { ThemeSwitch } from "./theme-switch";
-type MobileMenuItemProps = ComponentProps
& {
- title: ReactNode;
- icon: Icon;
+type Props = {
+ className?: string | undefined;
};
-const MobileMenuItem = ({
- title,
- icon: Icon,
- ...props
-}: MobileMenuItemProps) => (
-
-
-
- {title}
-
-
-);
+export const MobileMenu = ({ className }: Props) => {
+ const [isSupportDrawerOpen, setSupportDrawerOpen] = useState(false);
+ const openSupportDrawerOnClose = useRef(false);
+ const setOpenSupportDrawerOnClose = useCallback(() => {
+ openSupportDrawerOnClose.current = true;
+ }, []);
+ const maybeOpenSupportDrawer = useCallback(() => {
+ if (openSupportDrawerOnClose.current) {
+ setSupportDrawerOpen(true);
+ openSupportDrawerOnClose.current = false;
+ }
+ }, [setSupportDrawerOpen]);
+
+ return (
+ <>
+
+
+ Menu
+
+
+
+
+
+ Support
+
+
+ Dev Docs
+
+
+
+ Theme
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/apps/insights/src/components/Root/mobile-nav-tabs.module.scss b/apps/insights/src/components/Root/mobile-nav-tabs.module.scss
new file mode 100644
index 0000000000..8b447534c8
--- /dev/null
+++ b/apps/insights/src/components/Root/mobile-nav-tabs.module.scss
@@ -0,0 +1,52 @@
+@use "@pythnetwork/component-library/theme";
+
+.mobileNavTabs {
+ background: theme.color("background", "primary");
+ border-top: 1px solid theme.color("border");
+ position: sticky;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: theme.spacing(2);
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: theme.spacing(2);
+
+ .mobileTab {
+ @include theme.text("xs", "medium");
+
+ text-align: center;
+ position: relative;
+ padding: theme.spacing(2);
+ color: theme.color("foreground");
+ text-decoration: none;
+ line-height: theme.spacing(5);
+ outline: none;
+ transition:
+ color 200ms linear,
+ background-color 100ms linear;
+ border-radius: theme.border-radius("full");
+ -webkit-tap-highlight-color: transparent;
+
+ .bubble {
+ position: absolute;
+ inset: 0;
+ border-radius: theme.border-radius("full");
+ background-color: theme.color("button", "solid", "background", "normal");
+ outline: 4px solid transparent;
+ outline-offset: 0;
+ z-index: -1;
+ transition-property: background-color, outline-color;
+ transition-duration: 100ms;
+ transition-timing-function: linear;
+ }
+
+ &[data-is-selected] {
+ color: theme.color("background", "primary");
+ }
+
+ &[data-pressed] {
+ background: theme.color("button", "outline", "background", "active");
+ }
+ }
+}
diff --git a/apps/insights/src/components/Root/mobile-nav-tabs.tsx b/apps/insights/src/components/Root/mobile-nav-tabs.tsx
new file mode 100644
index 0000000000..76597ccc81
--- /dev/null
+++ b/apps/insights/src/components/Root/mobile-nav-tabs.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { Link } from "@pythnetwork/component-library/unstyled/Link";
+import clsx from "clsx";
+import { motion } from "motion/react";
+import { usePathname } from "next/navigation";
+import { type ReactNode, useId, useMemo } from "react";
+
+import styles from "./mobile-nav-tabs.module.scss";
+
+type Props = {
+ className?: string | undefined;
+ tabs: Tab[];
+};
+
+type Tab = {
+ href: string;
+ children: ReactNode;
+};
+
+export const MobileNavTabs = ({ tabs, className }: Props) => {
+ const bubbleId = useId();
+
+ return (
+
+ {tabs.map((tab) => (
+
+ ))}
+
+ );
+};
+
+type TabProps = {
+ tab: Tab;
+ bubbleId: string;
+};
+
+const NavTab = ({ tab, bubbleId }: TabProps) => {
+ const pathname = usePathname();
+ const isSelected = useMemo(
+ () => (tab.href === "/" ? pathname === "/" : pathname.startsWith(tab.href)),
+ [tab.href, pathname],
+ );
+
+ return (
+
+ {tab.children}
+ {isSelected && (
+
+ )}
+
+ );
+};
diff --git a/apps/insights/src/components/Root/nav-link.tsx b/apps/insights/src/components/Root/nav-link.tsx
deleted file mode 100644
index 9da03df30e..0000000000
--- a/apps/insights/src/components/Root/nav-link.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-"use client";
-
-import { Link } from "@pythnetwork/component-library/unstyled/Link";
-import { useSelectedLayoutSegment } from "next/navigation";
-import type { ReactNode } from "react";
-
-type Props = {
- href: string;
- target?: string | undefined;
- className?: string | undefined;
- children?: ReactNode | ReactNode[] | undefined;
-};
-
-export const NavLink = ({ href, target, className, children }: Props) => {
- const layoutSegment = useSelectedLayoutSegment();
-
- return `/${layoutSegment ?? ""}` === href ? (
-
- {children}
-
- ) : (
-
- {children}
-
- );
-};
diff --git a/apps/insights/src/components/Root/search-button.tsx b/apps/insights/src/components/Root/search-button.tsx
index 67e1bf86ff..a26c0a2a1b 100644
--- a/apps/insights/src/components/Root/search-button.tsx
+++ b/apps/insights/src/components/Root/search-button.tsx
@@ -3,27 +3,28 @@
import { MagnifyingGlass } from "@phosphor-icons/react/dist/ssr/MagnifyingGlass";
import { Button } from "@pythnetwork/component-library/Button";
import { Skeleton } from "@pythnetwork/component-library/Skeleton";
-import { useMemo } from "react";
+import { type ComponentProps, useMemo } from "react";
import { useIsSSR } from "react-aria";
import { useToggleSearchDialog } from "./search-dialog";
-export const SearchButton = () => {
+type Props = ComponentProps;
+
+export const SearchButton = (props: Props) => {
const toggleSearchDialog = useToggleSearchDialog();
+
return (
-
-
+ {...props}
+ />
);
};
-const SearchText = () => {
+export const SearchShortcutText = () => {
const isSSR = useIsSSR();
return isSSR ? : ;
};
diff --git a/apps/insights/src/components/Root/search-dialog.module.scss b/apps/insights/src/components/Root/search-dialog.module.scss
index 8985f5c938..90c69506c5 100644
--- a/apps/insights/src/components/Root/search-dialog.module.scss
+++ b/apps/insights/src/components/Root/search-dialog.module.scss
@@ -15,88 +15,193 @@
border-radius: theme.border-radius("2xl");
padding: theme.spacing(1);
max-height: theme.spacing(120);
+ width: min-content;
+ overflow: hidden;
+ display: flex;
+ }
+}
+
+.searchDialogContents {
+ gap: theme.spacing(1);
+ display: flex;
+ flex-flow: column nowrap;
+ overflow: hidden;
+ max-height: 100%;
+ min-height: 0;
+
+ .searchBar,
+ .left {
+ flex: none;
display: flex;
flex-flow: column nowrap;
- flex-grow: 1;
- gap: theme.spacing(1);
- width: min-content;
- .searchBar,
- .left {
- flex: none;
- display: flex;
+ @include theme.breakpoint("sm") {
flex-flow: row nowrap;
align-items: center;
}
+ }
+
+ .searchBar {
+ justify-content: space-between;
+ padding: theme.spacing(4);
+ flex: 1 0 0;
- .searchBar {
- justify-content: space-between;
+ @include theme.breakpoint("sm") {
+ flex: unset;
padding: theme.spacing(1);
}
- .left {
- gap: theme.spacing(2);
+ .searchInput {
+ @include theme.breakpoint("sm") {
+ width: theme.spacing(60);
+ }
+
+ @include theme.breakpoint("md") {
+ width: theme.spacing(70);
+ }
+
+ @include theme.breakpoint("lg") {
+ width: theme.spacing(90);
+ }
}
+ }
+
+ .left {
+ gap: theme.spacing(2);
+
+ @include theme.breakpoint("sm") {
+ gap: theme.spacing(4);
+ }
+
+ .typeFilter {
+ & > * {
+ flex: 1 0 0;
+
+ @include theme.breakpoint("sm") {
+ flex: unset;
+ }
+ }
+ }
+ }
+
+ .closeButton {
+ display: none;
+
+ @include theme.breakpoint("sm") {
+ display: inline flow-root;
+ }
+ }
- .body {
- background: theme.color("background", "primary");
- border-radius: theme.border-radius("xl");
+ .body {
+ background: theme.color("background", "primary");
+ border-radius: theme.border-radius("xl");
+ flex-grow: 1;
+ overflow: auto;
+ display: flex;
+
+ .listbox {
+ outline: none;
+ overflow: auto;
flex-grow: 1;
- overflow: hidden;
- display: flex;
- .listbox {
+ .item {
+ padding: theme.spacing(3) theme.spacing(4);
+ display: block;
+ cursor: pointer;
+ transition: background-color 100ms linear;
outline: none;
- overflow: auto;
- flex-grow: 1;
-
- .item {
- padding: theme.spacing(3) theme.spacing(4);
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
- cursor: pointer;
- transition: background-color 100ms linear;
- outline: none;
- text-decoration: none;
- border-top: 1px solid theme.color("background", "secondary");
+ text-decoration: none;
+ border-top: 1px solid theme.color("background", "secondary");
+ -webkit-tap-highlight-color: transparent;
- &[data-is-first] {
+ &[data-is-first] {
+ @include theme.breakpoint("sm") {
border-top: none;
}
+ }
- & > *:last-child {
- flex-shrink: 0;
- }
+ & > *:last-child {
+ flex-shrink: 0;
+ }
- &[data-focused] {
- background-color: theme.color(
- "button",
- "outline",
- "background",
- "hover"
- );
- }
+ &[data-focused] {
+ background-color: theme.color(
+ "button",
+ "outline",
+ "background",
+ "hover"
+ );
+ }
- &[data-pressed] {
- background-color: theme.color(
- "button",
- "outline",
- "background",
- "active"
- );
+ &[data-pressed] {
+ background-color: theme.color(
+ "button",
+ "outline",
+ "background",
+ "active"
+ );
+ }
+
+ .itemType {
+ flex-shrink: 0;
+ margin-right: theme.spacing(6);
+ }
+
+ .itemTag {
+ flex-grow: 1;
+ }
+
+ .smallScreen {
+ display: flex;
+ flex-flow: column nowrap;
+ gap: theme.spacing(2);
+
+ @include theme.breakpoint("sm") {
+ display: none;
}
- .itemType {
- flex-shrink: 0;
- margin-right: theme.spacing(6);
+ .bottom {
+ flex-flow: column nowrap;
+ gap: theme.spacing(2);
+ display: flex;
+ margin: 0;
+
+ .field {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+ gap: theme.spacing(4);
+ align-items: center;
+
+ dt {
+ color: theme.color("foreground");
+ font-weight: theme.font-weight("medium");
+ font-size: theme.font-size("sm");
+ }
+
+ dd {
+ margin: 0;
+ }
+ }
}
+ }
- .itemTag {
- flex-grow: 1;
+ .largeScreen {
+ display: none;
+ flex-flow: row nowrap;
+ align-items: center;
+
+ @include theme.breakpoint("sm") {
+ display: flex;
}
}
}
}
}
}
+
+// stylelint-disable property-no-unknown
+:export {
+ breakpoint-sm: theme.map-get-strict(theme.$breakpoints, "sm");
+}
+// stylelint-enable property-no-unknown
diff --git a/apps/insights/src/components/Root/search-dialog.tsx b/apps/insights/src/components/Root/search-dialog.tsx
index a707e8be87..b0fa49231c 100644
--- a/apps/insights/src/components/Root/search-dialog.tsx
+++ b/apps/insights/src/components/Root/search-dialog.tsx
@@ -3,6 +3,7 @@
import { XCircle } from "@phosphor-icons/react/dist/ssr/XCircle";
import { Badge } from "@pythnetwork/component-library/Badge";
import { Button } from "@pythnetwork/component-library/Button";
+import { Drawer } from "@pythnetwork/component-library/Drawer";
import { ModalDialog } from "@pythnetwork/component-library/ModalDialog";
import { SearchInput } from "@pythnetwork/component-library/SearchInput";
import { SingleToggleGroup } from "@pythnetwork/component-library/SingleToggleGroup";
@@ -14,9 +15,11 @@ import {
ListBox,
ListBoxItem,
} from "@pythnetwork/component-library/unstyled/ListBox";
+import { useMediaQuery } from "@react-hookz/web";
import { useRouter } from "next/navigation";
import {
type ReactNode,
+ type ComponentProps,
useState,
useCallback,
useEffect,
@@ -153,154 +156,245 @@ export const SearchDialogProvider = ({ children, publishers }: Props) => {
{children}
-
-
-
-
-
-
-
}
- slot="close"
- hideText
- rounded
- variant="ghost"
- size="sm"
- >
- Close
-
-
-
-
-
-
+
+
+
{
- setSearch("");
- }}
- />
- }
- >
- {(result) => (
-
-
-
- {result.type === ResultType.PriceFeed
- ? "PRICE FEED"
- : "PUBLISHER"}
-
-
- {result.type === ResultType.PriceFeed ? (
- <>
-
-
- >
- ) : (
- <>
-
-
- >
- )}
-
- )}
-
-
-
+ autoFocus
+ />
+
+
+
}
+ slot="close"
+ hideText
+ rounded
+ variant="ghost"
+ size="sm"
+ >
+ Close
+
+
+
+
+
+ {
+ setSearch("");
+ }}
+ />
+ }
+ >
+ {(result) => (
+
+
+ {result.type === ResultType.PriceFeed ? (
+
+ ) : (
+
+ )}
+
+
+
Type
+
+
+ {result.type === ResultType.PriceFeed
+ ? "PRICE FEED"
+ : "PUBLISHER"}
+
+
+
+
+ {result.type === ResultType.PriceFeed ? (
+ <>
+
Asset Class
+
+
+
+ >
+ ) : (
+ <>
+
Average Score
+
+
+
+ >
+ )}
+
+
+
+
+
+
+ {result.type === ResultType.PriceFeed
+ ? "PRICE FEED"
+ : "PUBLISHER"}
+
+
+ {result.type === ResultType.PriceFeed ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+ )}
+
+
+
+
-
+
>
);
};
+const SearchContainer = (
+ props: ComponentProps & { title: string },
+) => {
+ const isLarge = useMediaQuery(
+ `(min-width: ${styles["breakpoint-sm"] ?? ""})`,
+ );
+
+ return isLarge ? (
+
+ ) : (
+
+ );
+};
+
enum ResultType {
PriceFeed,
Publisher,
diff --git a/apps/insights/src/components/Root/support-drawer.module.scss b/apps/insights/src/components/Root/support-drawer.module.scss
index f6152c85d3..ab366bca0f 100644
--- a/apps/insights/src/components/Root/support-drawer.module.scss
+++ b/apps/insights/src/components/Root/support-drawer.module.scss
@@ -58,6 +58,8 @@
color: theme.color("muted");
grid-column: 2;
grid-row: 2;
+ text-overflow: ellipsis;
+ overflow: hidden;
}
.caret {
diff --git a/apps/insights/src/components/Root/support-drawer.tsx b/apps/insights/src/components/Root/support-drawer.tsx
index 81f340eeed..15b5374872 100644
--- a/apps/insights/src/components/Root/support-drawer.tsx
+++ b/apps/insights/src/components/Root/support-drawer.tsx
@@ -9,88 +9,83 @@ import {
type Props as CardProps,
Card,
} from "@pythnetwork/component-library/Card";
-import { DrawerTrigger, Drawer } from "@pythnetwork/component-library/Drawer";
+import { Drawer } from "@pythnetwork/component-library/Drawer";
import type { Link as UnstyledLink } from "@pythnetwork/component-library/unstyled/Link";
-import type { ReactNode } from "react";
+import type { ComponentProps, ReactNode } from "react";
import { socialLinks } from "./social-links";
import styles from "./support-drawer.module.scss";
-type Props = {
- children: ReactNode;
-};
-
-export const SupportDrawer = ({ children }: Props) => (
-
- {children}
-
- ,
- title: "Connect directly with real-time market data",
- description: "Integrate the Pyth data feeds into your app",
- target: "_blank",
- href: "https://docs.pyth.network/price-feeds/use-real-time-data",
- },
- {
- icon: ,
- title: "Learn how to work with Pyth data",
- description: "Read the Pyth Network documentation",
- target: "_blank",
- href: "https://docs.pyth.network",
- },
- {
- icon:
,
- title: "Try out the APIs",
- description:
- "Use the Pyth Network API Reference to experience the Pyth APIs",
- target: "_blank",
- href: "https://api-reference.pyth.network",
- },
- ]}
- />
- ,
- title: "Tokenomics",
- description:
- "Learn about how the $PYTH token is structured and distributed",
- target: "_blank",
- href: "https://docs.pyth.network/home/pyth-token/pyth-distribution",
- },
- {
- icon: ,
- title: "Oracle Integrity Staking (OIS) Guide",
- description: "Learn how to help secure the oracle and earn rewards",
- target: "_blank",
- href: "https://docs.pyth.network/home/oracle-integrity-staking",
- },
- {
- icon: ,
- title: "Pyth Governance Guide",
- description:
- "Gain voting power to help shape the future of DeFi by participating in governance",
- target: "_blank",
- href: "https://docs.pyth.network/home/pyth-token#staking-pyth-for-governance",
- },
- ]}
- />
- ({
- href,
+export const SupportDrawer = (
+ props: Omit, "title" | "bodyClassName">,
+) => (
+
+ ,
+ title: "Connect directly with real-time market data",
+ description: "Integrate the Pyth data feeds into your app",
+ target: "_blank",
+ href: "https://docs.pyth.network/price-feeds/use-real-time-data",
+ },
+ {
+ icon: ,
+ title: "Learn how to work with Pyth data",
+ description: "Read the Pyth Network documentation",
+ target: "_blank",
+ href: "https://docs.pyth.network",
+ },
+ {
+ icon:
,
+ title: "Try out the APIs",
+ description:
+ "Use the Pyth Network API Reference to experience the Pyth APIs",
+ target: "_blank",
+ href: "https://api-reference.pyth.network",
+ },
+ ]}
+ />
+ ,
+ title: "Tokenomics",
+ description:
+ "Learn about how the $PYTH token is structured and distributed",
+ target: "_blank",
+ href: "https://docs.pyth.network/home/pyth-token/pyth-distribution",
+ },
+ {
+ icon: ,
+ title: "Oracle Integrity Staking (OIS) Guide",
+ description: "Learn how to help secure the oracle and earn rewards",
+ target: "_blank",
+ href: "https://docs.pyth.network/home/oracle-integrity-staking",
+ },
+ {
+ icon: ,
+ title: "Pyth Governance Guide",
+ description:
+ "Gain voting power to help shape the future of DeFi by participating in governance",
target: "_blank",
- title: name,
- description: href,
- icon: ,
- }))}
- />
-
-
+ href: "https://docs.pyth.network/home/pyth-token#staking-pyth-for-governance",
+ },
+ ]}
+ />
+ ({
+ href,
+ target: "_blank",
+ title: name,
+ description: href,
+ icon: ,
+ }))}
+ />
+
);
type LinkListProps = {
diff --git a/apps/insights/src/components/Root/tabs.tsx b/apps/insights/src/components/Root/tabs.tsx
index 2c534e8eb2..75842c5131 100644
--- a/apps/insights/src/components/Root/tabs.tsx
+++ b/apps/insights/src/components/Root/tabs.tsx
@@ -19,28 +19,11 @@ export const TabRoot = (
};
export const MainNavTabs = (
- props: Omit<
- ComponentProps,
- "pathname" | "items"
- >,
+ props: Omit, "pathname">,
) => {
const pathname = usePathname();
- return (
-
- );
+ return ;
};
export const TabPanel = ({
diff --git a/apps/insights/src/components/Score/index.tsx b/apps/insights/src/components/Score/index.tsx
index 187909a747..5e57619663 100644
--- a/apps/insights/src/components/Score/index.tsx
+++ b/apps/insights/src/components/Score/index.tsx
@@ -2,6 +2,7 @@
import { Skeleton } from "@pythnetwork/component-library/Skeleton";
import { Meter } from "@pythnetwork/component-library/unstyled/Meter";
+import clsx from "clsx";
import type { CSSProperties } from "react";
import styles from "./index.module.scss";
@@ -11,6 +12,7 @@ const SCORE_WIDTH = 24;
type Props = {
width?: number | undefined;
fill?: boolean | undefined;
+ className?: string | undefined;
} & (
| { isLoading: true }
| {
@@ -19,10 +21,10 @@ type Props = {
}
);
-export const Score = ({ width, fill, ...props }: Props) =>
+export const Score = ({ width, fill, className, ...props }: Props) =>
props.isLoading ? (
/>
) : (
(
-
-
-
- ),
+ (Story) => {
+ const overlayVisibleState = useState(false);
+ return (
+
+
+
+ );
+ },
withThemeByClassName({
themes: {
Light: clsx(sans.className, styles.light),
diff --git a/packages/component-library/src/Breadcrumbs/index.module.scss b/packages/component-library/src/Breadcrumbs/index.module.scss
index 0f00f8715c..2767adb9e5 100644
--- a/packages/component-library/src/Breadcrumbs/index.module.scss
+++ b/packages/component-library/src/Breadcrumbs/index.module.scss
@@ -4,16 +4,24 @@
display: flex;
flex-flow: row nowrap;
align-items: center;
- gap: theme.spacing(4);
+ gap: theme.spacing(2);
list-style: none;
margin: 0;
padding: 0;
+ @include theme.breakpoint("sm") {
+ gap: theme.spacing(4);
+ }
+
.breadcrumb {
display: flex;
flex-flow: row nowrap;
align-items: center;
- gap: theme.spacing(4);
+ gap: theme.spacing(2);
+
+ @include theme.breakpoint("sm") {
+ gap: theme.spacing(4);
+ }
.separator {
color: theme.color("muted");
diff --git a/packages/component-library/src/Button/index.module.scss b/packages/component-library/src/Button/index.module.scss
index 6de1e6fc7a..f781019ff2 100644
--- a/packages/component-library/src/Button/index.module.scss
+++ b/packages/component-library/src/Button/index.module.scss
@@ -12,6 +12,8 @@
text-decoration: none;
outline-offset: 0;
outline: theme.spacing(1) solid transparent;
+ text-align: center;
+ -webkit-tap-highlight-color: transparent;
.iconWrapper {
display: inline-grid;
diff --git a/packages/component-library/src/Card/index.module.scss b/packages/component-library/src/Card/index.module.scss
index 646f50e241..d4691abbe7 100644
--- a/packages/component-library/src/Card/index.module.scss
+++ b/packages/component-library/src/Card/index.module.scss
@@ -16,6 +16,7 @@
position: relative;
padding: theme.spacing(1);
isolation: isolate;
+ -webkit-tap-highlight-color: transparent;
@at-root button#{&} {
cursor: pointer;
@@ -34,8 +35,10 @@
.header {
display: flex;
- padding: theme.spacing(3) theme.spacing(4);
+
+ // padding: theme.spacing(3) theme.spacing(4);
position: relative;
+ flex-flow: column nowrap;
.title {
color: theme.color("heading");
@@ -43,6 +46,7 @@
flex-flow: row nowrap;
gap: theme.spacing(3);
align-items: center;
+ padding: theme.spacing(3);
@include theme.text("lg", "medium");
@@ -54,14 +58,30 @@
}
.toolbar {
- position: absolute;
- right: theme.spacing(3);
- top: 0;
- bottom: theme.spacing(0);
display: flex;
flex-flow: row nowrap;
- gap: theme.spacing(4);
+ gap: theme.spacing(2);
align-items: center;
+ justify-content: center;
+ padding: theme.spacing(1.5);
+
+ @include theme.breakpoint("lg") {
+ position: absolute;
+ right: theme.spacing(3);
+ top: 0;
+ bottom: theme.spacing(0);
+ gap: theme.spacing(4);
+ justify-content: unset;
+ padding: 0;
+ }
+
+ &[data-always-on-top] {
+ position: absolute;
+ right: theme.spacing(3);
+ top: 0;
+ bottom: theme.spacing(0);
+ gap: theme.spacing(4);
+ }
}
}
diff --git a/packages/component-library/src/Card/index.tsx b/packages/component-library/src/Card/index.tsx
index de33b1c4cd..6716fc8e2d 100644
--- a/packages/component-library/src/Card/index.tsx
+++ b/packages/component-library/src/Card/index.tsx
@@ -22,6 +22,8 @@ type OwnProps = {
toolbar?: ReactNode | ReactNode[] | undefined;
footer?: ReactNode | undefined;
nonInteractive?: boolean | undefined;
+ toolbarClassName?: string | undefined;
+ toolbarAlwaysOnTop?: boolean | undefined;
};
export type Props = Omit<
@@ -59,6 +61,8 @@ const cardProps = ({
title,
toolbar,
footer,
+ toolbarClassName,
+ toolbarAlwaysOnTop,
...props
}: Props) => ({
...props,
@@ -73,7 +77,14 @@ const cardProps = ({
{icon && {icon}
}
{title}
- {toolbar}
+ {toolbar && (
+
+ {toolbar}
+
+ )}
)}
{children}
diff --git a/packages/component-library/src/CrossfadeTabPanels/index.tsx b/packages/component-library/src/CrossfadeTabPanels/index.tsx
index ef8dbadeef..92fd232a92 100644
--- a/packages/component-library/src/CrossfadeTabPanels/index.tsx
+++ b/packages/component-library/src/CrossfadeTabPanels/index.tsx
@@ -11,6 +11,7 @@ const AnimatedPanel = motion(UnstyledTabPanel);
type Props = {
items: {
id: string;
+ className?: string;
children: ReactNode;
}[];
};
diff --git a/packages/component-library/src/Drawer/index.module.scss b/packages/component-library/src/Drawer/index.module.scss
index c6a32e96d2..c23c025fa9 100644
--- a/packages/component-library/src/Drawer/index.module.scss
+++ b/packages/component-library/src/Drawer/index.module.scss
@@ -3,39 +3,88 @@
.modalOverlay {
position: fixed;
inset: 0;
- background: rgba(from black r g b / 30%);
+ background: rgba(from black r g b / 50%);
z-index: 1;
+ @include theme.breakpoint("sm") {
+ background: rgba(from black r g b / 30%);
+ }
+
.drawer {
position: fixed;
- top: theme.spacing(4);
- bottom: theme.spacing(4);
- right: theme.spacing(4);
- width: 60%;
- max-width: theme.spacing(180);
+ bottom: 0;
+ left: 1px;
+ right: 1px;
+ max-height: 90%;
outline: none;
background: theme.color("background", "primary");
border: 1px solid theme.color("border");
- border-radius: theme.border-radius("3xl");
+ border-top-left-radius: theme.border-radius("3xl");
+ border-top-right-radius: theme.border-radius("3xl");
display: flex;
flex-flow: column nowrap;
overflow-y: hidden;
- padding-bottom: theme.border-radius("3xl");
+
+ @include theme.breakpoint("sm") {
+ top: theme.spacing(4);
+ bottom: theme.spacing(4);
+ left: unset;
+ 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");
+ }
+
+ .handle {
+ padding: theme.spacing(3) 0;
+ touch-action: none;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+
+ @include theme.breakpoint("sm") {
+ display: none;
+ }
+
+ &::after {
+ display: block;
+ content: "";
+ border-radius: theme.border-radius("full");
+ background: theme.color("background", "secondary");
+ width: theme.spacing(18);
+ height: theme.spacing(1.5);
+ margin: 0 auto;
+ transition: background 40ms linear;
+ }
+
+ &[data-is-pressed]::after {
+ background: theme.color("muted");
+ }
+ }
.heading {
- padding: theme.spacing(4);
- padding-left: theme.spacing(6);
display: flex;
- flex-flow: row nowrap;
- justify-content: space-between;
- align-items: center;
- color: theme.color("heading");
+ padding: theme.spacing(4);
+ flex-flow: column nowrap;
flex: none;
- border-bottom: 1px solid theme.color("border");
+ gap: theme.spacing(2);
+
+ .headingTop {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ @include theme.breakpoint("sm") {
+ border-bottom: 1px solid theme.color("border");
+ padding-left: theme.spacing(6);
+ }
.title {
@include theme.h4;
+ color: theme.color("heading");
display: flex;
flex-flow: row nowrap;
gap: theme.spacing(3);
@@ -46,13 +95,26 @@
flex-flow: row nowrap;
gap: theme.spacing(3);
align-items: center;
+
+ .closeButton {
+ display: none;
+
+ @include theme.breakpoint("sm") {
+ display: unset;
+ }
+ }
}
}
.body {
+ display: grid;
flex: 1;
overflow-y: auto;
- padding: theme.spacing(6);
+ padding: theme.spacing(4);
+
+ @include theme.breakpoint("sm") {
+ padding: theme.spacing(6);
+ }
}
&[data-fill] {
@@ -73,5 +135,21 @@
padding: theme.spacing(4);
}
}
+
+ &[data-hide-heading] {
+ .heading {
+ display: none;
+
+ @include theme.breakpoint("sm") {
+ display: flex;
+ }
+ }
+ }
}
}
+
+// stylelint-disable property-no-unknown
+:export {
+ breakpoint-sm: theme.map-get-strict(theme.$breakpoints, "sm");
+}
+// stylelint-enable property-no-unknown
diff --git a/packages/component-library/src/Drawer/index.tsx b/packages/component-library/src/Drawer/index.tsx
index 918e399226..42e3acca8e 100644
--- a/packages/component-library/src/Drawer/index.tsx
+++ b/packages/component-library/src/Drawer/index.tsx
@@ -1,12 +1,21 @@
"use client";
import { XCircle } from "@phosphor-icons/react/dist/ssr/XCircle";
+import { useMediaQuery } from "@react-hookz/web";
import clsx from "clsx";
-import type { ComponentProps, ReactNode } from "react";
+import { animate, useMotionValue, useMotionValueEvent } from "motion/react";
+import {
+ type ComponentProps,
+ type ReactNode,
+ useState,
+ useRef,
+ useEffect,
+} from "react";
import { Heading } from "react-aria-components";
import styles from "./index.module.scss";
import { Button } from "../Button/index.js";
+import { useMainContentOffset } from "../MainContent/index.js";
import { ModalDialog } from "../ModalDialog/index.js";
export { ModalDialogTrigger as DrawerTrigger } from "../ModalDialog/index.js";
@@ -20,9 +29,11 @@ type OwnProps = {
closeHref?: string | undefined;
footer?: ReactNode | undefined;
headingExtra?: ReactNode | undefined;
+ headingAfter?: ReactNode | undefined;
headingClassName?: string | undefined;
bodyClassName?: string | undefined;
footerClassName?: string | undefined;
+ hideHeading?: boolean | undefined;
};
type Props = Omit<
@@ -42,62 +53,172 @@ export const Drawer = ({
bodyClassName,
footerClassName,
headingExtra,
+ headingAfter,
+ hideHeading,
...props
-}: Props) => (
-
- {(...args) => (
- <>
-
-
- {title}
-
-
- {headingExtra}
-
}
- slot="close"
- hideText
- rounded
- variant="ghost"
- size="sm"
- {...(closeHref && { href: closeHref })}
- >
- Close
-
+}: Props) => {
+ const [, setMainContentOffset] = useMainContentOffset();
+ const modalRef = useRef
(null);
+ const [isDragging, setIsDragging] = useState(false);
+ const [isHandlePressed, setIsHandlePressed] = useState(false);
+ const isLarge = useMediaQuery(
+ `(min-width: ${styles["breakpoint-sm"] ?? ""})`,
+ );
+ const y = useMotionValue("100%");
+
+ useMotionValueEvent(y, "change", (y) => {
+ if (typeof y === "string") {
+ setMainContentOffset(100 - Number.parseInt(y.replace(/%$/, ""), 10));
+ } else if (modalRef.current) {
+ setMainContentOffset(100 - (100 * y) / modalRef.current.offsetHeight);
+ }
+ });
+
+ return (
+ {
+ setIsDragging(true);
+ },
+ onDragEnd: (e, { velocity }, { state }) => {
+ setIsDragging(false);
+ if (e.type !== "pointercancel" && velocity.y > 10) {
+ state.close();
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ animate(y, "0", {
+ type: "inertia",
+ bounceStiffness: 300,
+ bounceDamping: 40,
+ timeConstant: 300,
+ min: 0,
+ max: 0,
+ });
+ }
+ },
+ })}
+ className={clsx(styles.drawer, className)}
+ data-has-footer={footer === undefined ? undefined : ""}
+ data-fill={fill ? "" : undefined}
+ data-hide-heading={hideHeading ? "" : undefined}
+ {...props}
+ >
+ {(...args) => (
+ <>
+ {
+ setMainContentOffset(0);
+ args[0].state.close();
+ }}
+ />
+ {
+ setIsHandlePressed(true);
+ }}
+ onPointerUp={() => {
+ setIsHandlePressed(false);
+ }}
+ data-is-pressed={isHandlePressed || isDragging ? "" : undefined}
+ />
+
+
+
+ {title}
+
+
+ {headingExtra}
+ }
+ slot="close"
+ hideText
+ rounded
+ variant="ghost"
+ size="sm"
+ {...(closeHref && { href: closeHref })}
+ >
+ Close
+
+
+
+ {headingAfter}
-
-
- {typeof children === "function" ? children(...args) : children}
-
- {footer && (
- {footer}
- )}
- >
- )}
-
-);
+
+ {typeof children === "function" ? children(...args) : children}
+
+ {footer && (
+ {footer}
+ )}
+ >
+ )}
+
+ );
+};
+
+type OnResizeProps = {
+ threshold: string | undefined;
+ onResize: () => void;
+};
+
+const OnResize = ({ threshold, onResize }: OnResizeProps) => {
+ const isAboveThreshold = useMediaQuery(`(min-width: ${threshold ?? ""})`, {
+ initializeWithValue: false,
+ });
+ const previousValue = useRef(undefined);
+ useEffect(() => {
+ if (previousValue.current === undefined) {
+ previousValue.current = isAboveThreshold;
+ } else if (isAboveThreshold !== previousValue.current) {
+ previousValue.current = isAboveThreshold;
+ onResize();
+ }
+ }, [isAboveThreshold, onResize]);
+ // eslint-disable-next-line unicorn/no-null
+ return null;
+};
diff --git a/packages/component-library/src/Html/base.scss b/packages/component-library/src/Html/base.scss
index f4bc030dcc..4630903d7e 100644
--- a/packages/component-library/src/Html/base.scss
+++ b/packages/component-library/src/Html/base.scss
@@ -2,8 +2,7 @@
@use "../theme";
:root {
- color: theme.color("foreground");
- background: theme.color("background", "primary");
+ background: black;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
scroll-behavior: smooth;
@@ -11,26 +10,7 @@
}
html {
- // We use `scrollbar-gutter: stable` which prevents the page from jumping when
- // adding or removing the scrollbar. However, react-aria [tries to add a
- // padding](https://github.com/adobe/react-spectrum/issues/5470) to the html
- // element when opening/closing popovers and does not account for
- // `scrollbar-gutter`, and there's no way (yet) to disable that behavior.
- // Forcing the padding to zero here effectively prevents that behavior from
- // causing the page to jump.
- // TODO Remove this when a fix for
- // https://github.com/adobe/react-spectrum/issues/5470 lands in react-aria
- scrollbar-gutter: stable;
padding-right: 0 !important;
-
- // We also have to disable `scrollbar-gutter: stable` when overlays are
- // visible, because chrome leaves an unsightly gap rather than letting the
- // modal backgrop fill the page even though it's fixed position.
- &[data-overlay-visible] {
- scrollbar-gutter: auto;
- padding-right: var(--scrollbar-width) !important;
- overflow: hidden;
- }
}
*::selection {
diff --git a/packages/component-library/src/Html/index.tsx b/packages/component-library/src/Html/index.tsx
index 2b32fe5f5a..88f6cd252f 100644
--- a/packages/component-library/src/Html/index.tsx
+++ b/packages/component-library/src/Html/index.tsx
@@ -1,58 +1,9 @@
-"use client";
-
import { sans } from "@pythnetwork/fonts";
import clsx from "clsx";
-import {
- type ComponentProps,
- type CSSProperties,
- useState,
- useEffect,
-} from "react";
-
-import {
- OverlayVisibleContextProvider,
- useIsOverlayVisible,
-} from "../overlay-visible-context.js";
+import type { ComponentProps } from "react";
import "./base.scss";
-export const Html = (props: ComponentProps<"html">) => (
-
-
-
+export const Html = ({ className, lang, ...props }: ComponentProps<"html">) => (
+
);
-
-const HtmlInner = ({ className, lang, ...props }: ComponentProps<"html">) => {
- const isOverlayVisible = useIsOverlayVisible();
- const scrollbarWidth = useScrollbarWidth();
-
- return (
-
- );
-};
-
-const DEFAULT_SCROLLBAR_WIDTH = 0;
-
-const useScrollbarWidth = () => {
- const [scrollbarWidth, setScrollbarWidth] = useState(DEFAULT_SCROLLBAR_WIDTH);
-
- useEffect(() => {
- const scrollDiv = document.createElement("div");
- scrollDiv.style.overflow = "scroll";
- document.body.append(scrollDiv);
- setScrollbarWidth(scrollDiv.offsetWidth - scrollDiv.clientWidth);
- scrollDiv.remove();
- }, []);
-
- return scrollbarWidth;
-};
diff --git a/packages/component-library/src/InfoBox/index.module.scss b/packages/component-library/src/InfoBox/index.module.scss
index 661cf93c0b..067ff1d346 100644
--- a/packages/component-library/src/InfoBox/index.module.scss
+++ b/packages/component-library/src/InfoBox/index.module.scss
@@ -1,7 +1,6 @@
@use "../theme";
.infoBox {
- grid-column: span 2 / span 2;
background: theme.color("states", "info", "background");
padding: theme.spacing(4);
border-radius: theme.border-radius("xl");
diff --git a/packages/component-library/src/MainContent/index.module.scss b/packages/component-library/src/MainContent/index.module.scss
new file mode 100644
index 0000000000..7392aaf9ec
--- /dev/null
+++ b/packages/component-library/src/MainContent/index.module.scss
@@ -0,0 +1,11 @@
+@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;
+}
diff --git a/packages/component-library/src/MainContent/index.tsx b/packages/component-library/src/MainContent/index.tsx
new file mode 100644
index 0000000000..03fb0dda88
--- /dev/null
+++ b/packages/component-library/src/MainContent/index.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import clsx from "clsx";
+import {
+ type ComponentProps,
+ type CSSProperties,
+ type Dispatch,
+ type SetStateAction,
+ createContext,
+ useState,
+ use,
+} from "react";
+
+import styles from "./index.module.scss";
+import { OverlayVisibleContext } from "../overlay-visible-context.js";
+
+const MainContentOffsetContext = createContext<
+ undefined | [number, Dispatch
>]
+>(undefined);
+
+export const MainContent = ({ className, ...props }: ComponentProps<"div">) => {
+ const overlayVisibleState = useState(false);
+ const offset = useState(0);
+
+ return (
+
+
+
+
+
+ );
+};
+
+export const useMainContentOffset = () => {
+ const value = use(MainContentOffsetContext);
+ if (value === undefined) {
+ throw new MainContentNotInitializedError();
+ } else {
+ return value;
+ }
+};
+
+class MainContentNotInitializedError extends Error {
+ constructor() {
+ super("This component must be contained within a ");
+ this.name = "MainContentNotInitializedError";
+ }
+}
diff --git a/packages/component-library/src/MainNavTabs/index.module.scss b/packages/component-library/src/MainNavTabs/index.module.scss
index d3cfc7f585..938da86510 100644
--- a/packages/component-library/src/MainNavTabs/index.module.scss
+++ b/packages/component-library/src/MainNavTabs/index.module.scss
@@ -39,6 +39,7 @@
&[data-selectable] .tab[data-selected] {
pointer-events: auto;
+ -webkit-tap-highlight-color: transparent;
&[data-hovered] .bubble {
background-color: theme.color("button", "solid", "background", "hover");
diff --git a/packages/component-library/src/ModalDialog/index.tsx b/packages/component-library/src/ModalDialog/index.tsx
index 81797deef7..5e7716a6ad 100644
--- a/packages/component-library/src/ModalDialog/index.tsx
+++ b/packages/component-library/src/ModalDialog/index.tsx
@@ -1,6 +1,6 @@
"use client";
-import { motion } from "motion/react";
+import { motion, type PanInfo } from "motion/react";
import {
type ComponentProps,
type Dispatch,
@@ -12,6 +12,7 @@ import {
useEffect,
} from "react";
import {
+ type ModalRenderProps,
Modal,
ModalOverlay,
Dialog,
@@ -79,6 +80,11 @@ type OwnProps = Pick, "children"> &
| ComponentProps["variants"]
| undefined;
onCloseFinish?: (() => void) | undefined;
+ onDragEnd?: (
+ e: MouseEvent | TouchEvent | PointerEvent,
+ panInfo: PanInfo,
+ modalState: ModalRenderProps,
+ ) => void;
};
type Props = Omit, keyof OwnProps> &
@@ -91,6 +97,7 @@ export const ModalDialog = ({
overlayClassName,
overlayVariants,
children,
+ onDragEnd,
...props
}: Props) => {
const contextAnimationState = use(ModalAnimationContext);
@@ -142,7 +149,14 @@ export const ModalDialog = ({
>
{(...args) => (
-
+ {
+ onDragEnd(e, info, args[0]);
+ },
+ })}
+ >
{typeof children === "function" ? children(...args) : children}
)}
diff --git a/packages/component-library/src/Paginator/index.module.scss b/packages/component-library/src/Paginator/index.module.scss
index 5679663437..25c692ef82 100644
--- a/packages/component-library/src/Paginator/index.module.scss
+++ b/packages/component-library/src/Paginator/index.module.scss
@@ -3,14 +3,22 @@
.paginator {
display: flex;
flex-flow: row nowrap;
- justify-content: space-between;
+ justify-content: center;
+
+ @include theme.breakpoint("sm") {
+ justify-content: space-between;
+ }
.pageSizeSelect {
- display: flex;
+ display: none;
flex-flow: row nowrap;
align-items: center;
gap: theme.spacing(1);
+ @include theme.breakpoint("sm") {
+ display: flex;
+ }
+
.loadingIndicator {
width: theme.spacing(4);
height: theme.spacing(4);
diff --git a/packages/component-library/src/SearchInput/index.module.scss b/packages/component-library/src/SearchInput/index.module.scss
index bf3331513a..622e8f4cc7 100644
--- a/packages/component-library/src/SearchInput/index.module.scss
+++ b/packages/component-library/src/SearchInput/index.module.scss
@@ -6,9 +6,12 @@
gap: theme.spacing(2);
position: relative;
display: inline-block;
- width: calc(theme.spacing(1) * var(--width));
color: theme.color("button", "outline", "foreground");
+ &[data-static-width] {
+ width: calc(theme.spacing(1) * var(--width));
+ }
+
.input {
display: inline-block;
width: 100%;
diff --git a/packages/component-library/src/SearchInput/index.tsx b/packages/component-library/src/SearchInput/index.tsx
index 815789217e..c8175f8de6 100644
--- a/packages/component-library/src/SearchInput/index.tsx
+++ b/packages/component-library/src/SearchInput/index.tsx
@@ -16,7 +16,7 @@ export const SIZES = ["xs", "sm", "md", "lg"] as const;
type Props = ComponentProps & {
label?: string | undefined;
size?: (typeof SIZES)[number] | undefined;
- width: number;
+ width?: number | undefined;
isPending?: boolean | undefined;
placeholder?: string;
};
@@ -33,8 +33,9 @@ export const SearchInput = ({
diff --git a/packages/component-library/src/SingleToggleGroup/index.module.scss b/packages/component-library/src/SingleToggleGroup/index.module.scss
index 5bc1f0d035..010b94b19e 100644
--- a/packages/component-library/src/SingleToggleGroup/index.module.scss
+++ b/packages/component-library/src/SingleToggleGroup/index.module.scss
@@ -27,6 +27,7 @@
&[data-selectable] {
pointer-events: auto;
+ -webkit-tap-highlight-color: transparent;
&[data-hovered] .bubble {
background-color: theme.color(
diff --git a/packages/component-library/src/TabList/index.module.scss b/packages/component-library/src/TabList/index.module.scss
index e5a67cbb5e..0cda45c273 100644
--- a/packages/component-library/src/TabList/index.module.scss
+++ b/packages/component-library/src/TabList/index.module.scss
@@ -4,15 +4,22 @@
border-bottom: 1px solid theme.color("border");
.tabList {
- @include theme.max-width;
-
display: flex;
flex-flow: row nowrap;
gap: theme.spacing(2);
padding-bottom: theme.spacing(1);
+ @include theme.max-width;
+
.tab {
position: relative;
+ flex: 1 0 0;
+ width: 0;
+
+ @include theme.breakpoint("sm") {
+ flex: unset;
+ width: unset;
+ }
.underline {
position: absolute;
diff --git a/packages/component-library/src/Table/index.module.scss b/packages/component-library/src/Table/index.module.scss
index 09e95b66b9..26ea5f1308 100644
--- a/packages/component-library/src/Table/index.module.scss
+++ b/packages/component-library/src/Table/index.module.scss
@@ -164,6 +164,7 @@
outline: theme.spacing(0.5) solid transparent;
outline-offset: -#{theme.spacing(0.5)};
transition: outline-color 100ms linear;
+ -webkit-tap-highlight-color: transparent;
&[data-focus-visible] {
outline: theme.spacing(0.5) solid theme.color("focus");
diff --git a/packages/component-library/src/overlay-visible-context.tsx b/packages/component-library/src/overlay-visible-context.tsx
index f5fecf9818..11f1e1f83e 100644
--- a/packages/component-library/src/overlay-visible-context.tsx
+++ b/packages/component-library/src/overlay-visible-context.tsx
@@ -1,9 +1,7 @@
import {
- type ComponentProps,
type Dispatch,
type SetStateAction,
createContext,
- useState,
useCallback,
use,
} from "react";
@@ -12,13 +10,6 @@ export const OverlayVisibleContext = createContext<
[boolean, Dispatch>] | undefined
>(undefined);
-export const OverlayVisibleContextProvider = (
- props: Omit, "value">,
-) => {
- const overlayVisibleState = useState(false);
- return ;
-};
-
const useOverlayVisible = () => {
const overlayVisible = use(OverlayVisibleContext);
if (overlayVisible === undefined) {
@@ -27,7 +18,6 @@ const useOverlayVisible = () => {
return overlayVisible;
};
-export const useIsOverlayVisible = () => useOverlayVisible()[0];
export const useSetOverlayVisible = () => {
const setOverlayVisible = useOverlayVisible()[1];
return {
diff --git a/packages/component-library/src/theme.scss b/packages/component-library/src/theme.scss
index 64816348b1..9ea277b3b5 100644
--- a/packages/component-library/src/theme.scss
+++ b/packages/component-library/src/theme.scss
@@ -719,16 +719,24 @@ $button-sizes: (
}
}
-$max-width: 96rem;
+$max-width: spacing(372);
+$max-width-padding: var(--max-width-padding);
@mixin max-width {
- margin: 0 auto;
- max-width: min(
- $max-width,
- calc(200vw - spacing(12) - 100% - var(--scrollbar-width))
- );
- padding: 0 spacing(6);
- box-sizing: content-box;
+ & {
+ --max-width-padding: #{spacing(4)};
+
+ margin-left: auto;
+ margin-right: auto;
+ padding-left: $max-width-padding;
+ padding-right: $max-width-padding;
+ width: 100%;
+ max-width: $max-width;
+ }
+
+ @include breakpoint("sm") {
+ --max-width-padding: #{spacing(6)};
+ }
}
@mixin row {
@@ -770,12 +778,14 @@ $elevations: (
}
@mixin h3 {
- font-size: font-size("2xl");
- font-style: normal;
- font-weight: font-weight("medium");
+ @include text("xl", "semibold");
+
line-height: 125%;
letter-spacing: letter-spacing("tighter");
- margin: 0;
+
+ @include breakpoint("sm") {
+ font-size: font-size("2xl");
+ }
}
@mixin h4 {
@@ -794,3 +804,17 @@ $elevations: (
font-style: normal;
line-height: 1;
}
+
+$breakpoints: (
+ "sm": 640px,
+ "md": 768px,
+ "lg": 1024px,
+ "xl": 1280px,
+ "2xl": 1536px,
+);
+
+@mixin breakpoint($point) {
+ @media (min-width: map-get-strict($breakpoints, $point)) {
+ @content;
+ }
+}
diff --git a/packages/component-library/src/unstyled/GridList/index.tsx b/packages/component-library/src/unstyled/GridList/index.tsx
new file mode 100644
index 0000000000..9406b3ba7b
--- /dev/null
+++ b/packages/component-library/src/unstyled/GridList/index.tsx
@@ -0,0 +1,3 @@
+"use client";
+
+export { GridList, GridListItem } from "react-aria-components";
diff --git a/packages/component-library/stylelint.config.js b/packages/component-library/stylelint.config.js
index f0c0f5ca97..d1a0ed4fc6 100644
--- a/packages/component-library/stylelint.config.js
+++ b/packages/component-library/stylelint.config.js
@@ -10,6 +10,12 @@ const config = {
`Expected class selector "${selector}" to be camel-case`,
},
],
+ "selector-pseudo-class-no-unknown": [
+ true,
+ {
+ ignorePseudoClasses: ["global", "export"],
+ },
+ ],
},
};
export default config;
diff --git a/packages/next-root/scss.d.ts b/packages/next-root/scss.d.ts
new file mode 100644
index 0000000000..1526e3d649
--- /dev/null
+++ b/packages/next-root/scss.d.ts
@@ -0,0 +1,4 @@
+declare module "*.scss" {
+ const content: Record;
+ export = content;
+}
diff --git a/packages/next-root/src/index.tsx b/packages/next-root/src/index.tsx
index 769f904abb..5f3ba3a689 100644
--- a/packages/next-root/src/index.tsx
+++ b/packages/next-root/src/index.tsx
@@ -1,5 +1,6 @@
import { GoogleAnalytics } from "@next/third-parties/google";
import { LoggerProvider } from "@pythnetwork/app-logger/provider";
+import { MainContent } from "@pythnetwork/component-library/MainContent";
import dynamic from "next/dynamic";
import { ThemeProvider } from "next-themes";
import type { ComponentProps, ReactNode } from "react";
@@ -46,7 +47,9 @@ export const Root = ({
{...props}
>
- {children}
+
+ {children}
+
{googleAnalyticsId && }
{amplitudeApiKey && }