diff --git a/.github/workflows/ci-turbo-build.yml b/.github/workflows/ci-turbo-build.yml index c1b17ef91d..08150686f4 100644 --- a/.github/workflows/ci-turbo-build.yml +++ b/.github/workflows/ci-turbo-build.yml @@ -15,6 +15,8 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Install libusb + run: sudo apt install -y libusb-1.0-0-dev - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - uses: actions/setup-node@v4 diff --git a/.github/workflows/ci-turbo-test.yml b/.github/workflows/ci-turbo-test.yml index 3c8b32d872..48712ec822 100644 --- a/.github/workflows/ci-turbo-test.yml +++ b/.github/workflows/ci-turbo-test.yml @@ -15,6 +15,8 @@ jobs: test: runs-on: ubuntu-latest steps: + - name: Install libusb + run: sudo apt install -y libusb-1.0-0-dev - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/publish-js.yml b/.github/workflows/publish-js.yml index c85f0adf2e..c73c8ecca5 100644 --- a/.github/workflows/publish-js.yml +++ b/.github/workflows/publish-js.yml @@ -9,6 +9,8 @@ jobs: name: Publish Javascript Packages to NPM runs-on: ubuntu-latest steps: + - name: Install libusb + run: sudo apt install -y libusb-1.0-0-dev - uses: actions/checkout@v2 - uses: actions/setup-node@v4 with: diff --git a/apps/insights/src/components/Cards/index.module.scss b/apps/insights/src/components/Cards/index.module.scss new file mode 100644 index 0000000000..fe131481e5 --- /dev/null +++ b/apps/insights/src/components/Cards/index.module.scss @@ -0,0 +1,40 @@ +@use "@pythnetwork/component-library/theme"; + +.cards { + display: flex; + flex-flow: row nowrap; + align-items: stretch; + gap: theme.spacing(6); + overflow-x: auto; + margin-left: calc(-1 * #{theme.$max-width-padding}); + margin-right: calc(-1 * #{theme.$max-width-padding}); + padding: theme.spacing(4) theme.$max-width-padding theme.spacing(4) + theme.$max-width-padding; + scroll-snap-type: x mandatory; + scroll-padding-inline: theme.$max-width-padding; + + @include theme.breakpoint("sm") { + padding-top: theme.spacing(6); + padding-bottom: theme.spacing(6); + } + + & > * { + flex: none; + width: 70vw; + max-width: theme.spacing(70); + scroll-snap-align: start; + + @include theme.breakpoint("sm") { + flex: 1 0 theme.spacing(70); + width: theme.spacing(70); + max-width: unset; + } + } + + .publishersChart, + .priceFeedsChart { + & svg { + cursor: pointer; + } + } +} diff --git a/apps/insights/src/components/Cards/index.tsx b/apps/insights/src/components/Cards/index.tsx new file mode 100644 index 0000000000..5a8d692934 --- /dev/null +++ b/apps/insights/src/components/Cards/index.tsx @@ -0,0 +1,8 @@ +import clsx from "clsx"; +import type { ComponentProps } from "react"; + +import styles from "./index.module.scss"; + +export const Cards = ({ className, ...props }: ComponentProps<"section">) => ( +
+); diff --git a/apps/insights/src/components/EntityList/index.module.scss b/apps/insights/src/components/EntityList/index.module.scss new file mode 100644 index 0000000000..a641bfef73 --- /dev/null +++ b/apps/insights/src/components/EntityList/index.module.scss @@ -0,0 +1,84 @@ +@use "@pythnetwork/component-library/theme"; + +.entityList { + background: theme.color("background", "primary"); + border-radius: theme.border-radius("xl"); + list-style-type: none; + padding: 0; + margin: 0; + + .entityItem { + padding: theme.spacing(3) theme.spacing(4); + border-bottom: 1px solid theme.color("background", "secondary"); + outline: theme.spacing(0.5) solid transparent; + outline-offset: -#{theme.spacing(0.5)}; + transition: + outline-color 100ms linear, + background-color 100ms linear; + -webkit-tap-highlight-color: transparent; + + &[data-focus-visible] { + outline: theme.spacing(0.5) solid theme.color("focus"); + } + + &[data-href] { + cursor: pointer; + } + + &[data-hovered] { + background-color: theme.color("button", "outline", "background", "hover"); + } + + &[data-pressed] { + background-color: theme.color( + "button", + "outline", + "background", + "active" + ); + } + + &:first-child { + border-top-left-radius: theme.border-radius("xl"); + border-top-right-radius: theme.border-radius("xl"); + } + + &:last-child { + border-bottom-left-radius: theme.border-radius("xl"); + border-bottom-right-radius: theme.border-radius("xl"); + border-bottom: none; + } + + .itemHeader, + .itemDetailsItem { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + } + + .itemDetails { + display: grid; + grid-template-columns: 1fr; + gap: theme.spacing(2) theme.spacing(18); + + @include theme.breakpoint("sm") { + grid-template-columns: repeat(2, 1fr); + } + + .itemDetailsItem { + height: theme.spacing(5); + + dt { + @include theme.text("sm", "normal"); + + color: theme.color("muted"); + } + + dd { + margin: 0; + } + } + } + } +} diff --git a/apps/insights/src/components/EntityList/index.tsx b/apps/insights/src/components/EntityList/index.tsx new file mode 100644 index 0000000000..eac8ebe55d --- /dev/null +++ b/apps/insights/src/components/EntityList/index.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { Skeleton } from "@pythnetwork/component-library/Skeleton"; +import { + GridList, + GridListItem, +} from "@pythnetwork/component-library/unstyled/GridList"; +import clsx from "clsx"; +import type { ComponentProps, ReactNode } from "react"; + +import styles from "./index.module.scss"; + +type Props = ComponentProps>> & { + headerLoadingSkeleton?: ReactNode | undefined; + label: string; + fields: ({ + id: T; + name: ReactNode; + } & ( + | { loadingSkeleton?: ReactNode | undefined } + | { loadingSkeletonWidth?: number | undefined } + ))[]; +} & ( + | { + isLoading: true; + rows?: RowConfig[] | undefined; + } + | { + isLoading?: false | undefined; + rows: RowConfig[]; + } + ); + +type RowConfig = { + id: string | number; + data: Record; + header: ReactNode; + href?: string; + textValue: string; +}; + +export const EntityList = ({ + fields, + isLoading, + rows, + headerLoadingSkeleton, + className, + label, + ...props +}: Props) => ( + + {isLoading ? ( + +
{headerLoadingSkeleton}
+
+ {fields.map((field) => ( +
+
{field.name}
+
+ {"loadingSkeleton" in field ? ( + field.loadingSkeleton + ) : ( + + )} +
+
+ ))} +
+
+ ) : ( + ({ data, header, ...props }) => ( + +
{header}
+
+ {fields.map((field) => ( +
+
{field.name}
+
{data[field.id]}
+
+ ))} +
+
+ ) + )} +
+); diff --git a/apps/insights/src/components/Error/index.module.scss b/apps/insights/src/components/Error/index.module.scss index 82eb6edb3f..8ec6973977 100644 --- a/apps/insights/src/components/Error/index.module.scss +++ b/apps/insights/src/components/Error/index.module.scss @@ -1,19 +1,35 @@ @use "@pythnetwork/component-library/theme"; .error { - @include theme.max-width; - display: flex; flex-flow: column nowrap; gap: theme.spacing(12); align-items: center; text-align: center; - padding: theme.spacing(36) theme.spacing(0); + padding-top: theme.spacing(8); + padding-bottom: theme.spacing(8); + + @include theme.max-width; + + @include theme.breakpoint("sm") { + padding-top: theme.spacing(18); + padding-bottom: theme.spacing(18); + } + + @include theme.breakpoint("lg") { + padding-top: theme.spacing(36); + padding-bottom: theme.spacing(36); + } .errorIcon { - font-size: theme.spacing(20); - height: theme.spacing(20); + font-size: theme.spacing(14); + height: theme.spacing(14); color: theme.color("states", "error", "color"); + + @include theme.breakpoint("sm") { + font-size: theme.spacing(20); + height: theme.spacing(20); + } } .text { diff --git a/apps/insights/src/components/Explain/index.module.scss b/apps/insights/src/components/Explain/index.module.scss index 4f5e356312..42626e4c21 100644 --- a/apps/insights/src/components/Explain/index.module.scss +++ b/apps/insights/src/components/Explain/index.module.scss @@ -1,9 +1,17 @@ @use "@pythnetwork/component-library/theme"; -.trigger { - @each $size, $values in theme.$button-sizes { - &[data-size="#{$size}"] { - margin: -#{theme.map-get-strict($values, "padding")}; +.explain { + display: none; + + @include theme.breakpoint("sm") { + display: grid; + } + + .trigger { + @each $size, $values in theme.$button-sizes { + &[data-size="#{$size}"] { + margin: -#{theme.map-get-strict($values, "padding")}; + } } } } diff --git a/apps/insights/src/components/Explain/index.tsx b/apps/insights/src/components/Explain/index.tsx index a3b2badb88..477a306543 100644 --- a/apps/insights/src/components/Explain/index.tsx +++ b/apps/insights/src/components/Explain/index.tsx @@ -13,23 +13,25 @@ type Props = { }; export const Explain = ({ size, title, children }: Props) => ( - - - } - bodyClassName={styles.description} - > - {children} - - +
+ + + } + bodyClassName={styles.description} + > + {children} + + +
); diff --git a/apps/insights/src/components/NoResults/index.tsx b/apps/insights/src/components/NoResults/index.tsx index 0ac8c3622d..5afec069fa 100644 --- a/apps/insights/src/components/NoResults/index.tsx +++ b/apps/insights/src/components/NoResults/index.tsx @@ -16,7 +16,7 @@ type Props = { } ); -type Variant = "success" | "error" | "warning" | "info" | "data"; +export type Variant = "success" | "error" | "warning" | "info" | "data"; export const NoResults = ({ onClearSearch, ...props }: Props) => (
* { - flex: 1 1 0px; - width: 0; - } - - .publishersChart, - .priceFeedsChart { - & svg { - cursor: pointer; - } - } + @include theme.h3; } .overviewMainContent { display: grid; - grid-template-columns: repeat(2, 1fr); - gap: theme.spacing(40); align-items: center; - padding: theme.spacing(18) 0; + padding-top: theme.spacing(6); + padding-bottom: theme.spacing(30); - .headline { - @include theme.text("3xl", "medium"); + @include theme.breakpoint("md") { + grid-template-columns: repeat(2, 1fr); + column-gap: theme.spacing(20); + padding-top: theme.spacing(12); + } - color: theme.color("heading"); - line-height: 125%; - margin-top: theme.spacing(8); - margin-bottom: theme.spacing(4); + @include theme.breakpoint("xl") { + column-gap: theme.spacing(40); } - .message { - @include theme.text("base", "normal"); + .intro { + margin-bottom: theme.spacing(6); + + .headline { + @include theme.text("3xl", "medium"); + + color: theme.color("heading"); + line-height: 125%; + margin-top: theme.spacing(8); + margin-bottom: theme.spacing(4); + } + + .message { + @include theme.text("base", "normal"); - color: theme.color("heading"); - line-height: 150%; + color: theme.color("heading"); + line-height: 150%; + } } .tabList { - margin: theme.spacing(12) 0; + margin: theme.spacing(6) 0; + + @include theme.breakpoint("md") { + margin: theme.spacing(12) 0; + grid-column: 1; + grid-row: 2; + } + } + + .imagePanel { + display: flex; + place-content: center; + + @include theme.breakpoint("md") { + grid-row: span 3 / span 3; + grid-column: 2; + } + + .darkImage, + .lightImage { + max-height: theme.spacing(80); + + @include theme.breakpoint("md") { + max-height: theme.spacing(120); + } + } + + .lightImage { + @at-root html[data-theme="dark"] & { + display: none; + } + } + + .darkImage { + @at-root html[data-theme="light"] & { + display: none; + } + } } .buttons { display: flex; flex-flow: row nowrap; gap: theme.spacing(3); + + @include theme.breakpoint("md") { + grid-column: 1; + grid-row: 3; + } } } } - -html[data-theme="dark"] .lightImage { - display: none; -} - -html[data-theme="light"] .darkImage { - display: none; -} diff --git a/apps/insights/src/components/Overview/index.tsx b/apps/insights/src/components/Overview/index.tsx index 5caf3075ed..81e61a4d91 100644 --- a/apps/insights/src/components/Overview/index.tsx +++ b/apps/insights/src/components/Overview/index.tsx @@ -15,6 +15,7 @@ import { activePublishers, activeFeeds, } from "../../static-data/stats"; +import { Cards } from "../Cards"; import { ChangePercent } from "../ChangePercent"; import { ChartCard } from "../ChartCard"; import { FormattedDate } from "../FormattedDate"; @@ -23,7 +24,7 @@ import { FormattedNumber } from "../FormattedNumber"; export const Overview = () => (

Overview

-
+ ( } stat={activeChains.at(-1)?.chains} /> -
+ -
+
INSIGHTS

Get the most from the Pyth Network

Insights Hub delivers transparency over the network status and - performance, and maximize productivity while integrating. + performance, and maximizes productivity while integrating.

- -
- - -
@@ -151,6 +129,7 @@ export const Overview = () => ( }, { id: "price feeds", + className: styles.imagePanel ?? "", children: ( <> @@ -160,6 +139,30 @@ export const Overview = () => ( }, ]} /> + +
+ + +
); diff --git a/apps/insights/src/components/PriceComponentDrawer/index.module.scss b/apps/insights/src/components/PriceComponentDrawer/index.module.scss index 87c45dcc34..6617515442 100644 --- a/apps/insights/src/components/PriceComponentDrawer/index.module.scss +++ b/apps/insights/src/components/PriceComponentDrawer/index.module.scss @@ -1,17 +1,52 @@ @use "@pythnetwork/component-library/theme"; .priceComponentDrawer { + .badges { + @include theme.breakpoint("lg") { + display: none; + } + } + + .ghostOpenButton { + @include theme.breakpoint("md") { + display: none; + } + } + + .bigScreenBadges { + display: none; + + @include theme.breakpoint("lg") { + display: flex; + flex-flow: row nowrap; + gap: theme.spacing(3); + align-items: center; + } + } + + .outlineOpenButton { + display: none; + + @include theme.breakpoint("md") { + display: inline flow-root; + } + } + .testFeedMessage { - grid-column: span 2 / span 2; margin-bottom: theme.spacing(10); } .stats { display: grid; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: repeat(2, 1fr); + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); gap: theme.spacing(4); margin-bottom: theme.spacing(10); + + @include theme.breakpoint("lg") { + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + } } .spinner { @@ -129,6 +164,64 @@ margin: theme.spacing(2) theme.spacing(4); } + .smallLegend { + list-style-type: none; + padding: 0; + margin: 0; + background: theme.color("background", "primary"); + border-radius: theme.border-radius("xl"); + + li { + padding: theme.spacing(3) theme.spacing(4); + border-bottom: 1px solid theme.color("background", "secondary"); + + &:last-child { + border-bottom: none; + } + + dl { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + font-size: theme.font-size("sm"); + margin: 0; + margin-top: theme.spacing(4); + + .weight, + .scoreValue { + display: flex; + flex-flow: row nowrap; + gap: theme.spacing(2); + + dt { + font-weight: theme.font-weight("medium"); + } + + dd { + padding: 0; + margin: 0; + } + } + } + } + + .metricDescription { + display: none; + } + + @include theme.breakpoint("lg") { + display: none; + } + } + + .legendTable { + display: none; + + @include theme.breakpoint("lg") { + display: unset; + } + } + .scoreCell { vertical-align: top; } diff --git a/apps/insights/src/components/PriceComponentDrawer/index.tsx b/apps/insights/src/components/PriceComponentDrawer/index.tsx index 978583745f..d227a410ea 100644 --- a/apps/insights/src/components/PriceComponentDrawer/index.tsx +++ b/apps/insights/src/components/PriceComponentDrawer/index.tsx @@ -1,3 +1,4 @@ +import { ArrowSquareOut } from "@phosphor-icons/react/dist/ssr/ArrowSquareOut"; import { Flask } from "@phosphor-icons/react/dist/ssr/Flask"; import { Button } from "@pythnetwork/component-library/Button"; import { Card } from "@pythnetwork/component-library/Card"; @@ -113,17 +114,41 @@ export const PriceComponentDrawer = ({ title={title} headingExtra={ <> - {headingExtra} - +
+ {headingExtra} + +
- + } + headingAfter={ +
+ {headingExtra} + +
+ } isOpen={isFeedDrawerOpen} - bodyClassName={styles.priceComponentDrawer} + className={styles.priceComponentDrawer ?? ""} > {cluster === Cluster.PythtestConformance && ( { Score details for{" "} {currentPoint && dateFormatter.format(currentPoint.time)} +
    +
  • + +
    +
    +
    Weight
    +
    40%
    +
    +
    +
    Score
    +
    {numberFormatter.format(currentPoint?.uptimeScore ?? 0)}
    +
    +
    +
  • +
  • + +
    +
    +
    Weight
    +
    40%
    +
    +
    +
    Score
    +
    + {numberFormatter.format(currentPoint?.deviationScore ?? 0)} +
    +
    +
    +
  • +
  • + +
    +
    +
    Weight
    +
    20%
    +
    +
    +
    Score
    +
    {numberFormatter.format(currentPoint?.stalledScore ?? 0)}
    +
    +
    +
  • +
  • + +
    +
    +
    +
    Score
    +
    {numberFormatter.format(currentPoint?.score ?? 0)}
    +
    +
    +
  • +
*:nth-child(1) { + order: 1; + } + + dl > *:nth-child(2) { + order: 3; + } + + dl > *:nth-child(3) { + order: 5; + } + + dl > *:nth-child(4) { + order: 2; + } + + dl > *:nth-child(5) { + order: 4; + } + + dl > *:nth-child(6) { + order: 6; + } + + dl > *:nth-child(7) { + order: 8; + } + + dl > *:nth-child(8) { + order: 7; + } + } + } +} diff --git a/apps/insights/src/components/PriceComponentsCard/index.tsx b/apps/insights/src/components/PriceComponentsCard/index.tsx index 52bf59a06c..6ceb735f5f 100644 --- a/apps/insights/src/components/PriceComponentsCard/index.tsx +++ b/apps/insights/src/components/PriceComponentsCard/index.tsx @@ -14,10 +14,18 @@ import { type SortDescriptor, Table, } from "@pythnetwork/component-library/Table"; +import clsx from "clsx"; import { useQueryState, parseAsStringEnum, parseAsBoolean } from "nuqs"; -import { type ReactNode, Suspense, useMemo, useCallback } from "react"; +import { + type ReactNode, + Fragment, + Suspense, + useMemo, + useCallback, +} from "react"; import { useFilter, useCollator } from "react-aria"; +import styles from "./index.module.scss"; import { useQueryParamFilterPagination } from "../../hooks/use-query-param-filter-pagination"; import { Cluster } from "../../services/pyth"; import { @@ -26,6 +34,7 @@ import { Status as StatusType, statusNameToStatus, } from "../../status"; +import { EntityList } from "../EntityList"; import { Explain } from "../Explain"; import { EvaluationTime } from "../Explanations"; import { FormattedNumber } from "../FormattedNumber"; @@ -181,6 +190,7 @@ export const ResolvedPriceComponentsCard = < () => paginatedItems.map((component) => ({ id: component.id, + nameAsString: component.nameAsString, data: { name: component.name, ...Object.fromEntries( @@ -189,61 +199,56 @@ export const ResolvedPriceComponentsCard = < component[column.id], ]) ?? [], ), - ...(showQuality - ? { - score: component.score !== undefined && ( - - ), - uptimeScore: component.uptimeScore !== undefined && ( - - ), - deviationScore: component.deviationScore !== undefined && ( - - ), - stalledScore: component.stalledScore !== undefined && ( - - ), - } - : { - slot: ( - - ), - price: ( - - ), - confidence: ( - - ), - }), + score: component.score !== undefined && ( + + ), + uptimeScore: component.uptimeScore !== undefined && ( + + ), + deviationScore: component.deviationScore !== undefined && ( + + ), + stalledScore: component.stalledScore !== undefined && ( + + ), + slot: ( + + ), + price: ( + + ), + confidence: ( + + ), status: , }, onAction: () => { onPriceComponentAction(component); }, })), - [paginatedItems, showQuality, onPriceComponentAction, props.extraColumns], + [paginatedItems, onPriceComponentAction, props.extraColumns], ); const updateStatus = useCallback( @@ -322,7 +327,7 @@ type PriceComponentsCardProps< onStatusChange: (newStatus: StatusName | "") => void; showQuality: boolean; setShowQuality: (newValue: boolean) => void; - rows: RowConfig[]; + rows: (RowConfig & { nameAsString: string })[]; } ); @@ -343,7 +348,7 @@ export const PriceComponentsCardContents = < const collator = useCollator(); return ( {label} @@ -355,56 +360,66 @@ export const PriceComponentsCardContents = < } toolbar={ - <> - {toolbarExtra} - - label="Status" - size="sm" - variant="outline" - hideLabel - options={[ - "", - ...Object.values(STATUS_NAMES).toSorted((a, b) => - collator.compare(a, b), - ), - ]} - {...(props.isLoading - ? { isPending: true, buttonLabel: "Status" } - : { - show: (value) => (value === "" ? "All" : value), - placement: "bottom end", - buttonLabel: props.status === "" ? "Status" : props.status, - selectedKey: props.status, - onSelectionChange: props.onStatusChange, - })} - /> - - { - props.setShowQuality(newValue === "quality"); - }, - })} - items={[ - { - id: "prices", - children: , - }, - { id: "quality", children: "Quality" }, - ]} - /> - +
+ {toolbarExtra && ( +
+ {toolbarExtra} +
+ )} +
+ + label="Status" + size="sm" + variant="outline" + hideLabel + options={[ + "", + ...Object.values(STATUS_NAMES).toSorted((a, b) => + collator.compare(a, b), + ), + ]} + {...(props.isLoading + ? { isPending: true, buttonLabel: "Status" } + : { + show: (value) => (value === "" ? "All" : value), + placement: "bottom end", + buttonLabel: props.status === "" ? "Status" : props.status, + selectedKey: props.status, + onSelectionChange: props.onStatusChange, + })} + /> + +
+
+ { + props.setShowQuality(newValue === "quality"); + }, + })} + items={[ + { + id: "prices", + children: , + }, + { id: "quality", children: "Quality" }, + ]} + /> +
+
} {...(!props.isLoading && { footer: ( @@ -420,11 +435,44 @@ export const PriceComponentsCardContents = < ), })} > + ({ + ...row, + textValue: row.nameAsString, + header: ( + <> + {row.data.name} + {extraColumns?.map((column) => ( + {row.data[column.id]} + ))} + + ), + })) + } + />
* { - flex: 1 1 0px; + flex: 1 1 0; width: 0; + + @include theme.breakpoint("sm") { + flex: unset; + width: unset; + } + } + } + + .priceFeedSelect { + display: none; + + @include theme.breakpoint("sm") { + display: block; } + } - .confidenceExplainButton { - margin-top: -#{theme.button-padding("xs", false)}; - margin-right: -#{theme.button-padding("xs", false)}; + .priceFeedTag { + @include theme.breakpoint("sm") { + display: none; } } } @@ -48,9 +67,9 @@ } .body { - @include theme.max-width; - padding-top: theme.spacing(6); + + @include theme.max-width; } } diff --git a/apps/insights/src/components/PriceFeed/layout.tsx b/apps/insights/src/components/PriceFeed/layout.tsx index 1d1c90df3c..debdccc5e4 100644 --- a/apps/insights/src/components/PriceFeed/layout.tsx +++ b/apps/insights/src/components/PriceFeed/layout.tsx @@ -1,7 +1,4 @@ -import { Info } from "@phosphor-icons/react/dist/ssr/Info"; -import { Lightbulb } from "@phosphor-icons/react/dist/ssr/Lightbulb"; import { ListDashes } from "@phosphor-icons/react/dist/ssr/ListDashes"; -import { Alert, AlertTrigger } from "@pythnetwork/component-library/Alert"; import { Badge } from "@pythnetwork/component-library/Badge"; import { Breadcrumbs } from "@pythnetwork/component-library/Breadcrumbs"; import { Button } from "@pythnetwork/component-library/Button"; @@ -14,6 +11,8 @@ import styles from "./layout.module.scss"; import { PriceFeedSelect } from "./price-feed-select"; import { ReferenceData } from "./reference-data"; import { Cluster, getFeeds } from "../../services/pyth"; +import { Cards } from "../Cards"; +import { Explain } from "../Explain"; import { FeedKey } from "../FeedKey"; import { LivePrice, @@ -63,9 +62,10 @@ export const PriceFeedLayout = async ({ children, params }: Props) => {
- + +
{
-
+ { /> } corner={ - + +

+ Confidence is how far from the aggregate price Pyth + believes the true price might be. It reflects a combination of + the confidence of individual quoters and how well individual + quoters agree with each other. +

- }> -

- Confidence is how far from the aggregate price Pyth - believes the true price might be. It reflects a combination - of the confidence of individual quoters and how well - individual quoters agree with each other. -

- -
-
+ } /> { /> } /> -
+ { +export const PriceFeedSelect = ({ children, className }: Props) => { const feeds = usePriceFeeds(); const collator = useCollator(); const filter = useFilter({ sensitivity: "base", usage: "search" }); @@ -61,7 +63,7 @@ export const PriceFeedSelect = ({ children }: Props) => { return (
* { - flex: 1 1 0px; - width: 0; - } - .activeDate { color: theme.color("muted"); } @@ -55,9 +47,9 @@ } .body { - @include theme.max-width; - padding-top: theme.spacing(6); + + @include theme.max-width; } } @@ -68,9 +60,9 @@ grid-template-rows: repeat(4, max-content); gap: theme.spacing(4); - .oisMeter { + .oisMeter, + .smallOisMeter { grid-column: span 2 / span 2; - margin-bottom: -#{theme.spacing(12)}; .oisMeterIcon { font-size: theme.spacing(6); @@ -83,6 +75,24 @@ @include theme.text("xl", "medium"); } } + + .smallOisMeter { + margin-top: -#{theme.spacing(12)}; + margin-bottom: -#{theme.spacing(8)}; + + @include theme.breakpoint("md") { + display: none; + } + } + + .oisMeter { + margin-bottom: -#{theme.spacing(12)}; + display: none; + + @include theme.breakpoint("md") { + display: grid; + } + } } .oisDrawerFooter { diff --git a/apps/insights/src/components/Publisher/layout.tsx b/apps/insights/src/components/Publisher/layout.tsx index 642250ef5e..a690384623 100644 --- a/apps/insights/src/components/Publisher/layout.tsx +++ b/apps/insights/src/components/Publisher/layout.tsx @@ -25,6 +25,7 @@ import { import { getPublisherCaps } from "../../services/hermes"; import { Cluster, ClusterToName, parseCluster } from "../../services/pyth"; import { getPublisherPoolData } from "../../services/staking"; +import { Cards } from "../Cards"; import { ChangePercent } from "../ChangePercent"; import { ChangeValue } from "../ChangeValue"; import { ChartCard } from "../ChartCard"; @@ -116,7 +117,7 @@ export const PublishersLayout = async ({ children, params }: Props) => { icon: , })} /> -
+ { } > + + +
OIS Pool
+
{ )} -
+ *:first-child { - grid-column: span 2 / span 2; + @include theme.breakpoint("lg") { + grid-template-columns: 1fr 1fr; + + .publishersRankingCard { + grid-column: span 2 / span 2; + } + } + + .publishersRankingCard { + .publishersRankingList { + @include theme.breakpoint("sm") { + display: none; + } + + .ranking { + width: theme.spacing(20); + } + } + + .publishersRankingTable { + display: none; + + @include theme.breakpoint("sm") { + display: unset; + } + } } } diff --git a/apps/insights/src/components/Publisher/performance.tsx b/apps/insights/src/components/Publisher/performance.tsx index 101f3d79c3..abbd2dd96d 100644 --- a/apps/insights/src/components/Publisher/performance.tsx +++ b/apps/insights/src/components/Publisher/performance.tsx @@ -8,6 +8,7 @@ import { Link } from "@pythnetwork/component-library/Link"; import { Table } from "@pythnetwork/component-library/Table"; import { lookup } from "@pythnetwork/known-publishers"; import { notFound } from "next/navigation"; +import type { ReactNode } from "react"; import { getPriceFeeds } from "./get-price-feeds"; import styles from "./performance.module.scss"; @@ -15,12 +16,13 @@ import { TopFeedsTable } from "./top-feeds-table"; import { getPublishers } from "../../services/clickhouse"; import { ClusterToName, parseCluster } from "../../services/pyth"; import { Status } from "../../status"; +import { EntityList } from "../EntityList"; import { ExplainActive, ExplainInactive, ExplainAverage, } from "../Explanations"; -import { NoResults } from "../NoResults"; +import { type Variant as NoResultsVariant, NoResults } from "../NoResults"; import { PriceFeedTag } from "../PriceFeedTag"; import { PublisherIcon } from "../PublisherIcon"; import { PublisherTag } from "../PublisherTag"; @@ -52,15 +54,95 @@ export const Performance = async ({ params }: Props) => { (publisher) => publisher.key === key, 2, ); + const rows = slicedPublishers?.map((publisher) => { + const knownPublisher = lookup(publisher.key); + return { + id: publisher.key, + nameAsString: knownPublisher?.name ?? publisher.key, + data: { + ranking: ( + + {publisher.rank} + + ), + activeFeeds: ( + + {publisher.activeFeeds} + + ), + inactiveFeeds: ( + + {publisher.inactiveFeeds} + + ), + averageScore: ( + + ), + name: ( + , + })} + /> + ), + }, + ...(publisher.key !== key && { + href: `/publishers/${ClusterToName[parsedCluster]}/${publisher.key}`, + }), + }; + }); + + const highPerformingFeeds = getFeedRows( + priceFeeds + .filter((feed) => hasRanking(feed)) + .filter(({ ranking }) => ranking.final_score > 0.9) + .sort((a, b) => b.ranking.final_score - a.ranking.final_score), + ); + + const lowPerformingFeeds = getFeedRows( + priceFeeds + .filter((feed) => hasRanking(feed)) + .filter(({ ranking }) => ranking.final_score < 0.7) + .sort((a, b) => a.ranking.final_score - b.ranking.final_score), + ); - return slicedPublishers === undefined ? ( + return rows === undefined ? ( notFound() ) : (
- } title="Publishers Ranking"> + } + title="Publishers Ranking" + className={styles.publishersRankingCard ?? ""} + > + ({ + ...row, + textValue: row.nameAsString, + header: row.data.name, + }))} + />
{ width: PUBLISHER_SCORE_WIDTH, }, ]} - rows={slicedPublishers.map((publisher) => { - const knownPublisher = lookup(publisher.key); - return { - id: publisher.key, - data: { - ranking: ( - - {publisher.rank} - - ), - activeFeeds: ( - - {publisher.activeFeeds} - - ), - inactiveFeeds: ( - - {publisher.inactiveFeeds} - - ), - averageScore: ( - - ), - name: ( - , - })} - /> - ), - }, - ...(publisher.key !== key && { - href: `/publishers/${ClusterToName[parsedCluster]}/${publisher.key}`, - }), - }; - })} - /> - - } title="High-Performing Feeds"> - } - header="Oh no!" - body="This publisher has no high performing feeds" - variant="error" - /> - } - rows={getFeedRows( - priceFeeds - .filter((feed) => hasRanking(feed)) - .filter(({ ranking }) => ranking.final_score > 0.9) - .sort((a, b) => b.ranking.final_score - a.ranking.final_score), - )} - /> - - } title="Low-Performing Feeds"> - } - header="Looking good!" - body="This publisher has no low performing feeds" - variant="success" - /> - } - rows={getFeedRows( - priceFeeds - .filter((feed) => hasRanking(feed)) - .filter(({ ranking }) => ranking.final_score < 0.7) - .sort((a, b) => a.ranking.final_score - b.ranking.final_score), - )} + rows={rows} /> + } + emptyHeader="Oh no!" + emptyBody="This publisher has no high performing feeds" + emptyVariant="error" + feeds={highPerformingFeeds} + /> + } + emptyHeader="Looking good!" + emptyBody="This publisher has no low performing feeds" + emptyVariant="success" + feeds={lowPerformingFeeds} + /> ); }; @@ -216,7 +227,8 @@ const getFeedRows = ( .filter((feed) => feed.status === Status.Active) .slice(0, 20) .map(({ feed, ranking }) => ({ - id: ranking.symbol, + id: feed.symbol, + textValue: feed.symbol, data: { asset: , assetClass: ( @@ -251,3 +263,38 @@ const sliceAround = ( const hasRanking = (feed: { ranking: T | undefined; }): feed is { ranking: T } => feed.ranking !== undefined; + +type TopFeedsCardProps = { + title: string; + emptyIcon: ReactNode; + emptyHeader: string; + emptyBody: string; + emptyVariant: NoResultsVariant; + feeds: ReturnType; +}; + +const TopFeedsCard = ({ + title, + emptyIcon, + emptyHeader, + emptyBody, + emptyVariant, + feeds, +}: TopFeedsCardProps) => ( + } title={`${title} Feeds`}> + {feeds.length === 0 ? ( + + ) : ( + + )} + +); diff --git a/apps/insights/src/components/Publisher/top-feeds-table.module.scss b/apps/insights/src/components/Publisher/top-feeds-table.module.scss new file mode 100644 index 0000000000..9663269b74 --- /dev/null +++ b/apps/insights/src/components/Publisher/top-feeds-table.module.scss @@ -0,0 +1,15 @@ +@use "@pythnetwork/component-library/theme"; + +.list { + @include theme.breakpoint("lg") { + display: none; + } +} + +.table { + display: none; + + @include theme.breakpoint("lg") { + display: unset; + } +} diff --git a/apps/insights/src/components/Publisher/top-feeds-table.tsx b/apps/insights/src/components/Publisher/top-feeds-table.tsx index c97e30df17..b388a95cbf 100644 --- a/apps/insights/src/components/Publisher/top-feeds-table.tsx +++ b/apps/insights/src/components/Publisher/top-feeds-table.tsx @@ -1,22 +1,19 @@ "use client"; import { type RowConfig, Table } from "@pythnetwork/component-library/Table"; -import { type ReactNode, useMemo } from "react"; +import { useMemo } from "react"; import { useSelectPriceFeed } from "./price-feed-drawer-provider"; +import styles from "./top-feeds-table.module.scss"; +import { EntityList } from "../EntityList"; type Props = { publisherScoreWidth: number; - rows: RowConfig<"score" | "asset" | "assetClass">[]; - emptyState: ReactNode; + rows: (RowConfig<"score" | "asset" | "assetClass"> & { textValue: string })[]; label: string; }; -export const TopFeedsTable = ({ - publisherScoreWidth, - rows, - ...props -}: Props) => { +export const TopFeedsTable = ({ publisherScoreWidth, rows, label }: Props) => { const selectPriceFeed = useSelectPriceFeed(); const rowsWithAction = useMemo( @@ -33,32 +30,47 @@ export const TopFeedsTable = ({ ); return ( -
+ <> + ({ + ...row, + textValue: row.textValue, + header: row.data.asset, + }))} + /> +
+ ); }; diff --git a/apps/insights/src/components/PublisherTag/index.module.scss b/apps/insights/src/components/PublisherTag/index.module.scss index bb1386c638..9d4c77a1cf 100644 --- a/apps/insights/src/components/PublisherTag/index.module.scss +++ b/apps/insights/src/components/PublisherTag/index.module.scss @@ -63,7 +63,7 @@ } .testBadge { - margin-left: theme.spacing(4); + margin-left: theme.spacing(2); } &[data-loading] { diff --git a/apps/insights/src/components/Publishers/index.module.scss b/apps/insights/src/components/Publishers/index.module.scss index e5549a60de..181b2d3eb6 100644 --- a/apps/insights/src/components/Publishers/index.module.scss +++ b/apps/insights/src/components/Publishers/index.module.scss @@ -1,6 +1,8 @@ @use "@pythnetwork/component-library/theme"; @use "../Root/index.module.scss" as root; +$gap: theme.spacing(4); + .publishers { @include theme.max-width; @@ -11,21 +13,24 @@ justify-content: space-between; .header { - @include theme.h3; - color: theme.color("heading"); - font-weight: theme.font-weight("semibold"); + + @include theme.h3; } .rankingsLastUpdated { @include theme.text("sm", "normal"); color: theme.color("muted"); - display: flex; flex-flow: row nowrap; gap: theme.spacing(1); align-items: center; line-height: normal; + display: none; + + @include theme.breakpoint("sm") { + display: flex; + } .clockIcon { font-size: theme.spacing(5); @@ -34,62 +39,82 @@ } .body { - display: flex; - flex-flow: row nowrap; - gap: theme.spacing(12); - align-items: flex-start; - margin-top: theme.spacing(6); - - .stats { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: theme.spacing(4); - align-items: center; - width: 30%; - position: sticky; - top: root.$header-height; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $gap; + margin-top: theme.spacing(4); + + @include theme.breakpoint("2xl") { + grid-template-columns: 15% 15% 1fr; + grid-template-rows: max-content 1fr; + margin-top: theme.spacing(6); + } + + .statCard { + @include theme.breakpoint("2xl") { + position: sticky; + top: root.$header-height; + } + } - .averageMedianScoreExplainButton { - margin-top: -#{theme.button-padding("xs", false)}; - margin-right: -#{theme.button-padding("xs", false)}; + .publishersCard { + grid-column: span 2 / span 2; + + @include theme.breakpoint("2xl") { + grid-column: 3; + grid-row: span 2 / span 2; + margin-left: theme.spacing(6); + } + } + + .oisCard { + grid-column: span 2 / span 2; + + @include theme.breakpoint("2xl") { + grid-row: 2; + align-self: start; + position: sticky; + + // TODO the following should be made into variables so we don't have + // to copy the values around... + $card-content: theme.spacing(15); + $card-pt: theme.spacing(3); + $card-pb: theme.spacing(2); + $card-wrapper-p: (2 * theme.spacing(1)); + $card-height: $card-content + $card-pt + $card-pb + $card-wrapper-p; + + top: calc(root.$header-height + $gap + $card-height); } - .oisCard { - grid-column: span 2 / span 2; - - .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"); - - @include theme.h3; - } - - .poolTotal { - margin: 0; - color: theme.color("muted"); - font-size: theme.font-size("sm"); - font-weight: theme.font-weight("normal"); - } + .oisPool { + .title { + font-size: theme.font-size("sm"); + font-weight: theme.font-weight("normal"); + color: theme.color("heading"); + margin: 0; + } + + .poolUsed { + line-height: 125%; + letter-spacing: letter-spacing("tighter"); + color: theme.color("heading"); + + @include theme.text("xl", "medium"); } - .oisStats { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: theme.spacing(1); + .poolTotal { + margin: 0; + color: theme.color("muted"); + font-size: theme.font-size("sm"); + font-weight: theme.font-weight("normal"); } } - } - .publishersCard { - width: 70%; + .oisStats { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: theme.spacing(1); + } } } } diff --git a/apps/insights/src/components/Publishers/index.tsx b/apps/insights/src/components/Publishers/index.tsx index 07034feffa..cd650359f1 100644 --- a/apps/insights/src/components/Publishers/index.tsx +++ b/apps/insights/src/components/Publishers/index.tsx @@ -55,83 +55,23 @@ export const Publishers = async () => { )}
-
- - } - stat={( - pythnetPublishers.reduce( - (sum, publisher) => sum + publisher.averageScore, - 0, - ) / pythnetPublishers.length - ).toFixed(2)} - /> - - Staking App - - } - > - - -

- -

-

- /{" "} - -

-
-
- - - - - } - /> - - - - - } - /> -
-
-
+ + } + className={styles.statCard ?? ""} + stat={( + pythnetPublishers.reduce( + (sum, publisher) => sum + publisher.averageScore, + 0, + ) / pythnetPublishers.length + ).toFixed(2)} + /> } @@ -142,6 +82,63 @@ export const Publishers = async () => { (publisher) => toTableRow(publisher), )} /> + + Staking App + + } + > + + +

+ +

+

+ /{" "} + +

+
+
+ + + + + } + /> + + + + + } + /> +
+
); diff --git a/apps/insights/src/components/Publishers/publishers-card.module.scss b/apps/insights/src/components/Publishers/publishers-card.module.scss new file mode 100644 index 0000000000..9c8b07c527 --- /dev/null +++ b/apps/insights/src/components/Publishers/publishers-card.module.scss @@ -0,0 +1,41 @@ +@use "@pythnetwork/component-library/theme"; + +.publishersCard { + .toolbar { + .searchInput { + flex-grow: 1; + } + } + + .table { + display: none; + + @include theme.breakpoint("lg") { + display: unset; + } + } + + .entityList { + @include theme.breakpoint("lg") { + display: none; + } + + @include theme.breakpoint("sm") { + dl > *:nth-child(1) { + order: 2; + } + + dl > *:nth-child(2) { + order: 1; + } + + dl > *:nth-child(3) { + order: 3; + } + } + + .rankingWraper { + width: theme.spacing(15); + } + } +} diff --git a/apps/insights/src/components/Publishers/publishers-card.tsx b/apps/insights/src/components/Publishers/publishers-card.tsx index 7f86ca57e9..ddaca905a3 100644 --- a/apps/insights/src/components/Publishers/publishers-card.tsx +++ b/apps/insights/src/components/Publishers/publishers-card.tsx @@ -14,12 +14,15 @@ import { type SortDescriptor, Table, } from "@pythnetwork/component-library/Table"; +import clsx from "clsx"; import { useQueryState, parseAsStringEnum } from "nuqs"; import { type ReactNode, Suspense, useMemo, useCallback } from "react"; import { useFilter, useCollator } from "react-aria"; +import styles from "./publishers-card.module.scss"; import { useQueryParamFilterPagination } from "../../hooks/use-query-param-filter-pagination"; import { CLUSTER_NAMES } from "../../services/pyth"; +import { EntityList } from "../EntityList"; import { ExplainActive, ExplainInactive } from "../Explanations"; import { NoResults } from "../NoResults"; import { PublisherTag } from "../PublisherTag"; @@ -133,6 +136,7 @@ const ResolvedPublishersCard = ({ }) => ({ id, href: `/publishers/${cluster}/${id}`, + textValue: publisher.name ?? id, data: { ranking: {ranking}, name: ( @@ -218,9 +222,9 @@ type PublishersCardContentsProps = Pick & mkPageLink: (page: number) => string; cluster: (typeof CLUSTER_NAMES)[number]; onChangeCluster: (value: (typeof CLUSTER_NAMES)[number]) => void; - rows: RowConfig< + rows: (RowConfig< "ranking" | "name" | "activeFeeds" | "inactiveFeeds" | "averageScore" - >[]; + > & { textValue: string })[]; } ); @@ -230,7 +234,7 @@ const PublishersCardContents = ({ ...props }: PublishersCardContentsProps) => ( } title={ <> @@ -242,8 +246,21 @@ const PublishersCardContents = ({ )} } + toolbarClassName={styles.toolbar} toolbar={ <> +
(
-
+
Pyth Homepage
- + Help - + + Documentation
-
+
{socialLinks.map(({ name, ...props }) => ( {name} diff --git a/apps/insights/src/components/Root/header.module.scss b/apps/insights/src/components/Root/header.module.scss index 65a3ff8506..8cb448893c 100644 --- a/apps/insights/src/components/Root/header.module.scss +++ b/apps/insights/src/components/Root/header.module.scss @@ -15,10 +15,14 @@ @include theme.max-width; .leftMenu { + @include theme.row; + flex: none; - gap: theme.spacing(6); + gap: theme.spacing(3); - @include theme.row; + @include theme.breakpoint("sm") { + gap: theme.spacing(6); + } .logoLink { padding: theme.spacing(3); @@ -26,10 +30,15 @@ color: theme.color("foreground"); .logoWrapper { - width: theme.spacing(9); - height: theme.spacing(9); + width: theme.spacing(8); + height: theme.spacing(8); position: relative; + @include theme.breakpoint("sm") { + width: theme.spacing(9); + height: theme.spacing(9); + } + .logo { position: absolute; top: 0; @@ -48,16 +57,57 @@ font-weight: theme.font-weight("semibold"); color: theme.color("heading"); } + + .mainNavTabs { + display: none; + + @include theme.breakpoint("sm") { + display: flex; + } + } } .rightMenu { - flex: none; - gap: theme.spacing(2); - @include theme.row; + flex: none; + gap: theme.spacing(3); margin-right: -#{theme.button-padding("sm", false)}; + @include theme.breakpoint("lg") { + gap: theme.spacing(2); + } + + .supportButton, + .themeSwitch, + .mainCta { + display: none; + + @include theme.breakpoint("lg") { + display: unset; + } + } + + .outlineSearchButton { + display: none; + + @include theme.breakpoint("md") { + display: unset; + } + } + + .ghostSearchButton { + @include theme.breakpoint("md") { + display: none; + } + } + + .mobileMenu { + @include theme.breakpoint("lg") { + display: none; + } + } + .themeSwitch { margin-left: theme.spacing(1); } diff --git a/apps/insights/src/components/Root/header.tsx b/apps/insights/src/components/Root/header.tsx index d83d733ccb..fcaef4b109 100644 --- a/apps/insights/src/components/Root/header.tsx +++ b/apps/insights/src/components/Root/header.tsx @@ -1,17 +1,23 @@ import { Lifebuoy } from "@phosphor-icons/react/dist/ssr/Lifebuoy"; import { Button } from "@pythnetwork/component-library/Button"; +import { DrawerTrigger } from "@pythnetwork/component-library/Drawer"; import { Link } from "@pythnetwork/component-library/Link"; import clsx from "clsx"; import type { ComponentProps } from "react"; import styles from "./header.module.scss"; import Logo from "./logo.svg"; -import { SearchButton } from "./search-button"; +import { MobileMenu } from "./mobile-menu"; +import { SearchButton, SearchShortcutText } from "./search-button"; import { SupportDrawer } from "./support-drawer"; import { MainNavTabs } from "./tabs"; import { ThemeSwitch } from "./theme-switch"; -export const Header = ({ className, ...props }: ComponentProps<"header">) => ( +type Props = ComponentProps<"header"> & { + tabs: ComponentProps["items"]; +}; + +export const Header = ({ className, tabs, ...props }: Props) => (
@@ -22,20 +28,41 @@ export const Header = ({ className, ...props }: ComponentProps<"header">) => (
Pyth Homepage
Insights
- +
- - - - + + + + + + + Search + + 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 ( + <> + + + +
    +
    + + +
    +
    + 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 ( + + ); +}; + +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} - -
    -
    - - -
    - -
    -
    - - - +
    +
    + { - setSearch(""); - }} - /> - } - > - {(result) => ( - -
    - - {result.type === ResultType.PriceFeed - ? "PRICE FEED" - : "PUBLISHER"} - -
    - {result.type === ResultType.PriceFeed ? ( - <> - - - - ) : ( - <> - - - - )} -
    - )} - - - + autoFocus + /> + +
    + +
    +
    + + + { + 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} - +}: 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} + +
    +
    + {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 && }