diff --git a/apps/insights/.env.development b/apps/insights/.env.development new file mode 100644 index 0000000000..addc9dd5a0 --- /dev/null +++ b/apps/insights/.env.development @@ -0,0 +1 @@ +DISABLE_ACCESSIBILITY_REPORTING=true \ No newline at end of file diff --git a/apps/insights/.gitignore b/apps/insights/.gitignore index 9d2ee2a739..e6fc78b8ba 100644 --- a/apps/insights/.gitignore +++ b/apps/insights/.gitignore @@ -1 +1,2 @@ .env*.local +.env*.development \ No newline at end of file diff --git a/apps/insights/src/components/CardTitle/index.module.scss b/apps/insights/src/components/CardTitle/index.module.scss new file mode 100644 index 0000000000..ccb19f4b02 --- /dev/null +++ b/apps/insights/src/components/CardTitle/index.module.scss @@ -0,0 +1,25 @@ +@use "@pythnetwork/component-library/theme"; + +.cardTitle { + display: flex; + flex-flow: row nowrap; + gap: theme.spacing(3); + align-items: center; + justify-content: flex-start; + .title { + color: theme.color("heading"); + display: flex; + flex-flow: row nowrap; + gap: theme.spacing(3); + align-items: center; + @include theme.text("base", "semibold"); + @include theme.breakpoint("md") { + @include theme.text("lg", "semibold"); + } + } + .icon { + font-size: theme.spacing(6); + height: theme.spacing(6); + color: theme.color("button", "primary", "background", "normal"); + } +} diff --git a/apps/insights/src/components/CardTitle/index.tsx b/apps/insights/src/components/CardTitle/index.tsx new file mode 100644 index 0000000000..902235689e --- /dev/null +++ b/apps/insights/src/components/CardTitle/index.tsx @@ -0,0 +1,20 @@ +import clsx from "clsx"; +import type { ComponentProps, ReactNode } from "react"; + +import styles from "./index.module.scss"; + +type CardTitleProps = { + children: ReactNode; + icon?: ReactNode | undefined; + badge?: ReactNode; +} & ComponentProps<"div">; + +export const CardTitle = ({ children, icon, badge, ...props }: CardTitleProps) => { + return ( + <div className={clsx(styles.cardTitle, props.className)} {...props}> + {icon && <div className={styles.icon}>{icon}</div>} + <h2 className={styles.title}>{children}</h2> + {badge} + </div> + ) +} \ No newline at end of file diff --git a/apps/insights/src/components/CopyButton/index.tsx b/apps/insights/src/components/CopyButton/index.tsx index f002ccbbbc..53de57de10 100644 --- a/apps/insights/src/components/CopyButton/index.tsx +++ b/apps/insights/src/components/CopyButton/index.tsx @@ -27,7 +27,6 @@ export const CopyButton = ({ text, children, className, ...props }: Props) => { const [isCopied, setIsCopied] = useState(false); const logger = useLogger(); const copy = useCallback(() => { - // eslint-disable-next-line n/no-unsupported-features/node-builtins navigator.clipboard .writeText(text) .then(() => { diff --git a/apps/insights/src/components/MobileMenu/mobile-menu.module.scss b/apps/insights/src/components/MobileMenu/mobile-menu.module.scss new file mode 100644 index 0000000000..ae47132655 --- /dev/null +++ b/apps/insights/src/components/MobileMenu/mobile-menu.module.scss @@ -0,0 +1,54 @@ +@use "@pythnetwork/component-library/theme"; + +.mobileMenuTrigger { + display: block; + + @include theme.breakpoint("md") { + display: none; + } +} + +.mobileMenuOverlay { + background: rgb(0 0 0 / 40%); + position: fixed; + inset: 0; + z-index: 999; +} + +.mobileMenuContainer { + border-top-left-radius: theme.border-radius("2xl"); + border-top-right-radius: theme.border-radius("2xl"); + background: theme.color("background", "modal"); + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 1rem; + display: flex; + flex-flow: column nowrap; + gap: theme.spacing(4); +} + +.mobileMenuHandle { + background: theme.color("background", "secondary"); + width: 33%; + height: 6px; + border-radius: theme.border-radius("full"); + align-self: center; +} + +.mobileThemeSwitcher { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + align-items: center; +} + +.mobileThemeSwitcherFeedback { + display: flex; + flex-flow: row nowrap; + align-items: center; + gap: theme.spacing(3); + text-transform: capitalize; + font-weight: theme.font-weight("medium"); +} diff --git a/apps/insights/src/components/MobileMenu/mobile-menu.tsx b/apps/insights/src/components/MobileMenu/mobile-menu.tsx new file mode 100644 index 0000000000..4e5932b425 --- /dev/null +++ b/apps/insights/src/components/MobileMenu/mobile-menu.tsx @@ -0,0 +1,29 @@ +"use client"; +import { List } from "@phosphor-icons/react/dist/ssr/List"; +import { Button } from "@pythnetwork/component-library/Button"; +import clsx from "clsx"; +import { useState, type ComponentProps } from "react"; + +import styles from "./mobile-menu.module.scss"; + +export const MobileMenu = ({ className, ...props }: ComponentProps<"div">) => { + const [isOpen, setIsOpen] = useState(false); + + const toggleMenu = () => { + setIsOpen(!isOpen); + }; + + return ( + <div className={clsx(styles.mobileMenuTrigger, className)} {...props}> + <Button + variant="ghost" + size="sm" + afterIcon={List} + rounded + onPress={toggleMenu} + > + Menu + </Button> + </div> + ); +}; diff --git a/apps/insights/src/components/MobileNavigation/mobile-navigation.module.scss b/apps/insights/src/components/MobileNavigation/mobile-navigation.module.scss new file mode 100644 index 0000000000..00e1c7ceec --- /dev/null +++ b/apps/insights/src/components/MobileNavigation/mobile-navigation.module.scss @@ -0,0 +1,12 @@ +@use "@pythnetwork/component-library/theme"; + +.mobileNavigation { + display: block; + padding: theme.spacing(2); + background: theme.color("background", "primary"); + border-top: 1px solid theme.color("background", "secondary"); + + @include theme.breakpoint("md") { + display: none; + } +} diff --git a/apps/insights/src/components/MobileNavigation/mobile-navigation.tsx b/apps/insights/src/components/MobileNavigation/mobile-navigation.tsx new file mode 100644 index 0000000000..0ee7773a4b --- /dev/null +++ b/apps/insights/src/components/MobileNavigation/mobile-navigation.tsx @@ -0,0 +1,9 @@ +import styles from "./mobile-navigation.module.scss"; +import { MainNavTabs } from "../Root/tabs"; +export const MobileNavigation = () => { + return ( + <div className={styles.mobileNavigation}> + <MainNavTabs /> + </div> + ); +}; diff --git a/apps/insights/src/components/Overview/index.tsx b/apps/insights/src/components/Overview/index.tsx index 3185cbd16d..1d9bf083d2 100644 --- a/apps/insights/src/components/Overview/index.tsx +++ b/apps/insights/src/components/Overview/index.tsx @@ -1,7 +1,9 @@ -import styles from "./index.module.scss"; +import { Card } from "@pythnetwork/component-library/Card"; + +import { PageLayout } from "../PageLayout/page-layout"; export const Overview = () => ( - <div className={styles.overview}> - <h1 className={styles.header}>Overview</h1> - </div> + <PageLayout title={"Overview"}> + <Card title="Overview"></Card> + </PageLayout> ); diff --git a/apps/insights/src/components/PageLayout/page-layout.module.scss b/apps/insights/src/components/PageLayout/page-layout.module.scss new file mode 100644 index 0000000000..442bb61b8e --- /dev/null +++ b/apps/insights/src/components/PageLayout/page-layout.module.scss @@ -0,0 +1,24 @@ +@use "@pythnetwork/component-library/theme"; + +.pageLayout { + @include theme.max-width; + display: flex; + gap: theme.spacing(6); + flex-direction: column; + + .pageTitleContainer { + display: flex; + flex-flow: row nowrap; + gap: theme.spacing(3); + width: 100%; + align-items: center; + justify-content: space-between; + + .pageTitle { + @include theme.h3; + color: theme.color("heading"); + font-weight: theme.font-weight("semibold"); + flex-grow: 1; + } + } +} diff --git a/apps/insights/src/components/PageLayout/page-layout.tsx b/apps/insights/src/components/PageLayout/page-layout.tsx new file mode 100644 index 0000000000..b70e04c255 --- /dev/null +++ b/apps/insights/src/components/PageLayout/page-layout.tsx @@ -0,0 +1,15 @@ +import type { ReactNode } from "react"; + +import styles from "./page-layout.module.scss"; + +export const PageLayout = ({ children, title, actions }: { children: ReactNode; title: ReactNode, actions?: ReactNode }) => { + return ( + <div className={styles.pageLayout}> + <div className={styles.pageTitleContainer}> + <h1 className={styles.pageTitle}>{title}</h1> + {actions && <div className={styles.actions}>{actions}</div>} + </div> + {children} + </div> + ) +} \ No newline at end of file diff --git a/apps/insights/src/components/PriceFeedTag/index.module.scss b/apps/insights/src/components/PriceFeedTag/index.module.scss index 0ca6b99f12..fc92baf48e 100644 --- a/apps/insights/src/components/PriceFeedTag/index.module.scss +++ b/apps/insights/src/components/PriceFeedTag/index.module.scss @@ -1,8 +1,8 @@ @use "@pythnetwork/component-library/theme"; .priceFeedTag { - display: flex; - flex-flow: row nowrap; + display: grid; + grid-template-columns: theme.spacing(10) 1fr; gap: theme.spacing(3); align-items: center; @@ -13,6 +13,8 @@ } .nameAndDescription { + width: 100%; + position: relative; display: flex; flex-flow: column nowrap; gap: theme.spacing(1.5); @@ -52,6 +54,8 @@ color: theme.color("muted"); overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; // Add this line + width: 100%; @include theme.text("xs", "medium"); } diff --git a/apps/insights/src/components/PriceFeeds/index.module.scss b/apps/insights/src/components/PriceFeeds/index.module.scss index d5927a894b..dd081b90e7 100644 --- a/apps/insights/src/components/PriceFeeds/index.module.scss +++ b/apps/insights/src/components/PriceFeeds/index.module.scss @@ -1,66 +1,53 @@ @use "@pythnetwork/component-library/theme"; -.priceFeeds { - @include theme.max-width; +.toolbarContainer { + display: flex; + flex-flow: row nowrap; + gap: theme.spacing(2); +} + +.feedKey { + margin: 0 -#{theme.button-padding("xs", true)}; +} - .header { - @include theme.h3; +.featuredFeeds { + display: flex; + flex-flow: column nowrap; + align-items: stretch; - color: theme.color("heading"); - font-weight: theme.font-weight("semibold"); + @include theme.breakpoint("md") { + flex-flow: row nowrap; + + & > * { + flex: 1 1 0px; + width: 0; + } } +} + +.featuredFeeds { + gap: theme.spacing(1); - .body { + .feedCardContents { display: flex; flex-flow: column nowrap; + justify-content: space-between; + align-items: stretch; + padding: theme.spacing(3); gap: theme.spacing(6); - margin-top: theme.spacing(6); - - .feedKey { - margin: 0 -#{theme.button-padding("xs", true)}; - } - .featuredFeeds, - .stats { + .prices { display: flex; flex-flow: row nowrap; + justify-content: space-between; align-items: center; + color: theme.color("heading"); + font-weight: theme.font-weight("medium"); + line-height: 1; + font-size: theme.font-size("base"); - & > * { - flex: 1 1 0px; - width: 0; - } - } - - .stats { - gap: theme.spacing(6); - } - - .featuredFeeds { - gap: theme.spacing(1); - - .feedCardContents { - display: flex; - flex-flow: column nowrap; - justify-content: space-between; - align-items: stretch; - padding: theme.spacing(3); - gap: theme.spacing(6); - - .prices { - display: flex; - flex-flow: row nowrap; - justify-content: space-between; - align-items: center; - color: theme.color("heading"); - font-weight: theme.font-weight("medium"); - line-height: 1; - font-size: theme.font-size("base"); - - .changePercent { - font-size: theme.font-size("sm"); - } - } + .changePercent { + font-size: theme.font-size("sm"); } } } diff --git a/apps/insights/src/components/PriceFeeds/index.tsx b/apps/insights/src/components/PriceFeeds/index.tsx index 16691c0844..05f5d1f35a 100644 --- a/apps/insights/src/components/PriceFeeds/index.tsx +++ b/apps/insights/src/components/PriceFeeds/index.tsx @@ -19,10 +19,13 @@ import styles from "./index.module.scss"; import { PriceFeedsCard } from "./price-feeds-card"; import { Cluster, getData } from "../../services/pyth"; import { priceFeeds as priceFeedsStaticConfig } from "../../static-data/price-feeds"; +import { CardTitle } from "../CardTitle"; import { YesterdaysPricesProvider, ChangePercent } from "../ChangePercent"; import { LivePrice } from "../LivePrices"; +import { PageLayout } from "../PageLayout/page-layout"; import { PriceFeedIcon } from "../PriceFeedIcon"; import { PriceFeedTag } from "../PriceFeedTag"; +import { Stats } from "../Stats"; const PRICE_FEEDS_ANCHOR = "priceFeeds"; @@ -45,99 +48,94 @@ export const PriceFeeds = async () => { ); return ( - <div className={styles.priceFeeds}> - <h1 className={styles.header}>Price Feeds</h1> - <div className={styles.body}> - <section className={styles.stats}> - <StatCard - variant="primary" - header="Active Feeds" - stat={priceFeeds.activeFeeds.length} - href={`#${PRICE_FEEDS_ANCHOR}`} - corner={<ArrowLineDown />} - /> - <StatCard - header="Frequency" - stat={priceFeedsStaticConfig.updateFrequency} - /> + <PageLayout title={"Price Feeds"}> + <Stats> + <StatCard + variant="primary" + header="Active Feeds" + stat={priceFeeds.activeFeeds.length} + href={`#${PRICE_FEEDS_ANCHOR}`} + corner={<ArrowLineDown />} + /> + <StatCard + header="Frequency" + stat={priceFeedsStaticConfig.updateFrequency} + /> + <StatCard + header="Active Chains" + stat={priceFeedsStaticConfig.activeChains} + href="https://docs.pyth.network/price-feeds/contract-addresses" + target="_blank" + corner={<ArrowSquareOut weight="fill" />} + /> + <AssetClassesDrawer numFeedsByAssetClass={numFeedsByAssetClass}> <StatCard - header="Active Chains" - stat={priceFeedsStaticConfig.activeChains} - href="https://docs.pyth.network/price-feeds/contract-addresses" - target="_blank" - corner={<ArrowSquareOut weight="fill" />} + header="Asset Classes" + stat={Object.keys(numFeedsByAssetClass).length} + corner={<Info weight="fill" />} /> - <AssetClassesDrawer numFeedsByAssetClass={numFeedsByAssetClass}> - <StatCard - header="Asset Classes" - stat={Object.keys(numFeedsByAssetClass).length} - corner={<Info weight="fill" />} - /> - </AssetClassesDrawer> - </section> - <YesterdaysPricesProvider - feeds={Object.fromEntries( - featuredRecentlyAdded.map(({ symbol, product }) => [ - symbol, - product.price_account, - ]), - )} - > - <FeaturedFeedsCard - title="Recently Added" - icon={<StackPlus />} - feeds={featuredRecentlyAdded} - showPrices - linkFeeds - /> - </YesterdaysPricesProvider> + </AssetClassesDrawer> + </Stats> + <YesterdaysPricesProvider + feeds={Object.fromEntries( + featuredRecentlyAdded.map(({ symbol, product }) => [ + symbol, + product.price_account, + ]), + )} + > <FeaturedFeedsCard - title="Coming Soon" - icon={<ClockCountdown />} - feeds={featuredComingSoon} - toolbar={ - <DrawerTrigger> - <Button size="xs" variant="outline"> - Show all - </Button> - <Drawer - fill - className={styles.comingSoonCard ?? ""} - title={ - <> - <span>Coming Soon</span> - <Badge>{priceFeeds.comingSoon.length}</Badge> - </> - } - > - <ComingSoonList - comingSoonFeeds={priceFeeds.comingSoon.map((feed) => ({ - id: feed.product.price_account, - displaySymbol: feed.product.display_symbol, - assetClass: feed.product.asset_type, - icon: ( - <PriceFeedIcon symbol={feed.product.display_symbol} /> - ), - }))} - /> - </Drawer> - </DrawerTrigger> - } - /> - <PriceFeedsCard - id={PRICE_FEEDS_ANCHOR} - priceFeeds={priceFeeds.activeFeeds.map((feed) => ({ - symbol: feed.symbol, - icon: <PriceFeedIcon symbol={feed.product.display_symbol} />, - id: feed.product.price_account, - displaySymbol: feed.product.display_symbol, - assetClass: feed.product.asset_type, - exponent: feed.price.exponent, - numQuoters: feed.price.numQuoters, - }))} + title={<CardTitle icon={<StackPlus />}>Recently added</CardTitle>} + feeds={featuredRecentlyAdded} + showPrices + linkFeeds /> - </div> - </div> + </YesterdaysPricesProvider> + <FeaturedFeedsCard + title={<CardTitle icon={<ClockCountdown />}>Coming soon</CardTitle>} + feeds={featuredComingSoon} + action={ + <DrawerTrigger> + <Button size="sm" variant="outline"> + Show all + </Button> + <Drawer + fill + className={styles.comingSoonCard ?? ""} + title={ + <> + <span>Coming Soon</span> + <Badge>{priceFeeds.comingSoon.length}</Badge> + </> + } + > + <ComingSoonList + comingSoonFeeds={priceFeeds.comingSoon.map((feed) => ({ + id: feed.product.price_account, + displaySymbol: feed.product.display_symbol, + assetClass: feed.product.asset_type, + icon: ( + <PriceFeedIcon symbol={feed.product.display_symbol} /> + ), + }))} + /> + </Drawer> + </DrawerTrigger> + } + /> + <PriceFeedsCard + id={PRICE_FEEDS_ANCHOR} + priceFeeds={priceFeeds.activeFeeds.map((feed) => ({ + symbol: feed.symbol, + icon: <PriceFeedIcon symbol={feed.product.display_symbol} />, + id: feed.product.price_account, + displaySymbol: feed.product.display_symbol, + assetClass: feed.product.asset_type, + exponent: feed.price.exponent, + numQuoters: feed.price.numQuoters, + }))} + /> + </PageLayout> ); }; diff --git a/apps/insights/src/components/PriceFeeds/price-feed-items.module.scss b/apps/insights/src/components/PriceFeeds/price-feed-items.module.scss new file mode 100644 index 0000000000..9825c9e6e4 --- /dev/null +++ b/apps/insights/src/components/PriceFeeds/price-feed-items.module.scss @@ -0,0 +1,22 @@ +@use "@pythnetwork/component-library/theme"; + +.priceFeedItemsWrapper { + display: flex; + flex-flow: column nowrap; + gap: 0; + border-radius: theme.border-radius("xl"); + background: theme.color("background", "card-secondary"); + + @include theme.breakpoint("md") { + display: none; + } +} + +.priceFeedItem { + padding: theme.spacing(4); + position: relative; + + &:not(:last-child) { + border-bottom: 1px solid theme.color("background", "secondary"); + } +} diff --git a/apps/insights/src/components/PriceFeeds/price-feed-items.tsx b/apps/insights/src/components/PriceFeeds/price-feed-items.tsx new file mode 100644 index 0000000000..3759311ca7 --- /dev/null +++ b/apps/insights/src/components/PriceFeeds/price-feed-items.tsx @@ -0,0 +1,32 @@ +import styles from "./price-feed-items.module.scss"; +import { PriceFeedTag } from "../PriceFeedTag"; +import { StructuredList } from "../StructuredList"; + +export const PriceFeedItems = () => { + return ( + <div className={styles.priceFeedItemsWrapper}> + {Array.from({ length: 20 }).map((_, index) => { + return ( + <div className={styles.priceFeedItem} key={index}> + <StructuredList + items={[ + { + label: <PriceFeedTag compact isLoading />, + value: "$32,323.22", + }, + { + label: "Last Price", + value: "$10,000.00", + }, + { + label: "Last Updated", + value: "2022-01-01", + }, + ]} + /> + </div> + ); + })} + </div> + ); +}; diff --git a/apps/insights/src/components/PriceFeeds/price-feeds-card.tsx b/apps/insights/src/components/PriceFeeds/price-feeds-card.tsx index 0fe94882fd..3d632740ed 100644 --- a/apps/insights/src/components/PriceFeeds/price-feeds-card.tsx +++ b/apps/insights/src/components/PriceFeeds/price-feeds-card.tsx @@ -16,7 +16,9 @@ import { useQueryState, parseAsString } from "nuqs"; import { type ReactNode, Suspense, useCallback, useMemo } from "react"; import { useFilter, useCollator } from "react-aria"; +import { PriceFeedItems } from "./price-feed-items"; import { useQueryParamFilterPagination } from "../../use-query-param-filter-pagination"; +import { CardTitle } from "../CardTitle"; import { FeedKey } from "../FeedKey"; import { SKELETON_WIDTH, @@ -195,49 +197,56 @@ type PriceFeedsCardContents = Pick<Props, "id"> & ( | { isLoading: true } | { - isLoading?: false; - numResults: number; - search: string; - sortDescriptor: SortDescriptor; - onSortChange: (newSort: SortDescriptor) => void; - assetClass: string; - assetClasses: string[]; - numPages: number; - page: number; - pageSize: number; - onSearchChange: (newSearch: string) => void; - onAssetClassChange: (newAssetClass: string) => void; - onPageSizeChange: (newPageSize: number) => void; - onPageChange: (newPage: number) => void; - mkPageLink: (page: number) => string; - rows: RowConfig< - | "priceFeedName" - | "assetClass" - | "priceFeedId" - | "price" - | "confidenceInterval" - | "exponent" - | "numPublishers" - >[]; - } + isLoading?: false; + numResults: number; + search: string; + sortDescriptor: SortDescriptor; + onSortChange: (newSort: SortDescriptor) => void; + assetClass: string; + assetClasses: string[]; + numPages: number; + page: number; + pageSize: number; + onSearchChange: (newSearch: string) => void; + onAssetClassChange: (newAssetClass: string) => void; + onPageSizeChange: (newPageSize: number) => void; + onPageChange: (newPage: number) => void; + mkPageLink: (page: number) => string; + rows: RowConfig< + | "priceFeedName" + | "assetClass" + | "priceFeedId" + | "price" + | "confidenceInterval" + | "exponent" + | "numPublishers" + >[]; + } ); const PriceFeedsCardContents = ({ id, ...props }: PriceFeedsCardContents) => ( <Card id={id} - icon={<ChartLine />} title={ - <> - <span>Price Feeds</span> - {!props.isLoading && ( - <Badge style="filled" variant="neutral" size="md"> - {props.numResults} - </Badge> - )} - </> + <CardTitle icon={<ChartLine />} badge={!props.isLoading && ( + <Badge style="filled" variant="neutral" size="md"> + {props.numResults} + </Badge> + )}>Price Feeds</CardTitle> } toolbar={ <> + <SearchInput + size="sm" + width={50} + placeholder="Feed symbol" + {...(props.isLoading + ? { isPending: true, isDisabled: true } + : { + value: props.search, + onChange: props.onSearchChange, + })} + /> <Select<string> label="Asset Class" size="sm" @@ -246,29 +255,18 @@ const PriceFeedsCardContents = ({ id, ...props }: PriceFeedsCardContents) => ( {...(props.isLoading ? { isPending: true, options: [], buttonLabel: "Asset Class" } : { - optionGroups: [ - { name: "All", options: [""] }, - { name: "Asset classes", options: props.assetClasses }, - ], - hideGroupLabel: true, - show: (value) => (value === "" ? "All" : value), - placement: "bottom end", - buttonLabel: - props.assetClass === "" ? "Asset Class" : props.assetClass, - selectedKey: props.assetClass, - onSelectionChange: props.onAssetClassChange, - })} - /> - <SearchInput - size="sm" - width={50} - placeholder="Feed symbol" - {...(props.isLoading - ? { isPending: true, isDisabled: true } - : { - value: props.search, - onChange: props.onSearchChange, - })} + optionGroups: [ + { name: "All", options: [""] }, + { name: "Asset classes", options: props.assetClasses }, + ], + hideGroupLabel: true, + show: (value) => (value === "" ? "All" : value), + placement: "bottom end", + buttonLabel: + props.assetClass === "" ? "Asset Class" : props.assetClass, + selectedKey: props.assetClass, + onSelectionChange: props.onAssetClassChange, + })} /> </> } @@ -286,6 +284,8 @@ const PriceFeedsCardContents = ({ id, ...props }: PriceFeedsCardContents) => ( ), })} > + <PriceFeedItems /> + <Table rounded fill @@ -344,21 +344,21 @@ const PriceFeedsCardContents = ({ id, ...props }: PriceFeedsCardContents) => ( ]} {...(props.isLoading ? { - isLoading: true, - } + isLoading: true, + } : { - rows: props.rows, - sortDescriptor: props.sortDescriptor, - onSortChange: props.onSortChange, - emptyState: ( - <NoResults - query={props.search} - onClearSearch={() => { - props.onSearchChange(""); - }} - /> - ), - })} + rows: props.rows, + sortDescriptor: props.sortDescriptor, + onSortChange: props.onSortChange, + emptyState: ( + <NoResults + query={props.search} + onClearSearch={() => { + props.onSearchChange(""); + }} + /> + ), + })} /> </Card> ); diff --git a/apps/insights/src/components/Publisher/layout.module.scss b/apps/insights/src/components/Publisher/layout.module.scss index 81697a4ec2..be96824372 100644 --- a/apps/insights/src/components/Publisher/layout.module.scss +++ b/apps/insights/src/components/Publisher/layout.module.scss @@ -84,8 +84,7 @@ .body { @include theme.max-width; - - padding-top: theme.spacing(6); + margin-top: theme.spacing(6); } } diff --git a/apps/insights/src/components/Publisher/layout.tsx b/apps/insights/src/components/Publisher/layout.tsx index fc7cfc7bae..55df87dd79 100644 --- a/apps/insights/src/components/Publisher/layout.tsx +++ b/apps/insights/src/components/Publisher/layout.tsx @@ -40,6 +40,7 @@ import { PublisherKey } from "../PublisherKey"; import { PublisherTag } from "../PublisherTag"; import { ScoreHistory } from "../ScoreHistory"; import { SemicircleMeter } from "../SemicircleMeter"; +import { Stats } from "../Stats"; import { TabPanel, TabRoot, Tabs } from "../Tabs"; import { TokenIcon } from "../TokenIcon"; @@ -110,264 +111,266 @@ export const PublishersLayout = async ({ children, params }: Props) => { /> </div> <section className={styles.stats}> - <ChartCard - variant="primary" - header="Publisher Ranking" - lineClassName={styles.primarySparkChartLine} - corner={ - <AlertTrigger> - <Button - variant="ghost" - size="xs" - beforeIcon={(props) => <Info weight="fill" {...props} />} - rounded - hideText - className={styles.publisherRankingExplainButton ?? ""} - > - Explain Publisher Ranking - </Button> - <Alert title="Publisher Ranking" icon={<Lightbulb />}> - <p className={styles.publisherRankingExplainDescription}> - Each <b>Publisher</b> receives a <b>Ranking</b> which is - derived from the number of price feeds the{" "} - <b>Publisher</b> is actively publishing. - </p> - </Alert> - </AlertTrigger> - } - data={rankingHistory.map(({ timestamp, rank }) => ({ - x: timestamp, - y: rank, - displayX: ( - <span className={styles.activeDate}> - <FormattedDate value={timestamp} /> - </span> - ), - }))} - stat={currentRanking.rank} - {...(previousRanking && { - miniStat: ( - <ChangeValue - direction={getChangeDirection( - currentRanking.rank, - previousRanking.rank, - )} - > - {Math.abs(currentRanking.rank - previousRanking.rank)} - </ChangeValue> - ), - })} - /> - <DrawerTrigger> + <Stats> <ChartCard - header="Median Score" - chartClassName={styles.medianScoreChart} - lineClassName={styles.secondarySparkChartLine} - corner={<Info weight="fill" />} - data={medianScoreHistory.map(({ time, score }) => ({ - x: time, - y: score, + variant="primary" + header="Publisher Ranking" + lineClassName={styles.primarySparkChartLine} + corner={ + <AlertTrigger> + <Button + variant="ghost" + size="xs" + beforeIcon={(props) => <Info weight="fill" {...props} />} + rounded + hideText + className={styles.publisherRankingExplainButton ?? ""} + > + Explain Publisher Ranking + </Button> + <Alert title="Publisher Ranking" icon={<Lightbulb />}> + <p className={styles.publisherRankingExplainDescription}> + Each <b>Publisher</b> receives a <b>Ranking</b> which is + derived from the number of price feeds the{" "} + <b>Publisher</b> is actively publishing. + </p> + </Alert> + </AlertTrigger> + } + data={rankingHistory.map(({ timestamp, rank }) => ({ + x: timestamp, + y: rank, displayX: ( <span className={styles.activeDate}> - <FormattedDate value={time} /> + <FormattedDate value={timestamp} /> </span> ), - displayY: ( - <FormattedNumber - maximumSignificantDigits={5} - value={score} - /> - ), }))} - stat={ - <FormattedNumber - maximumSignificantDigits={5} - value={currentMedianScore.score} - /> - } - {...(previousMedianScore && { + stat={currentRanking.rank} + {...(previousRanking && { miniStat: ( <ChangeValue direction={getChangeDirection( - previousMedianScore.score, - currentMedianScore.score, + currentRanking.rank, + previousRanking.rank, )} > - <FormattedNumber - maximumSignificantDigits={2} - value={ - (100 * - Math.abs( - currentMedianScore.score - - previousMedianScore.score, - )) / - previousMedianScore.score - } - /> - % + {Math.abs(currentRanking.rank - previousRanking.rank)} </ChangeValue> ), })} /> - <Drawer - title="Median Score" - className={styles.medianScoreDrawer ?? ""} - bodyClassName={styles.medianScoreDrawerBody} - footerClassName={styles.medianScoreDrawerFooter} - footer={ - <Button - variant="outline" - size="sm" - href="https://docs.pyth.network/home/oracle-integrity-staking/publisher-quality-ranking" - target="_blank" - beforeIcon={BookOpenText} - > - Documentation - </Button> - } - > - <ScoreHistory isMedian scoreHistory={medianScoreHistory} /> - <InfoBox icon={<Ranking />} header="Publisher Score"> - Each price feed a publisher provides has an associated score, - which is determined by the component{"'"}s uptime, price - deviation, and staleness. This panel shows the median for each - score across all price feeds published by this publisher, as - well as the overall median score across all those feeds. - </InfoBox> - </Drawer> - </DrawerTrigger> - <ActiveFeedsCard - publisherKey={key} - activeFeeds={ - priceFeeds.filter((feed) => feed.status === Status.Active) - .length - } - totalFeeds={totalFeedsCount} - /> - <DrawerTrigger> - <StatCard - header="OIS Pool Allocation" - stat={ - <span - className={styles.oisAllocation} - data-is-overallocated={ - Number(oisStats.poolUtilization) > oisStats.maxPoolSize - ? "" - : undefined - } - > + <DrawerTrigger> + <ChartCard + header="Median Score" + chartClassName={styles.medianScoreChart} + lineClassName={styles.secondarySparkChartLine} + corner={<Info weight="fill" />} + data={medianScoreHistory.map(({ time, score }) => ({ + x: time, + y: score, + displayX: ( + <span className={styles.activeDate}> + <FormattedDate value={time} /> + </span> + ), + displayY: ( + <FormattedNumber + maximumSignificantDigits={5} + value={score} + /> + ), + }))} + stat={ <FormattedNumber - maximumFractionDigits={2} - value={ - (100 * Number(oisStats.poolUtilization)) / - oisStats.maxPoolSize - } + maximumSignificantDigits={5} + value={currentMedianScore.score} /> - % - </span> - } - corner={<Info weight="fill" />} - > - <Meter - value={Number(oisStats.poolUtilization)} - maxValue={oisStats.maxPoolSize} - label="OIS Pool" - startLabel={ - <span className={styles.tokens}> - <TokenIcon /> - <span> - <FormattedTokens tokens={oisStats.poolUtilization} /> - </span> - </span> } - endLabel={ - <span className={styles.tokens}> - <TokenIcon /> - <span> - <FormattedTokens - tokens={BigInt(oisStats.maxPoolSize)} + {...(previousMedianScore && { + miniStat: ( + <ChangeValue + direction={getChangeDirection( + previousMedianScore.score, + currentMedianScore.score, + )} + > + <FormattedNumber + maximumSignificantDigits={2} + value={ + (100 * + Math.abs( + currentMedianScore.score - + previousMedianScore.score, + )) / + previousMedianScore.score + } /> - </span> - </span> - } + % + </ChangeValue> + ), + })} /> - </StatCard> - <Drawer - title="OIS Pool Allocation" - className={styles.oisDrawer ?? ""} - bodyClassName={styles.oisDrawerBody} - footerClassName={styles.oisDrawerFooter} - footer={ - <> - <Button - variant="solid" - size="sm" - href="https://staking.pyth.network" - target="_blank" - beforeIcon={Browsers} - > - Open Staking App - </Button> + <Drawer + title="Median Score" + className={styles.medianScoreDrawer ?? ""} + bodyClassName={styles.medianScoreDrawerBody} + footerClassName={styles.medianScoreDrawerFooter} + footer={ <Button variant="outline" size="sm" - href="https://docs.pyth.network/home/oracle-integrity-staking" + href="https://docs.pyth.network/home/oracle-integrity-staking/publisher-quality-ranking" target="_blank" beforeIcon={BookOpenText} > Documentation </Button> - </> - } - > - <SemicircleMeter - width={420} - height={420} - value={Number(oisStats.poolUtilization)} - maxValue={oisStats.maxPoolSize} - className={styles.oisMeter ?? ""} - aria-label="OIS Pool Utilization" + } > - <TokenIcon className={styles.oisMeterIcon} /> - <div className={styles.oisMeterLabel}>OIS Pool</div> - </SemicircleMeter> + <ScoreHistory isMedian scoreHistory={medianScoreHistory} /> + <InfoBox icon={<Ranking />} header="Publisher Score"> + Each price feed a publisher provides has an associated score, + which is determined by the component{"'"}s uptime, price + deviation, and staleness. This panel shows the median for each + score across all price feeds published by this publisher, as + well as the overall median score across all those feeds. + </InfoBox> + </Drawer> + </DrawerTrigger> + <ActiveFeedsCard + publisherKey={key} + activeFeeds={ + priceFeeds.filter((feed) => feed.status === Status.Active) + .length + } + totalFeeds={totalFeedsCount} + /> + <DrawerTrigger> <StatCard - header="Total Staked" - variant="secondary" - nonInteractive + header="OIS Pool Allocation" stat={ - <> - <TokenIcon /> - <FormattedTokens tokens={oisStats.poolUtilization} /> - </> + <span + className={styles.oisAllocation} + data-is-overallocated={ + Number(oisStats.poolUtilization) > oisStats.maxPoolSize + ? "" + : undefined + } + > + <FormattedNumber + maximumFractionDigits={2} + value={ + (100 * Number(oisStats.poolUtilization)) / + oisStats.maxPoolSize + } + /> + % + </span> } - /> - <StatCard - header="Pool Capacity" - variant="secondary" - nonInteractive - stat={ + corner={<Info weight="fill" />} + > + <Meter + value={Number(oisStats.poolUtilization)} + maxValue={oisStats.maxPoolSize} + label="OIS Pool" + startLabel={ + <span className={styles.tokens}> + <TokenIcon /> + <span> + <FormattedTokens tokens={oisStats.poolUtilization} /> + </span> + </span> + } + endLabel={ + <span className={styles.tokens}> + <TokenIcon /> + <span> + <FormattedTokens + tokens={BigInt(oisStats.maxPoolSize)} + /> + </span> + </span> + } + /> + </StatCard> + <Drawer + title="OIS Pool Allocation" + className={styles.oisDrawer ?? ""} + bodyClassName={styles.oisDrawerBody} + footerClassName={styles.oisDrawerFooter} + footer={ <> - <TokenIcon /> - <FormattedTokens tokens={BigInt(oisStats.maxPoolSize)} /> + <Button + variant="solid" + size="sm" + href="https://staking.pyth.network" + target="_blank" + beforeIcon={Browsers} + > + Open Staking App + </Button> + <Button + variant="outline" + size="sm" + href="https://docs.pyth.network/home/oracle-integrity-staking" + target="_blank" + beforeIcon={BookOpenText} + > + Documentation + </Button> </> } - /> - <OisApyHistory apyHistory={oisStats.apyHistory ?? []} /> - <InfoBox - icon={<ShieldChevron />} - header="Oracle Integrity Staking (OIS)" > - OIS allows anyone to help secure Pyth and protect DeFi. - Through decentralized staking rewards and slashing, OIS - incentivizes Pyth publishers to maintain high-quality data - contributions. PYTH holders can stake to publishers to further - reinforce oracle security. Rewards are programmatically - distributed to high quality publishers and the stakers - supporting them to strengthen oracle integrity. - </InfoBox> - </Drawer> - </DrawerTrigger> + <SemicircleMeter + width={420} + height={420} + value={Number(oisStats.poolUtilization)} + maxValue={oisStats.maxPoolSize} + className={styles.oisMeter ?? ""} + aria-label="OIS Pool Utilization" + > + <TokenIcon className={styles.oisMeterIcon} /> + <div className={styles.oisMeterLabel}>OIS Pool</div> + </SemicircleMeter> + <StatCard + header="Total Staked" + variant="secondary" + nonInteractive + stat={ + <> + <TokenIcon /> + <FormattedTokens tokens={oisStats.poolUtilization} /> + </> + } + /> + <StatCard + header="Pool Capacity" + variant="secondary" + nonInteractive + stat={ + <> + <TokenIcon /> + <FormattedTokens tokens={BigInt(oisStats.maxPoolSize)} /> + </> + } + /> + <OisApyHistory apyHistory={oisStats.apyHistory ?? []} /> + <InfoBox + icon={<ShieldChevron />} + header="Oracle Integrity Staking (OIS)" + > + OIS allows anyone to help secure Pyth and protect DeFi. + Through decentralized staking rewards and slashing, OIS + incentivizes Pyth publishers to maintain high-quality data + contributions. PYTH holders can stake to publishers to further + reinforce oracle security. Rewards are programmatically + distributed to high quality publishers and the stakers + supporting them to strengthen oracle integrity. + </InfoBox> + </Drawer> + </DrawerTrigger> + </Stats> </section> </section> <TabRoot> diff --git a/apps/insights/src/components/Publisher/performance.module.scss b/apps/insights/src/components/Publisher/performance.module.scss index e5e5fd6bad..32ef3b26f9 100644 --- a/apps/insights/src/components/Publisher/performance.module.scss +++ b/apps/insights/src/components/Publisher/performance.module.scss @@ -2,11 +2,14 @@ .performance { display: grid; - grid-template-columns: 1fr 1fr; - gap: theme.spacing(12) theme.spacing(6); + grid-template-columns: 1fr; + gap: theme.spacing(6); align-items: flex-start; - > *:first-child { - grid-column: span 2 / span 2; + @include theme.breakpoint("md") { + grid-template-columns: 1fr 1fr; + > *:first-child { + grid-column: span 2 / span 2; + } } } diff --git a/apps/insights/src/components/Publisher/performance.tsx b/apps/insights/src/components/Publisher/performance.tsx index 37a629f23c..1d70171bf8 100644 --- a/apps/insights/src/components/Publisher/performance.tsx +++ b/apps/insights/src/components/Publisher/performance.tsx @@ -14,6 +14,7 @@ import { TopFeedsTable } from "./top-feeds-table"; import { getPublishers } from "../../services/clickhouse"; import { Cluster, getTotalFeedCount } from "../../services/pyth"; import { Status } from "../../status"; +import { CardTitle } from "../CardTitle"; import { NoResults } from "../NoResults"; import { PriceFeedIcon } from "../PriceFeedIcon"; import { PriceFeedTag } from "../PriceFeedTag"; @@ -47,7 +48,7 @@ export const Performance = async ({ params }: Props) => { notFound() ) : ( <div className={styles.performance}> - <Card icon={<Broadcast />} title="Publishers Ranking"> + <Card icon={<Broadcast />} title={<CardTitle>Publisher Ranking</CardTitle>}> <Table rounded fill @@ -118,7 +119,7 @@ export const Performance = async ({ params }: Props) => { })} /> </Card> - <Card icon={<Network />} title="High-Performing Feeds"> + <Card title={<CardTitle icon={<Network />}>High-performing Feeds</CardTitle>}> <TopFeedsTable label="High-Performing Feeds" publisherScoreWidth={PUBLISHER_SCORE_WIDTH} @@ -138,7 +139,7 @@ export const Performance = async ({ params }: Props) => { )} /> </Card> - <Card icon={<Network />} title="Low-Performing Feeds"> + <Card title={<CardTitle icon={<Network />}>Low-performing Feeds</CardTitle>}> <TopFeedsTable label="Low-Performing Feeds" publisherScoreWidth={PUBLISHER_SCORE_WIDTH} diff --git a/apps/insights/src/components/Publishers/index.module.scss b/apps/insights/src/components/Publishers/index.module.scss index a7568592a6..8e5871e68b 100644 --- a/apps/insights/src/components/Publishers/index.module.scss +++ b/apps/insights/src/components/Publishers/index.module.scss @@ -1,74 +1,74 @@ @use "@pythnetwork/component-library/theme"; @use "../Root/index.module.scss" as root; -.publishers { - @include theme.max-width; +.body { + display: flex; + flex-flow: column nowrap; + width: 100%; - .header { - @include theme.h3; - - color: theme.color("heading"); - font-weight: theme.font-weight("semibold"); + @include theme.breakpoint("lg") { + flex-flow: row nowrap; + align-items: flex-start; + gap: theme.spacing(6); } - .body { + .stats { display: flex; - flex-flow: row nowrap; - gap: theme.spacing(12); - align-items: flex-start; - margin-top: theme.spacing(6); + flex-flow: column; + gap: theme.spacing(6); + align-items: stretch; + flex-grow: 1; - .stats { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: theme.spacing(4); - align-items: center; - width: 40%; + @include theme.breakpoint("lg") { position: sticky; top: root.$header-height; + } - .averageMedianScoreExplainButton { - margin-top: -#{theme.button-padding("xs", false)}; - margin-right: -#{theme.button-padding("xs", false)}; - } - - .oisCard { - grid-column: span 2 / span 2; + .averageMedianScoreExplainButton { + margin-top: -#{theme.button-padding("xs", false)}; + margin-right: -#{theme.button-padding("xs", false)}; + } - .oisPool { - .title { - font-size: theme.font-size("sm"); - font-weight: theme.font-weight("normal"); - color: theme.color("heading"); - margin: 0; - } + .oisCard { + .oisPool { + .title { + font-size: theme.font-size("sm"); + font-weight: theme.font-weight("normal"); + color: theme.color("heading"); + margin: 0; + } - .poolUsed { - margin: 0; - color: theme.color("heading"); + .poolUsed { + margin: 0; + color: theme.color("heading"); - @include theme.h3; - } + @include theme.h3; + } - .poolTotal { - margin: 0; - color: theme.color("muted"); - font-size: theme.font-size("sm"); - font-weight: theme.font-weight("normal"); - } + .poolTotal { + margin: 0; + color: theme.color("muted"); + font-size: theme.font-size("sm"); + font-weight: theme.font-weight("normal"); } + } + + .oisStats { + display: flex; + flex-flow: column; + gap: theme.spacing(1); - .oisStats { + @include theme.breakpoint("lg") { + column-span: 2; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: theme.spacing(1); } } } + } - .publishersCard { - width: 60%; - } + .publishersCard { + width: 60%; } } diff --git a/apps/insights/src/components/Publishers/index.tsx b/apps/insights/src/components/Publishers/index.tsx index 1d53cc051c..c84b8655c6 100644 --- a/apps/insights/src/components/Publishers/index.tsx +++ b/apps/insights/src/components/Publishers/index.tsx @@ -17,10 +17,13 @@ import { getClaimableRewards, getDistributedRewards, } from "../../services/staking"; +import { CardTitle } from "../CardTitle"; import { FormattedTokens } from "../FormattedTokens"; +import { PageLayout } from "../PageLayout/page-layout"; import { PublisherIcon } from "../PublisherIcon"; import { PublisherTag } from "../PublisherTag"; import { SemicircleMeter, Label } from "../SemicircleMeter"; +import { Stats } from "../Stats"; import { TokenIcon } from "../TokenIcon"; const INITIAL_REWARD_POOL_SIZE = 60_000_000_000_000n; @@ -33,64 +36,65 @@ export const Publishers = async () => { ]); return ( - <div className={styles.publishers}> - <h1 className={styles.header}>Publishers</h1> + <PageLayout title={"Publishers"}> <div className={styles.body}> <section className={styles.stats}> - <StatCard - variant="primary" - header="Active Publishers" - stat={publishers.length} - /> - <StatCard - header="Avg. Median Score" - corner={ - <AlertTrigger> - <Button - variant="ghost" - size="xs" - beforeIcon={(props) => <Info weight="fill" {...props} />} - rounded - hideText - className={styles.averageMedianScoreExplainButton ?? ""} - > - Explain Average Median Score - </Button> - <Alert title="Average Median Score" icon={<Lightbulb />}> - <p className={styles.averageMedianScoreDescription}> - Each <b>Price Feed Component</b> that a <b>Publisher</b>{" "} - provides has an associated <b>Score</b>, which is determined - by that component{"'"}s <b>Uptime</b>,{" "} - <b>Price Deviation</b>, and <b>Staleness</b>. The publisher - {"'"}s <b>Median Score</b> measures the 50th percentile of - the <b>Score</b> across all of that publisher{"'"}s{" "} - <b>Price Feed Components</b>. The{" "} - <b>Average Median Score</b> is the average of the{" "} - <b>Median Scores</b> of all publishers who contribute to the - Pyth Network. - </p> + <Stats> + <StatCard + variant="primary" + header="Active Publishers" + stat={publishers.length} + /> + <StatCard + header="Avg. Median Score" + corner={ + <AlertTrigger> <Button + variant="ghost" size="xs" - variant="solid" - href="https://docs.pyth.network/home/oracle-integrity-staking/publisher-quality-ranking" - target="_blank" + beforeIcon={(props) => <Info weight="fill" {...props} />} + rounded + hideText + className={styles.averageMedianScoreExplainButton ?? ""} > - Learn more + Explain Average Median Score </Button> - </Alert> - </AlertTrigger> - } - stat={( - publishers.reduce( - (sum, publisher) => sum + publisher.medianScore, - 0, - ) / publishers.length - ).toFixed(2)} - /> + <Alert title="Average Median Score" icon={<Lightbulb />}> + <p className={styles.averageMedianScoreDescription}> + Each <b>Price Feed Component</b> that a <b>Publisher</b>{" "} + provides has an associated <b>Score</b>, which is determined + by that component{"'"}s <b>Uptime</b>,{" "} + <b>Price Deviation</b>, and <b>Staleness</b>. The publisher + {"'"}s <b>Median Score</b> measures the 50th percentile of + the <b>Score</b> across all of that publisher{"'"}s{" "} + <b>Price Feed Components</b>. The{" "} + <b>Average Median Score</b> is the average of the{" "} + <b>Median Scores</b> of all publishers who contribute to the + Pyth Network. + </p> + <Button + size="xs" + variant="solid" + href="https://docs.pyth.network/home/oracle-integrity-staking/publisher-quality-ranking" + target="_blank" + > + Learn more + </Button> + </Alert> + </AlertTrigger> + } + stat={( + publishers.reduce( + (sum, publisher) => sum + publisher.medianScore, + 0, + ) / publishers.length + ).toFixed(2)} + /> + </Stats> <Card - title="Oracle Integrity Staking (OIS)" + title={<CardTitle>OIS</CardTitle>} className={styles.oisCard} - toolbar={ + action={ <Button href="https://staking.pyth.network" target="_blank" @@ -169,7 +173,7 @@ export const Publishers = async () => { )} /> </div> - </div> + </PageLayout > ); }; diff --git a/apps/insights/src/components/Publishers/publishers-card.tsx b/apps/insights/src/components/Publishers/publishers-card.tsx index 0d421575d3..b0fa44b758 100644 --- a/apps/insights/src/components/Publishers/publishers-card.tsx +++ b/apps/insights/src/components/Publishers/publishers-card.tsx @@ -14,6 +14,7 @@ import { type ReactNode, Suspense, useMemo } from "react"; import { useFilter, useCollator } from "react-aria"; import { useQueryParamFilterPagination } from "../../use-query-param-filter-pagination"; +import { CardTitle } from "../CardTitle"; import { NoResults } from "../NoResults"; import { PublisherTag } from "../PublisherTag"; import { Ranking } from "../Ranking"; @@ -35,9 +36,9 @@ type Publisher = { inactiveFeeds: number; medianScore: number; } & ( - | { name: string; icon: ReactNode } - | { name?: undefined; icon?: undefined } -); + | { name: string; icon: ReactNode } + | { name?: undefined; icon?: undefined } + ); export const PublishersCard = ({ publishers, ...props }: Props) => ( <Suspense fallback={<PublishersCardContents isLoading {...props} />}> @@ -155,22 +156,22 @@ type PublishersCardContentsProps = Pick< ( | { isLoading: true } | { - isLoading?: false; - numResults: number; - search: string; - sortDescriptor: SortDescriptor; - numPages: number; - page: number; - pageSize: number; - onSearchChange: (newSearch: string) => void; - onSortChange: (newSort: SortDescriptor) => void; - onPageSizeChange: (newPageSize: number) => void; - onPageChange: (newPage: number) => void; - mkPageLink: (page: number) => string; - rows: RowConfig< - "ranking" | "name" | "activeFeeds" | "inactiveFeeds" | "medianScore" - >[]; - } + isLoading?: false; + numResults: number; + search: string; + sortDescriptor: SortDescriptor; + numPages: number; + page: number; + pageSize: number; + onSearchChange: (newSearch: string) => void; + onSortChange: (newSort: SortDescriptor) => void; + onPageSizeChange: (newPageSize: number) => void; + onPageChange: (newPage: number) => void; + mkPageLink: (page: number) => string; + rows: RowConfig< + "ranking" | "name" | "activeFeeds" | "inactiveFeeds" | "medianScore" + >[]; + } ); const PublishersCardContents = ({ @@ -180,16 +181,12 @@ const PublishersCardContents = ({ }: PublishersCardContentsProps) => ( <Card className={className} - icon={<Broadcast />} title={ - <> - <span>Publishers</span> - {!props.isLoading && ( - <Badge style="filled" variant="neutral" size="md"> - {props.numResults} - </Badge> - )} - </> + <CardTitle badge={!props.isLoading && ( + <Badge style="filled" variant="neutral" size="md"> + {props.numResults} + </Badge> + )} icon={<Broadcast />}>Publishers</CardTitle> } toolbar={ <SearchInput @@ -199,9 +196,9 @@ const PublishersCardContents = ({ {...(props.isLoading ? { isPending: true, isDisabled: true } : { - value: props.search, - onChange: props.onSearchChange, - })} + value: props.search, + onChange: props.onSearchChange, + })} /> } {...(!props.isLoading && { @@ -264,21 +261,21 @@ const PublishersCardContents = ({ ]} {...(props.isLoading ? { - isLoading: true, - } + isLoading: true, + } : { - rows: props.rows, - sortDescriptor: props.sortDescriptor, - onSortChange: props.onSortChange, - emptyState: ( - <NoResults - query={props.search} - onClearSearch={() => { - props.onSearchChange(""); - }} - /> - ), - })} + rows: props.rows, + sortDescriptor: props.sortDescriptor, + onSortChange: props.onSortChange, + emptyState: ( + <NoResults + query={props.search} + onClearSearch={() => { + props.onSearchChange(""); + }} + /> + ), + })} /> </Card> ); diff --git a/apps/insights/src/components/Root/footer.module.scss b/apps/insights/src/components/Root/footer.module.scss index f6799a3bf8..d28c839e48 100644 --- a/apps/insights/src/components/Root/footer.module.scss +++ b/apps/insights/src/components/Root/footer.module.scss @@ -2,41 +2,32 @@ .footer { // SM - background: theme.color("background", "primary"); - - // XL + margin-top: theme.spacing(8); padding: theme.spacing(8) 0; - - // bg-beige-100 sm:border-t sm:border-stone-300 + background: theme.color("background", "primary"); .topContent { display: flex; gap: theme.spacing(6); - - // SM - flex-flow: row nowrap; - align-items: center; + flex-flow: column nowrap; + align-items: flex-start; justify-content: space-between; @include theme.max-width; - // XL - margin-bottom: theme.spacing(12); - - // py-6 + margin-bottom: theme.spacing(6); - // flex-col + @include theme.breakpoint("sm") { + flex-flow: row nowrap; + align-items: center; + } .left { display: flex; align-items: stretch; justify-content: space-between; - - // SM gap: theme.spacing(6); - // gap-8 - .logoLink { height: theme.spacing(5); box-sizing: content-box; @@ -86,10 +77,12 @@ .bottomContent { display: flex; gap: theme.spacing(6); + flex-flow: column; - // SM - flex-flow: row nowrap; - justify-content: space-between; + @include theme.breakpoint("sm") { + flex-flow: row nowrap; + justify-content: space-between; + } // "flex-col diff --git a/apps/insights/src/components/Root/footer.tsx b/apps/insights/src/components/Root/footer.tsx index 343d49e678..04fa9b773b 100644 --- a/apps/insights/src/components/Root/footer.tsx +++ b/apps/insights/src/components/Root/footer.tsx @@ -1,3 +1,4 @@ +import { ArrowSquareOut } from "@phosphor-icons/react/dist/ssr/ArrowSquareOut"; import { type Props as ButtonProps, Button, @@ -21,10 +22,10 @@ export const Footer = () => ( <div className={styles.divider} /> <div className={styles.help}> <SupportDrawer> - <Link>Help</Link> + <Link>Support</Link> </SupportDrawer> <Link href="https://docs.pyth.network" target="_blank"> - Documentation + Documentation <ArrowSquareOut /> </Link> </div> </div> diff --git a/apps/insights/src/components/Root/header.module.scss b/apps/insights/src/components/Root/header.module.scss index 65a3ff8506..fea43a9ca9 100644 --- a/apps/insights/src/components/Root/header.module.scss +++ b/apps/insights/src/components/Root/header.module.scss @@ -4,8 +4,9 @@ position: sticky; top: 0; width: 100%; - background-color: theme.color("background", "nav-blur"); - backdrop-filter: blur(32px); + background-color: theme.color("background", "primary"); + // TODO: This causes that navigation is not fixed + // backdrop-filter: blur(32px); .content { height: 100%; @@ -16,18 +17,24 @@ .leftMenu { flex: none; - gap: theme.spacing(6); + gap: theme.spacing(4); + position: relative; @include theme.row; .logoLink { padding: theme.spacing(3); - margin: -#{theme.spacing(3)}; color: theme.color("foreground"); + position: relative; + + @include theme.breakpoint("3xl") { + position: absolute; + left: -#{theme.spacing(16)}; + } .logoWrapper { - width: theme.spacing(9); - height: theme.spacing(9); + width: theme.spacing(8); + height: theme.spacing(8); position: relative; .logo { @@ -52,33 +59,34 @@ .rightMenu { flex: none; + position: relative; gap: theme.spacing(2); @include theme.row; - margin-right: -#{theme.button-padding("sm", false)}; - .themeSwitch { - margin-left: theme.spacing(1); - } - } - - @media screen and (min-width: theme.$max-width + (2 * (theme.spacing(9) + theme.spacing(8) + theme.spacing(7)))) { - .leftMenu { - margin-left: -#{theme.spacing(9) + theme.spacing(7)}; + display: none; - .logoLink { - margin-right: -#{theme.spacing(2)}; + @include theme.breakpoint("md") { + position: relative; + display: block; } - } - - .rightMenu { - margin-right: -#{theme.spacing(9) + theme.spacing(7)}; - .themeSwitch { - margin-left: theme.spacing(5); + @include theme.breakpoint("3xl") { + display: block; + position: absolute; + right: -#{theme.spacing(16)}; } } } } + + .desktopNavigation, + .desktopSupport, + .desktopDocs { + display: none; + @include theme.breakpoint("md") { + display: block; + } + } } diff --git a/apps/insights/src/components/Root/header.tsx b/apps/insights/src/components/Root/header.tsx index d83d733ccb..d1f54e6294 100644 --- a/apps/insights/src/components/Root/header.tsx +++ b/apps/insights/src/components/Root/header.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Lifebuoy } from "@phosphor-icons/react/dist/ssr/Lifebuoy"; import { Button } from "@pythnetwork/component-library/Button"; import { Link } from "@pythnetwork/component-library/Link"; @@ -10,6 +12,7 @@ import { SearchButton } from "./search-button"; import { SupportDrawer } from "./support-drawer"; import { MainNavTabs } from "./tabs"; import { ThemeSwitch } from "./theme-switch"; +import { MobileMenu } from "../MobileMenu/mobile-menu"; export const Header = ({ className, ...props }: ComponentProps<"header">) => ( <header className={clsx(styles.header, className)} {...props}> @@ -22,24 +25,31 @@ export const Header = ({ className, ...props }: ComponentProps<"header">) => ( <div className={styles.logoLabel}>Pyth Homepage</div> </Link> <div className={styles.appName}>Insights</div> - <MainNavTabs /> + <div className={styles.desktopNavigation}> + <MainNavTabs /> + </div> </div> <div className={styles.rightMenu}> - <SupportDrawer> - <Button beforeIcon={Lifebuoy} variant="ghost" size="sm" rounded> - Support - </Button> - </SupportDrawer> + <div className={styles.desktopSupport}> + <SupportDrawer> + <Button beforeIcon={Lifebuoy} variant="ghost" size="sm" rounded> + Support + </Button> + </SupportDrawer> + </div> <SearchButton /> - <Button - href="https://docs.pyth.network" - size="sm" - rounded - target="_blank" - > - Dev Docs - </Button> + <div className={styles.desktopDocs}> + <Button + href="https://docs.pyth.network" + size="sm" + rounded + target="_blank" + > + Dev Docs + </Button> + </div> <ThemeSwitch className={styles.themeSwitch ?? ""} /> + <MobileMenu /> </div> </div> </header> diff --git a/apps/insights/src/components/Root/index.tsx b/apps/insights/src/components/Root/index.tsx index e3fb96107d..b2fd1e2a88 100644 --- a/apps/insights/src/components/Root/index.tsx +++ b/apps/insights/src/components/Root/index.tsx @@ -18,6 +18,7 @@ import { toHex } from "../../hex"; import { getPublishers } from "../../services/clickhouse"; import { Cluster, getData } from "../../services/pyth"; import { LivePricesProvider } from "../LivePrices"; +import { MobileNavigation } from "../MobileNavigation/mobile-navigation"; import { PriceFeedIcon } from "../PriceFeedIcon"; import { PublisherIcon } from "../PublisherIcon"; @@ -65,6 +66,7 @@ export const Root = async ({ children }: Props) => { <TabPanel>{children}</TabPanel> </main> <Footer /> + <MobileNavigation /> </TabRoot> </SearchDialogProvider> </BaseRoot> diff --git a/apps/insights/src/components/Root/search-button.tsx b/apps/insights/src/components/Root/search-button.tsx index d099ba7431..0f421acb4f 100644 --- a/apps/insights/src/components/Root/search-button.tsx +++ b/apps/insights/src/components/Root/search-button.tsx @@ -31,7 +31,7 @@ const SearchText = () => { const SearchTextImpl = () => { // This component can only ever render in the client so we can safely ignore // this eslint rule. - // eslint-disable-next-line n/no-unsupported-features/node-builtins + const isMac = useMemo(() => navigator.userAgent.includes("Mac"), []); return isMac ? "⌘ K" : "Ctrl K"; }; diff --git a/apps/insights/src/components/Root/support-drawer.module.scss b/apps/insights/src/components/Root/support-drawer.module.scss index f6152c85d3..9ef4761f9a 100644 --- a/apps/insights/src/components/Root/support-drawer.module.scss +++ b/apps/insights/src/components/Root/support-drawer.module.scss @@ -34,12 +34,12 @@ 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; + gap: theme.spacing(1) theme.spacing(4); + align-items: start; width: 100%; .icon { - font-size: theme.spacing(8); + font-size: theme.spacing(6); color: theme.color("states", "data", "normal"); grid-row: span 2 / span 2; display: grid; @@ -47,17 +47,22 @@ } .header { - @include theme.text("sm", "medium"); + @include theme.h6; + line-height: 1.5; color: theme.color("heading"); } .description { - @include theme.text("xs", "normal"); + @include theme.text("sm", "normal"); + line-height: 1.5; color: theme.color("muted"); grid-column: 2; grid-row: 2; + word-wrap: break-word; + overflow: hidden; + max-width: 100%; } .caret { diff --git a/apps/insights/src/components/Stats/index.module.scss b/apps/insights/src/components/Stats/index.module.scss new file mode 100644 index 0000000000..8cecc3044f --- /dev/null +++ b/apps/insights/src/components/Stats/index.module.scss @@ -0,0 +1,60 @@ +@use "@pythnetwork/component-library/theme"; + +.statsContainer { + .statScrollWrapper { + width: 100dvw; // Extend the width beyond the root padding + margin-left: -#{theme.spacing(4)}; + margin-right: -#{theme.spacing(4)}; + white-space: nowrap; + overflow: auto hidden; + -webkit-overflow-scrolling: touch; + scroll-snap-type: x mandatory; + scroll-padding: theme.spacing(4); + left: 0; + right: 0; + + // Optional: Hide scrollbars + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + @include theme.breakpoint("md") { + width: 100%; + position: relative; + display: flex; + flex-flow: row nowrap; + overflow: visible; + margin: 0; + } + + .statsItemsContainer { + display: flex; + flex-flow: row nowrap; + width: max-content; + gap: theme.spacing(3); + padding: 0 theme.spacing(4); + + > * { + width: 280px; + scroll-snap-align: start; + } + + @include theme.breakpoint("md") { + gap: theme.spacing(6); + width: 100%; + padding: 0; + + > * { + display: flex; + width: 100%; + flex: 1 0 0; + min-width: 0; + max-width: 100%; + min-height: 0; + } + } + } + } +} diff --git a/apps/insights/src/components/Stats/index.tsx b/apps/insights/src/components/Stats/index.tsx new file mode 100644 index 0000000000..1a379e913e --- /dev/null +++ b/apps/insights/src/components/Stats/index.tsx @@ -0,0 +1,18 @@ +import clsx from "clsx"; +import type { ComponentProps } from "react"; + +import styles from "./index.module.scss"; + +type StatsProps = { + children: React.ReactNode; +} & ComponentProps<"div">; + +export const Stats = ({ children, ...props }: StatsProps) => { + return ( + <div className={clsx(styles.statsContainer, props.className)} {...props}> + <div className={styles.statScrollWrapper}> + <div className={styles.statsItemsContainer}>{children}</div> + </div> + </div> + ); +}; diff --git a/apps/insights/src/components/StructuredList/index.module.scss b/apps/insights/src/components/StructuredList/index.module.scss new file mode 100644 index 0000000000..150d791f45 --- /dev/null +++ b/apps/insights/src/components/StructuredList/index.module.scss @@ -0,0 +1,27 @@ +@use "@pythnetwork/component-library/theme"; + +.structuredList { + display: flex; + flex-flow: column nowrap; + gap: theme.spacing(4); + + .structuredListItem { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: theme.spacing(2); + align-items: center; + justify-content: space-between; + + // font-size: theme.font-size("sm"); + .structuredListItemLabel { + color: theme.color("muted"); + } + + .structuredListItemValue { + text-align: right; + place-items: end; + font-weight: theme.font-weight("medium"); + color: theme.color("heading"); + } + } +} diff --git a/apps/insights/src/components/StructuredList/index.tsx b/apps/insights/src/components/StructuredList/index.tsx new file mode 100644 index 0000000000..6ac09802b2 --- /dev/null +++ b/apps/insights/src/components/StructuredList/index.tsx @@ -0,0 +1,41 @@ +import clsx from "clsx"; +import type { ComponentProps } from "react"; + +import styles from "./index.module.scss"; + +type StructuredListProps = { + items: StructureListItemProps[]; +} & ComponentProps<"div">; + +type StructureListItemProps = { + label: React.ReactNode; + value: React.ReactNode; +} & ComponentProps<"div">; + +export const StructuredList = ({ items, ...props }: StructuredListProps) => { + return ( + items.length > 0 && ( + <div className={clsx(styles.structuredList, props.className)} {...props}> + {items.map((item, index) => ( + <StructuredListItem key={index} {...item} /> + ))} + </div> + ) + ); +}; + +export const StructuredListItem = ({ + label, + value, + ...props +}: StructureListItemProps) => { + return ( + <div + className={clsx(styles.structuredListItem, props.className)} + {...props} + > + <div className={styles.structuredListItemLabel}>{label}</div> + <div className={styles.structuredListItemValue}>{value}</div> + </div> + ); +}; diff --git a/packages/component-library/src/Badge/index.module.scss b/packages/component-library/src/Badge/index.module.scss index 8ce2c49fe9..891a98dbfd 100644 --- a/packages/component-library/src/Badge/index.module.scss +++ b/packages/component-library/src/Badge/index.module.scss @@ -1,7 +1,9 @@ @use "../theme"; .badge { - display: inline flow-root; + display: inline-flex; + align-items: center; + justify-content: center; border-radius: theme.border-radius("3xl"); transition-property: color, background-color, border-color; transition-duration: 100ms; @@ -9,6 +11,7 @@ white-space: nowrap; border-width: 1px; border-style: solid; + flex-shrink: 1; &[data-size="xs"] { line-height: theme.spacing(4); diff --git a/packages/component-library/src/Button/index.module.scss b/packages/component-library/src/Button/index.module.scss index 6de1e6fc7a..57379baeaa 100644 --- a/packages/component-library/src/Button/index.module.scss +++ b/packages/component-library/src/Button/index.module.scss @@ -1,7 +1,7 @@ @use "../theme"; .button { - display: inline flow-root; + display: flex; cursor: pointer; white-space: nowrap; font-weight: theme.font-weight("medium"); @@ -12,6 +12,9 @@ text-decoration: none; outline-offset: 0; outline: theme.spacing(1) solid transparent; + text-align: center; + justify-content: center; + align-items: center; .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 2f127ce2a6..dd79ee1e76 100644 --- a/packages/component-library/src/Card/index.module.scss +++ b/packages/component-library/src/Card/index.module.scss @@ -33,35 +33,43 @@ } .header { - display: flex; - padding: theme.spacing(3) theme.spacing(4); position: relative; - - .title { - color: theme.color("heading"); - display: inline-flex; + display: flex; + padding: theme.spacing(3); + gap: theme.spacing(3); + flex-direction: column; + justify-content: flex-start; + + @include theme.breakpoint("lg") { + padding: 0 theme.spacing(2); + height: theme.spacing(12); + justify-content: space-between; flex-flow: row nowrap; - gap: theme.spacing(3); align-items: center; + } - @include theme.text("lg", "medium"); - - .icon { - font-size: theme.spacing(6); - height: theme.spacing(6); - color: theme.color("button", "primary", "background", "normal"); + .toolbar { + &:empty { + display: none; } + gap: theme.spacing(2); + display: flex; } - .toolbar { + .action { + align-self: center; + grid-area: action; + // display: grid; + // place-items: center; + // height: 100%; + // padding: 0 theme.spacing(2); position: absolute; - right: theme.spacing(3); - top: 0; - bottom: theme.spacing(0); - display: flex; - flex-flow: row nowrap; - gap: theme.spacing(2); - align-items: center; + top: theme.spacing(1.5); + right: theme.spacing(1.5); + @include theme.breakpoint("md") { + position: static; + padding: 0; + } } } diff --git a/packages/component-library/src/Card/index.tsx b/packages/component-library/src/Card/index.tsx index de33b1c4cd..2d1b646552 100644 --- a/packages/component-library/src/Card/index.tsx +++ b/packages/component-library/src/Card/index.tsx @@ -20,6 +20,7 @@ type OwnProps = { icon?: ReactNode | undefined; title?: ReactNode | undefined; toolbar?: ReactNode | ReactNode[] | undefined; + action?: ReactNode | undefined; footer?: ReactNode | undefined; nonInteractive?: boolean | undefined; }; @@ -59,6 +60,7 @@ const cardProps = <T extends ElementType>({ title, toolbar, footer, + action, ...props }: Props<T>) => ({ ...props, @@ -69,11 +71,9 @@ const cardProps = <T extends ElementType>({ <div className={styles.cardHoverBackground} /> {(Boolean(icon) || Boolean(title) || Boolean(toolbar)) && ( <div className={styles.header}> - <h2 className={styles.title}> - {icon && <div className={styles.icon}>{icon}</div>} - {title} - </h2> - <div className={styles.toolbar}>{toolbar}</div> + <div className={styles.title}>{title}</div> + {toolbar && <div className={styles.toolbar}>{toolbar}</div>} + {action && <div className={styles.action}>{action}</div>} </div> )} {children} diff --git a/packages/component-library/src/Drawer/index.module.scss b/packages/component-library/src/Drawer/index.module.scss index 3840e4b28e..039a26e6d5 100644 --- a/packages/component-library/src/Drawer/index.module.scss +++ b/packages/component-library/src/Drawer/index.module.scss @@ -7,31 +7,46 @@ z-index: 1; .drawer { - position: fixed; - top: theme.spacing(4); - bottom: theme.spacing(4); - right: theme.spacing(4); - width: 60%; - max-width: theme.spacing(160); - outline: none; + width: 100%; + border-radius: 0; + border: none; + padding-bottom: 0; background: theme.color("background", "primary"); - border: 1px solid theme.color("border"); - border-radius: theme.border-radius("3xl"); - display: flex; - flex-flow: column nowrap; - overflow-y: hidden; - padding-bottom: theme.border-radius("3xl"); + + @include theme.breakpoint("sm") { + position: fixed; + top: theme.spacing(4); + bottom: theme.spacing(4); + right: theme.spacing(4); + width: 80%; + max-width: theme.spacing(160); + outline: none; + border: 1px solid theme.color("background", "secondary"); + border-radius: theme.border-radius("3xl"); + display: flex; + flex-flow: column nowrap; + overflow-y: hidden; + } + + @include theme.breakpoint("lg") { + width: 60%; + } .heading { - padding: theme.spacing(4); - padding-left: theme.spacing(6); + padding: theme.spacing(3); + padding-left: theme.spacing(4); display: flex; flex-flow: row nowrap; justify-content: space-between; align-items: center; color: theme.color("heading"); flex: none; - border-bottom: 1px solid theme.color("border"); + border-bottom: 1px solid theme.color("background", "secondary"); + + @include theme.breakpoint("sm") { + padding: theme.spacing(4); + padding-left: theme.spacing(6); + } .title { @include theme.h4; @@ -51,8 +66,12 @@ .body { flex: 1; - overflow-y: auto; - padding: theme.spacing(6); + overflow: hidden auto; + padding: theme.spacing(4); + + @include theme.breakpoint("sm") { + padding: theme.spacing(6); + } } &[data-fill] { diff --git a/packages/component-library/src/MainNavTabs/index.module.scss b/packages/component-library/src/MainNavTabs/index.module.scss index 0d6e7573ef..c82cc45c65 100644 --- a/packages/component-library/src/MainNavTabs/index.module.scss +++ b/packages/component-library/src/MainNavTabs/index.module.scss @@ -2,12 +2,18 @@ .mainNavTabs { gap: theme.spacing(2); + z-index: 1; @include theme.row; .tab { + width: 100%; position: relative; outline: none; + z-index: 1; + @include theme.breakpoint("md") { + width: auto; + } .bubble { position: absolute; @@ -39,21 +45,11 @@ pointer-events: auto; &[data-hovered] .bubble { - background-color: theme.color( - "button", - "solid", - "background", - "hover" - ); + background-color: theme.color("button", "solid", "background", "hover"); } &[data-pressed] .bubble { - background-color: theme.color( - "button", - "solid", - "background", - "active" - ); + background-color: theme.color("button", "solid", "background", "active"); } } } diff --git a/packages/component-library/src/Paginator/index.module.scss b/packages/component-library/src/Paginator/index.module.scss index 5679663437..cd49af2658 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("lg") { + justify-content: space-between; + } .pageSizeSelect { - display: flex; + display: none; flex-flow: row nowrap; align-items: center; gap: theme.spacing(1); + @include theme.breakpoint("lg") { + display: flex; + } + .loadingIndicator { width: theme.spacing(4); height: theme.spacing(4); diff --git a/packages/component-library/src/SingleToggleGroup/index.module.scss b/packages/component-library/src/SingleToggleGroup/index.module.scss index 5bc1f0d035..64a0400d7d 100644 --- a/packages/component-library/src/SingleToggleGroup/index.module.scss +++ b/packages/component-library/src/SingleToggleGroup/index.module.scss @@ -1,7 +1,7 @@ @use "../theme"; .singleToggleGroup { - gap: theme.spacing(2); + gap: theme.spacing(1); @include theme.row; diff --git a/packages/component-library/src/Table/index.module.scss b/packages/component-library/src/Table/index.module.scss index 6e96da9c1f..0b2d17abd8 100644 --- a/packages/component-library/src/Table/index.module.scss +++ b/packages/component-library/src/Table/index.module.scss @@ -1,9 +1,14 @@ @use "../theme"; .tableContainer { + display: none; background-color: theme.color("background", "primary"); position: relative; + @include theme.breakpoint("md") { + display: block; + } + .loaderWrapper { position: absolute; top: theme.spacing(10); @@ -83,7 +88,7 @@ .cell { position: relative; - border-bottom: 1px solid theme.color("border"); + border-bottom: 1px solid theme.color("background", "secondary"); font-weight: theme.font-weight("medium"); .divider { @@ -164,21 +169,11 @@ } &[data-hovered] .cell { - background-color: theme.color( - "button", - "outline", - "background", - "hover" - ); + background-color: theme.color("button", "outline", "background", "hover"); } &[data-pressed] .cell { - background-color: theme.color( - "button", - "outline", - "background", - "active" - ); + background-color: theme.color("button", "outline", "background", "active"); } } } diff --git a/packages/component-library/src/theme.scss b/packages/component-library/src/theme.scss index 0cfb0b6f9b..1f12d81998 100644 --- a/packages/component-library/src/theme.scss +++ b/packages/component-library/src/theme.scss @@ -80,6 +80,50 @@ $border-radius: ( @return map-get-strict($border-radius, $radius); } +$breakpoints: ( + "xs": 480px, + "sm": 720px, + "md": 960px, + "lg": 1024px, + "xl": 1280px, + "2xl": 1536px, + "3xl": 1720px, +); + +@function breakpoint-old($breakpoint) { + @return map-get-strict($breakpoints, $breakpoint); +} + +@mixin breakpoint($point) { + @media (min-width: map-get-strict($breakpoints, $point)) { + @content; + } +} + +@mixin mobile() { + @media screen and (max-width: breakpoint-old("md")) { + @content; + + background: cyan; + } +} + +@mixin tablet() { + @media screen and (min-width: breakpoint-old("md")) { + @content; + + background: orange; + } +} + +@mixin desktop() { + @media screen and (min-width: breakpoint-old("3xl")) { + @content; + + background: pink; + } +} + $color-pallette: ( "black": #000, "white": #fff, @@ -433,7 +477,7 @@ $color: ( "highlight": light-dark(pallette-color("violet", 600), pallette-color("violet", 500)), "muted": - light-dark(pallette-color("stone", 700), pallette-color("steel", 300)), + light-dark(pallette-color("stone", 500), pallette-color("steel", 400)), "border": light-dark(pallette-color("stone", 300), pallette-color("steel", 600)), "selection": ( @@ -724,8 +768,12 @@ $max-width: 96rem; @mixin max-width { margin: 0 auto; max-width: $max-width; - padding: 0 spacing(6); + padding: 0 spacing(4); box-sizing: content-box; + + @include breakpoint("xl") { + padding: 0 spacing(6); + } } @mixin row { @@ -784,6 +832,24 @@ $elevations: ( margin: 0; } +@mixin h5 { + font-size: font-size("lg"); + font-style: normal; + font-weight: font-weight("medium"); + line-height: 125%; + letter-spacing: letter-spacing("tight"); + margin: 0; +} + +@mixin h6 { + font-size: font-size("base"); + font-style: normal; + font-weight: font-weight("medium"); + line-height: 125%; + letter-spacing: letter-spacing("tight"); + margin: 0; +} + @mixin text($size: "base", $weight: "normal") { font-size: font-size($size); font-weight: font-weight($weight);