diff --git a/apps/insights/next.config.js b/apps/insights/next.config.js index 58ecfdcf9e..0a7b291b9e 100644 --- a/apps/insights/next.config.js +++ b/apps/insights/next.config.js @@ -3,6 +3,10 @@ const config = { pageExtensions: ["ts", "tsx", "mdx"], + experimental: { + useCache: true, + }, + logging: { fetches: { fullUrl: true, diff --git a/apps/insights/src/app/layout.ts b/apps/insights/src/app/layout.ts index 1818ef541f..47f8c23fd1 100644 --- a/apps/insights/src/app/layout.ts +++ b/apps/insights/src/app/layout.ts @@ -1,5 +1,2 @@ export { Root as default } from "../components/Root"; export { metadata, viewport } from "../metadata"; - -export const dynamic = "error"; -export const revalidate = 3600; diff --git a/apps/insights/src/app/price-feeds/[slug]/(main)/loading.ts b/apps/insights/src/app/price-feeds/[slug]/(main)/loading.ts new file mode 100644 index 0000000000..a1085ea612 --- /dev/null +++ b/apps/insights/src/app/price-feeds/[slug]/(main)/loading.ts @@ -0,0 +1 @@ +export { ChartPageLoading as default } from "../../../../components/PriceFeed/chart-page"; diff --git a/apps/insights/src/app/price-feeds/[slug]/(main)/page.ts b/apps/insights/src/app/price-feeds/[slug]/(main)/page.ts new file mode 100644 index 0000000000..165ad5abcd --- /dev/null +++ b/apps/insights/src/app/price-feeds/[slug]/(main)/page.ts @@ -0,0 +1,3 @@ +export { ChartPage as default } from "../../../../components/PriceFeed/chart-page"; + +export const revalidate = 3600; diff --git a/apps/insights/src/app/price-feeds/[slug]/@feedCountBadge/default.ts b/apps/insights/src/app/price-feeds/[slug]/@feedCountBadge/default.ts new file mode 100644 index 0000000000..7d697623ea --- /dev/null +++ b/apps/insights/src/app/price-feeds/[slug]/@feedCountBadge/default.ts @@ -0,0 +1 @@ +export { FeedCountBadge as default } from "../../../../components/PriceFeed/feed-count-badge"; diff --git a/apps/insights/src/app/price-feeds/[slug]/@header/default.ts b/apps/insights/src/app/price-feeds/[slug]/@header/default.ts new file mode 100644 index 0000000000..a14ec313a1 --- /dev/null +++ b/apps/insights/src/app/price-feeds/[slug]/@header/default.ts @@ -0,0 +1 @@ +export { PriceFeedHeader as default } from "../../../../components/PriceFeed/header"; diff --git a/apps/insights/src/app/price-feeds/[slug]/layout.ts b/apps/insights/src/app/price-feeds/[slug]/layout.ts index 34bbe99738..fa1b392938 100644 --- a/apps/insights/src/app/price-feeds/[slug]/layout.ts +++ b/apps/insights/src/app/price-feeds/[slug]/layout.ts @@ -1,11 +1,14 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; +import type { ReactNode } from "react"; import { Cluster, getFeeds } from "../../../services/pyth"; export { PriceFeedLayout as default } from "../../../components/PriceFeed/layout"; type Props = { + feedCountBadge: ReactNode; + header: ReactNode; params: Promise<{ slug: string; }>; @@ -29,5 +32,4 @@ export const generateMetadata = async ({ : notFound(); }; -export const dynamic = "error"; export const revalidate = 3600; diff --git a/apps/insights/src/app/price-feeds/[slug]/page.ts b/apps/insights/src/app/price-feeds/[slug]/page.ts deleted file mode 100644 index 902b104ac0..0000000000 --- a/apps/insights/src/app/price-feeds/[slug]/page.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { ChartPage as default } from "../../../components/PriceFeed/chart-page"; - -export const dynamic = "error"; -export const revalidate = 3600; diff --git a/apps/insights/src/app/price-feeds/[slug]/publishers/loading.tsx b/apps/insights/src/app/price-feeds/[slug]/publishers/loading.tsx new file mode 100644 index 0000000000..41498a61ad --- /dev/null +++ b/apps/insights/src/app/price-feeds/[slug]/publishers/loading.tsx @@ -0,0 +1 @@ +export { PublishersLoading as default } from "../../../../components/PriceFeed/publishers"; diff --git a/apps/insights/src/app/price-feeds/[slug]/publishers/page.tsx b/apps/insights/src/app/price-feeds/[slug]/publishers/page.tsx index 806d389407..9da39f4a31 100644 --- a/apps/insights/src/app/price-feeds/[slug]/publishers/page.tsx +++ b/apps/insights/src/app/price-feeds/[slug]/publishers/page.tsx @@ -1,4 +1,3 @@ export { Publishers as default } from "../../../../components/PriceFeed/publishers"; -export const dynamic = "error"; export const revalidate = 3600; diff --git a/apps/insights/src/app/price-feeds/layout.ts b/apps/insights/src/app/price-feeds/layout.ts deleted file mode 100644 index 356243f82e..0000000000 --- a/apps/insights/src/app/price-feeds/layout.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Metadata } from "next"; - -export { ZoomLayoutTransition as default } from "../../components/ZoomLayoutTransition"; - -export const metadata: Metadata = { - title: { - default: "Price Feeds", - template: "%s | Price Feeds | Pyth Network Insights", - }, - description: "Explore market data on the Pyth network.", -}; diff --git a/apps/insights/src/app/price-feeds/page.ts b/apps/insights/src/app/price-feeds/page.ts index 4f5be74d0d..1c7b79d008 100644 --- a/apps/insights/src/app/price-feeds/page.ts +++ b/apps/insights/src/app/price-feeds/page.ts @@ -1,4 +1,13 @@ +import type { Metadata } from "next"; + export { PriceFeeds as default } from "../../components/PriceFeeds"; -export const dynamic = "error"; export const revalidate = 3600; + +export const metadata: Metadata = { + title: { + default: "Price Feeds", + template: "%s | Price Feeds | Pyth Network Insights", + }, + description: "Explore market data on the Pyth network.", +}; diff --git a/apps/insights/src/app/publishers/[cluster]/[key]/(performance)/loading.ts b/apps/insights/src/app/publishers/[cluster]/[key]/(performance)/loading.ts new file mode 100644 index 0000000000..e7ecd141e6 --- /dev/null +++ b/apps/insights/src/app/publishers/[cluster]/[key]/(performance)/loading.ts @@ -0,0 +1 @@ +export { PerformanceLoading as default } from "../../../../../components/Publisher/performance"; diff --git a/apps/insights/src/app/publishers/[cluster]/[key]/(performance)/page.ts b/apps/insights/src/app/publishers/[cluster]/[key]/(performance)/page.ts new file mode 100644 index 0000000000..32c901ddbc --- /dev/null +++ b/apps/insights/src/app/publishers/[cluster]/[key]/(performance)/page.ts @@ -0,0 +1,3 @@ +export { Performance as default } from "../../../../../components/Publisher/performance"; + +export const revalidate = 3600; diff --git a/apps/insights/src/app/publishers/[cluster]/[key]/layout.ts b/apps/insights/src/app/publishers/[cluster]/[key]/layout.ts index 190b693df9..c40c748c2f 100644 --- a/apps/insights/src/app/publishers/[cluster]/[key]/layout.ts +++ b/apps/insights/src/app/publishers/[cluster]/[key]/layout.ts @@ -1,7 +1,7 @@ import { lookup } from "@pythnetwork/known-publishers"; import type { Metadata } from "next"; -export { PublishersLayout as default } from "../../../../components/Publisher/layout"; +export { PublisherLayout as default } from "../../../../components/Publisher/layout"; type Props = { params: Promise<{ @@ -22,5 +22,4 @@ export const generateMetadata = async ({ }; }; -export const dynamic = "error"; export const revalidate = 3600; diff --git a/apps/insights/src/app/publishers/[cluster]/[key]/page.ts b/apps/insights/src/app/publishers/[cluster]/[key]/page.ts deleted file mode 100644 index 48bace34ce..0000000000 --- a/apps/insights/src/app/publishers/[cluster]/[key]/page.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { Performance as default } from "../../../../components/Publisher/performance"; - -export const dynamic = "error"; -export const revalidate = 3600; diff --git a/apps/insights/src/app/publishers/[cluster]/[key]/price-feeds/loading.tsx b/apps/insights/src/app/publishers/[cluster]/[key]/price-feeds/loading.tsx new file mode 100644 index 0000000000..84bf627bef --- /dev/null +++ b/apps/insights/src/app/publishers/[cluster]/[key]/price-feeds/loading.tsx @@ -0,0 +1 @@ +export { PriceFeedsLoading as default } from "../../../../../components/Publisher/price-feeds"; diff --git a/apps/insights/src/app/publishers/[cluster]/[key]/price-feeds/page.ts b/apps/insights/src/app/publishers/[cluster]/[key]/price-feeds/page.ts index cd2fe95a26..fbe7554eb4 100644 --- a/apps/insights/src/app/publishers/[cluster]/[key]/price-feeds/page.ts +++ b/apps/insights/src/app/publishers/[cluster]/[key]/price-feeds/page.ts @@ -1,4 +1,3 @@ export { PriceFeeds as default } from "../../../../../components/Publisher/price-feeds"; -export const dynamic = "error"; export const revalidate = 3600; diff --git a/apps/insights/src/app/publishers/layout.ts b/apps/insights/src/app/publishers/layout.ts deleted file mode 100644 index c4d4ea35eb..0000000000 --- a/apps/insights/src/app/publishers/layout.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Metadata } from "next"; - -export { ZoomLayoutTransition as default } from "../../components/ZoomLayoutTransition"; - -export const metadata: Metadata = { - title: { - default: "Publishers", - template: "%s | Publishers | Pyth Network Insights", - }, - description: "Explore publishers who contribute to the Pyth network.", -}; diff --git a/apps/insights/src/app/publishers/page.ts b/apps/insights/src/app/publishers/page.ts index 8c0e52ec30..71e08d0595 100644 --- a/apps/insights/src/app/publishers/page.ts +++ b/apps/insights/src/app/publishers/page.ts @@ -1,4 +1,12 @@ +import type { Metadata } from "next"; export { Publishers as default } from "../../components/Publishers"; -export const dynamic = "error"; export const revalidate = 3600; + +export const metadata: Metadata = { + title: { + default: "Publishers", + template: "%s | Publishers | Pyth Network Insights", + }, + description: "Explore publishers who contribute to the Pyth network.", +}; diff --git a/apps/insights/src/components/AssetClassBadge/index.tsx b/apps/insights/src/components/AssetClassBadge/index.tsx new file mode 100644 index 0000000000..3868e595b3 --- /dev/null +++ b/apps/insights/src/components/AssetClassBadge/index.tsx @@ -0,0 +1,12 @@ +import { Badge } from "@pythnetwork/component-library/Badge"; +import type { ComponentProps } from "react"; + +type Props = Omit, "children"> & { + children: string; +}; + +export const AssetClassBadge = ({ children, ...props }: Props) => ( + + {children.toUpperCase()} + +); diff --git a/apps/insights/src/components/AssetClassTag/index.tsx b/apps/insights/src/components/AssetClassTag/index.tsx deleted file mode 100644 index 73534a9abe..0000000000 --- a/apps/insights/src/components/AssetClassTag/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Badge } from "@pythnetwork/component-library/Badge"; -import type { ComponentProps } from "react"; - -import { usePriceFeeds } from "../../hooks/use-price-feeds"; - -type Props = ComponentProps & { - symbol: string; -}; - -export const AssetClassTag = ({ symbol }: Props) => { - const feed = usePriceFeeds().get(symbol); - - if (feed) { - return ( - - {feed.assetClass.toUpperCase()} - - ); - } else { - throw new NoSuchFeedError(symbol); - } -}; - -class NoSuchFeedError extends Error { - constructor(symbol: string) { - super(`No feed exists named ${symbol}`); - this.name = "NoSuchFeedError"; - } -} diff --git a/apps/insights/src/components/EntityList/index.tsx b/apps/insights/src/components/EntityList/index.tsx index eac8ebe55d..a27a252756 100644 --- a/apps/insights/src/components/EntityList/index.tsx +++ b/apps/insights/src/components/EntityList/index.tsx @@ -55,7 +55,7 @@ export const EntityList = ({ {...props} > {isLoading ? ( - +
{headerLoadingSkeleton}
{fields.map((field) => ( diff --git a/apps/insights/src/components/Explain/index.tsx b/apps/insights/src/components/Explain/index.tsx index 477a306543..d76036bc8a 100644 --- a/apps/insights/src/components/Explain/index.tsx +++ b/apps/insights/src/components/Explain/index.tsx @@ -1,6 +1,5 @@ import { Info } from "@phosphor-icons/react/dist/ssr/Info"; import { Lightbulb } from "@phosphor-icons/react/dist/ssr/Lightbulb"; -import { Alert, AlertTrigger } from "@pythnetwork/component-library/Alert"; import { Button } from "@pythnetwork/component-library/Button"; import type { ComponentProps, ReactNode } from "react"; @@ -14,24 +13,21 @@ type Props = { export const Explain = ({ size, title, children }: Props) => (
- - - } - bodyClassName={styles.description} - > - {children} - - +
); diff --git a/apps/insights/src/components/LayoutTransition/index.tsx b/apps/insights/src/components/LayoutTransition/index.tsx deleted file mode 100644 index 15622a1fed..0000000000 --- a/apps/insights/src/components/LayoutTransition/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -"use client"; - -import type { TargetAndTransition, Target } from "motion/react"; -import { AnimatePresence, motion } from "motion/react"; -import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime"; -import { useSelectedLayoutSegment } from "next/navigation"; -import type { ReactNode, ComponentProps } from "react"; -import { useContext, useEffect, useRef } from "react"; - -type OwnProps = { - children: ReactNode; - variants?: Record< - string, - | TargetAndTransition - | (( - custom: VariantArg, - current: Target, - velocity: Target, - ) => TargetAndTransition | string) - >; -}; - -export type VariantArg = { - segment: ReturnType; - prevSegment: ReturnType; -}; - -type Props = Omit, keyof OwnProps> & OwnProps; - -export const LayoutTransition = ({ children, ...props }: Props) => { - const segment = useSelectedLayoutSegment(); - const prevSegment = - useRef>(segment); - const nextSegment = - useRef>(segment); - - useEffect(() => { - nextSegment.current = segment; - }, [segment]); - - const updatePrevSegment = () => { - prevSegment.current = nextSegment.current; - }; - - return ( - - - {children} - - - ); -}; - -const FrozenRouter = ({ children }: { children: ReactNode }) => { - const context = useContext(LayoutRouterContext); - // eslint-disable-next-line unicorn/no-null - const prevContext = usePreviousValue(context) ?? null; - - const segment = useSelectedLayoutSegment(); - const prevSegment = usePreviousValue(segment); - - const changed = segment !== prevSegment && prevSegment !== undefined; - - return ( - - {children} - - ); -}; - -const usePreviousValue = (value: T): T | undefined => { - const prevValue = useRef(undefined); - - useEffect(() => { - prevValue.current = value; - return () => { - prevValue.current = undefined; - }; - }); - - return prevValue.current; -}; diff --git a/apps/insights/src/components/PriceComponentDrawer/index.tsx b/apps/insights/src/components/PriceComponentDrawer/index.tsx index 87a3047653..296ef7bcc2 100644 --- a/apps/insights/src/components/PriceComponentDrawer/index.tsx +++ b/apps/insights/src/components/PriceComponentDrawer/index.tsx @@ -1,17 +1,22 @@ import { ArrowSquareOut } from "@phosphor-icons/react/dist/ssr/ArrowSquareOut"; import { Flask } from "@phosphor-icons/react/dist/ssr/Flask"; +import { useLogger } from "@pythnetwork/app-logger"; +import type { Props as ButtonProps } from "@pythnetwork/component-library/Button"; import { Button } from "@pythnetwork/component-library/Button"; import { Card } from "@pythnetwork/component-library/Card"; -import { Drawer } from "@pythnetwork/component-library/Drawer"; import { InfoBox } from "@pythnetwork/component-library/InfoBox"; import { Select } from "@pythnetwork/component-library/Select"; import { Spinner } from "@pythnetwork/component-library/Spinner"; import { StatCard } from "@pythnetwork/component-library/StatCard"; import { Table } from "@pythnetwork/component-library/Table"; +import type { Button as UnstyledButton } from "@pythnetwork/component-library/unstyled/Button"; +import { useDrawer } from "@pythnetwork/component-library/useDrawer"; +import { useMountEffect } from "@react-hookz/web"; import dynamic from "next/dynamic"; import { useRouter } from "next/navigation"; +import { useQueryState, parseAsString } from "nuqs"; import type { ReactNode } from "react"; -import { Suspense, useState, useRef, useCallback, useMemo } from "react"; +import { Suspense, useState, useCallback, useMemo, useTransition } from "react"; import { RouterProvider, useDateFormatter, @@ -37,10 +42,8 @@ const LineChart = dynamic( }, ); -type Props = { - onClose: () => void; - title: ReactNode; - headingExtra?: ReactNode | undefined; +type PriceComponent = { + name: ReactNode; publisherKey: string; symbol: string; displaySymbol: string; @@ -50,50 +53,279 @@ type Props = { rank: number | undefined; status: Status; identifiesPublisher?: boolean | undefined; - navigateHref: string; - firstEvaluation: Date; + firstEvaluation?: Date | undefined; cluster: Cluster; }; -export const PriceComponentDrawer = ({ - publisherKey, - onClose, - symbol, - displaySymbol, - assetClass, - feedKey, - score, - rank, - title, - status, - headingExtra, - navigateHref, - firstEvaluation, - cluster, +export const usePriceComponentDrawer = ({ + components, identifiesPublisher, -}: Props) => { - const goToPriceFeedPageOnClose = useRef(false); - const [isFeedDrawerOpen, setIsFeedDrawerOpen] = useState(true); +}: { + components: PriceComponent[]; + identifiesPublisher?: boolean | undefined; +}) => { + const logger = useLogger(); + const drawer = useDrawer(); const router = useRouter(); - const handleClose = useCallback( - (isOpen: boolean) => { - if (!isOpen) { - setIsFeedDrawerOpen(false); + const [isRouting, startTransition] = useTransition(); + + const navigate = useCallback( + (route: string) => { + startTransition(() => { + router.push(route); + drawer.close().catch((error: unknown) => { + logger.error(error); + }); + }); + }, + [router, startTransition, logger, drawer], + ); + + const [selectedComponentId, setSelectedComponentId] = useQueryState( + identifiesPublisher ? "publisher" : "priceFeed", + parseAsString.withDefault(""), + ); + + const updateSelectedComponentId = useCallback( + (componentId: string) => { + if (!isRouting) { + setSelectedComponentId(componentId).catch((error: unknown) => { + logger.error(error); + }); } }, - [setIsFeedDrawerOpen], + [setSelectedComponentId, isRouting, logger], ); - const handleCloseFinish = useCallback(() => { - if (goToPriceFeedPageOnClose.current) { - router.push(navigateHref); - } else { - onClose(); + + const clearSelectedComponent = useCallback(() => { + updateSelectedComponentId(""); + }, [updateSelectedComponentId]); + + useMountEffect(() => { + if (selectedComponentId) { + const component = components.find( + (component) => + component[identifiesPublisher ? "publisherKey" : "feedKey"], + ); + if (component) { + openDrawer(component); + } } - }, [router, onClose, navigateHref]); - const handleOpenFeed = useCallback(() => { - goToPriceFeedPageOnClose.current = true; - setIsFeedDrawerOpen(false); - }, [setIsFeedDrawerOpen]); + }); + + const openDrawer = useCallback( + (component: PriceComponent) => { + drawer.open({ + title: component.name, + className: styles.priceComponentDrawer ?? "", + bodyClassName: styles.priceComponentDrawerBody ?? "", + onClose: clearSelectedComponent, + headingExtra: ( + + + + ), + headingAfter: ( +
+ +
+ ), + contents: ( + + {component.cluster === Cluster.PythtestConformance && ( + } + header={`This publisher is in test`} + className={styles.testFeedMessage} + > + This is a test publisher. Its prices are not included in the + Pyth aggregate price for {component.displaySymbol}. + + )} +
+ + Aggregated + + } + small + stat={ + + } + /> + + Publisher + + } + variant="primary" + small + stat={ + + } + /> + + } + /> + + } + /> + + ) : ( + <> + ) + } + /> + } + /> +
+ {component.firstEvaluation && ( + + )} +
+ ), + }); + }, + [clearSelectedComponent, drawer, identifiesPublisher, navigate], + ); + + const selectComponent = useCallback( + (component: PriceComponent) => { + updateSelectedComponentId( + component[identifiesPublisher ? "publisherKey" : "feedKey"], + ); + openDrawer(component); + }, + [updateSelectedComponentId, openDrawer, identifiesPublisher], + ); + + return { selectComponent }; +}; + +type HeadingExtraProps = { + status: Status; + identifiesPublisher?: boolean | undefined; + cluster: Cluster; + publisherKey: string; + symbol: string; +}; + +const HeadingExtra = ({ status, ...props }: HeadingExtraProps) => { + return ( + <> +
+ +
+ + + + ); +}; + +type OpenButtonProps = Omit, "children"> & { + identifiesPublisher?: boolean | undefined; + cluster: Cluster; + publisherKey: string; + symbol: string; +}; + +const OpenButton = ({ + identifiesPublisher, + cluster, + publisherKey, + symbol, + ...props +}: OpenButtonProps) => { + const href = useMemo( + () => + identifiesPublisher + ? `/publishers/${ClusterToName[cluster]}/${publisherKey}` + : `/price-feeds/${encodeURIComponent(symbol)}`, + [identifiesPublisher, cluster, publisherKey, symbol], + ); + + return ( + + ); +}; + +type ScoreBreakdownProps = { + firstEvaluation: Date; + cluster: Cluster; + publisherKey: string; + symbol: string; +}; + +const ScoreBreakdown = ({ + firstEvaluation, + cluster, + publisherKey, + symbol, +}: ScoreBreakdownProps) => { const { selectedPeriod, setSelectedPeriod, evaluationPeriods } = useEvaluationPeriods(firstEvaluation); const scoreHistoryState = useData( @@ -102,152 +334,32 @@ export const PriceComponentDrawer = ({ ); return ( - -
- {headingExtra} - -
- - - - - - } - headingAfter={ -
- {headingExtra} - -
+ { + const evaluationPeriod = evaluationPeriods.find( + (period) => period.label === label, + ); + if (evaluationPeriod) { + setSelectedPeriod(evaluationPeriod); + } + }} + options={evaluationPeriods.map(({ label }) => label)} + placement="bottom end" + /> } - isOpen={isFeedDrawerOpen} - className={styles.priceComponentDrawer ?? ""} - bodyClassName={styles.priceComponentDrawerBody ?? ""} > - {cluster === Cluster.PythtestConformance && ( - } - header={`This publisher is in test`} - className={styles.testFeedMessage} - > - This is a test publisher. Its prices are not included in the Pyth - aggregate price for {displaySymbol}. - - )} -
- - Aggregated - - } - small - stat={} - /> - - Publisher - - } - variant="primary" - small - stat={ - - } - /> - - } - /> - - } - /> - : <>} - /> - } - /> -
- { - const evaluationPeriod = evaluationPeriods.find( - (period) => period.label === label, - ); - if (evaluationPeriod) { - setSelectedPeriod(evaluationPeriod); - } - }} - options={evaluationPeriods.map(({ label }) => label)} - placement="bottom end" - /> - } - > - - -
+ + ); }; diff --git a/apps/insights/src/components/PriceComponentsCard/index.tsx b/apps/insights/src/components/PriceComponentsCard/index.tsx index 7b9e5d7745..ce5276ae72 100644 --- a/apps/insights/src/components/PriceComponentsCard/index.tsx +++ b/apps/insights/src/components/PriceComponentsCard/index.tsx @@ -35,6 +35,7 @@ import { EvaluationTime } from "../Explanations"; import { FormattedNumber } from "../FormattedNumber"; import { LivePrice, LiveConfidence, LiveComponentValue } from "../LivePrices"; import { NoResults } from "../NoResults"; +import { usePriceComponentDrawer } from "../PriceComponentDrawer"; import { PriceName } from "../PriceName"; import rootStyles from "../Root/index.module.scss"; import { Score } from "../Score"; @@ -44,21 +45,33 @@ const SCORE_WIDTH = 32; type Props> = { className?: string | undefined; - priceComponents: T[]; - metricsTime?: Date | undefined; nameLoadingSkeleton: ReactNode; label: string; searchPlaceholder: string; - onPriceComponentAction: (component: T) => void; toolbarExtra?: ReactNode; assetClass?: string | undefined; extraColumns?: ColumnConfig[] | undefined; nameWidth?: number | undefined; -}; + identifiesPublisher?: boolean | undefined; +} & ( + | { + isLoading: true; + } + | { + isLoading?: false | undefined; + priceComponents: T[]; + metricsTime?: Date | undefined; + } +); -type PriceComponent = { +export type PriceComponent = { id: string; score: number | undefined; + rank: number | undefined; + symbol: string; + displaySymbol: string; + firstEvaluation?: Date | undefined; + assetClass: string; uptimeScore: number | undefined; deviationScore: number | undefined; stalledScore: number | undefined; @@ -73,31 +86,45 @@ type PriceComponent = { export const PriceComponentsCard = < U extends string, T extends PriceComponent & Record, ->({ - priceComponents, - onPriceComponentAction, - ...props -}: Props) => ( - }> - - -); +>( + props: Props, +) => { + if (props.isLoading) { + return ; + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { isLoading, priceComponents, ...otherProps } = props; + return ( + } + > + + + ); + } +}; export const ResolvedPriceComponentsCard = < U extends string, T extends PriceComponent & Record, >({ priceComponents, - onPriceComponentAction, + identifiesPublisher, ...props -}: Props) => { +}: Omit, "isLoading"> & { + priceComponents: T[]; + metricsTime?: Date | undefined; +}) => { const logger = useLogger(); const collator = useCollator(); const filter = useFilter({ sensitivity: "base", usage: "search" }); + const { selectComponent } = usePriceComponentDrawer({ + components: priceComponents, + identifiesPublisher, + }); const [status, setStatus] = useQueryState( "status", parseAsStringEnum(["", ...Object.values(STATUS_NAMES)]).withDefault(""), @@ -186,6 +213,9 @@ export const ResolvedPriceComponentsCard = < paginatedItems.map((component) => ({ id: component.id, nameAsString: component.nameAsString, + onAction: () => { + selectComponent(component); + }, data: { name: component.name, ...Object.fromEntries( @@ -239,11 +269,8 @@ export const ResolvedPriceComponentsCard = < ), status: , }, - onAction: () => { - onPriceComponentAction(component); - }, })), - [paginatedItems, onPriceComponentAction, props.extraColumns], + [paginatedItems, props.extraColumns, selectComponent], ); const updateStatus = useCallback( @@ -294,7 +321,6 @@ type PriceComponentsCardProps< > = Pick< Props, | "className" - | "metricsTime" | "nameLoadingSkeleton" | "label" | "searchPlaceholder" @@ -307,6 +333,7 @@ type PriceComponentsCardProps< | { isLoading: true } | { isLoading?: false; + metricsTime?: Date | undefined; numResults: number; search: string; sortDescriptor: SortDescriptor; @@ -331,7 +358,6 @@ export const PriceComponentsCardContents = < T extends PriceComponent & Record, >({ className, - metricsTime, nameLoadingSkeleton, label, searchPlaceholder, @@ -347,11 +373,9 @@ export const PriceComponentsCardContents = < title={ <> {label} - {!props.isLoading && ( - - {props.numResults} - - )} + + {!props.isLoading && props.numResults} + } toolbar={ @@ -501,7 +525,9 @@ export const PriceComponentsCardContents = < Unranked feeds have not yet been evaluated by Pyth - {metricsTime && } + {!props.isLoading && props.metricsTime && ( + + )} ), diff --git a/apps/insights/src/components/PriceFeed/chart-page.module.scss b/apps/insights/src/components/PriceFeed/chart-page.module.scss index 6b63513b40..edab5582d0 100644 --- a/apps/insights/src/components/PriceFeed/chart-page.module.scss +++ b/apps/insights/src/components/PriceFeed/chart-page.module.scss @@ -6,6 +6,17 @@ height: theme.spacing(140); border-radius: theme.border-radius("xl"); overflow: hidden; + + .spinnerContainer { + width: 100%; + height: 100%; + display: grid; + place-content: center; + + .spinner { + font-size: theme.spacing(12); + } + } } } diff --git a/apps/insights/src/components/PriceFeed/chart-page.tsx b/apps/insights/src/components/PriceFeed/chart-page.tsx index 563a283060..aa2797827d 100644 --- a/apps/insights/src/components/PriceFeed/chart-page.tsx +++ b/apps/insights/src/components/PriceFeed/chart-page.tsx @@ -1,11 +1,11 @@ import { Info } from "@phosphor-icons/react/dist/ssr/Info"; import { Card } from "@pythnetwork/component-library/Card"; import { Link } from "@pythnetwork/component-library/Link"; -import { notFound } from "next/navigation"; +import { Spinner } from "@pythnetwork/component-library/Spinner"; import { Chart } from "./chart"; import styles from "./chart-page.module.scss"; -import { Cluster, getFeeds } from "../../services/pyth"; +import { getFeed } from "./get-feed"; type Props = { params: Promise<{ @@ -13,25 +13,44 @@ type Props = { }>; }; -export const ChartPage = async ({ params }: Props) => { - const [{ slug }, feeds] = await Promise.all([ - params, - getFeeds(Cluster.Pythnet), - ]); - const symbol = decodeURIComponent(slug); - const feed = feeds.find((item) => item.symbol === symbol); +export const ChartPage = async ({ params }: Props) => ( + +); - return feed ? ( - -
- -
- -
- ) : ( - notFound() - ); -}; +export const ChartPageLoading = () => ; + +type ChartPageImplProps = + | { isLoading: true } + | (Awaited> & { + isLoading?: false | undefined; + }); + +const ChartPageImpl = (props: ChartPageImplProps) => ( + +
+ {props.isLoading ? ( +
+ +
+ ) : ( + + )} +
+ {!props.isLoading && ( + + )} +
+); type DisclaimerProps = { displaySymbol: string; diff --git a/apps/insights/src/components/PriceFeed/feed-count-badge.tsx b/apps/insights/src/components/PriceFeed/feed-count-badge.tsx new file mode 100644 index 0000000000..da53ac7d49 --- /dev/null +++ b/apps/insights/src/components/PriceFeed/feed-count-badge.tsx @@ -0,0 +1,32 @@ +import { Badge } from "@pythnetwork/component-library/Badge"; +import { Suspense } from "react"; + +import { getFeed } from "./get-feed"; +import { Cluster } from "../../services/pyth"; +import { LiveValue } from "../LivePrices"; + +type Props = { + params: Promise<{ + slug: string; + }>; +}; + +export const FeedCountBadge = ({ params }: Props) => ( + + + +); + +const FeedCountBadgeImpl = async ({ params }: Props) => { + const { feed } = await getFeed(params); + return ( + + + + ); +}; diff --git a/apps/insights/src/components/PriceFeed/get-feed.tsx b/apps/insights/src/components/PriceFeed/get-feed.tsx new file mode 100644 index 0000000000..429c98f259 --- /dev/null +++ b/apps/insights/src/components/PriceFeed/get-feed.tsx @@ -0,0 +1,20 @@ +import { notFound } from "next/navigation"; + +import { Cluster, getFeeds } from "../../services/pyth"; + +export const getFeed = async (params: Promise<{ slug: string }>) => { + "use cache"; + + const [{ slug }, feeds] = await Promise.all([params, getPythnetFeeds()]); + const symbol = decodeURIComponent(slug); + return { + feeds, + feed: feeds.find((item) => item.symbol === symbol) ?? notFound(), + symbol, + } as const; +}; + +const getPythnetFeeds = async () => { + "use cache"; + return getFeeds(Cluster.Pythnet); +}; diff --git a/apps/insights/src/components/PriceFeed/header.module.scss b/apps/insights/src/components/PriceFeed/header.module.scss new file mode 100644 index 0000000000..90ebba35b9 --- /dev/null +++ b/apps/insights/src/components/PriceFeed/header.module.scss @@ -0,0 +1,59 @@ +@use "@pythnetwork/component-library/theme"; + +.header { + margin-bottom: theme.spacing(4); + display: flex; + flex-flow: column nowrap; + gap: theme.spacing(4); + + @include theme.max-width; + + @include theme.breakpoint("sm") { + margin-bottom: theme.spacing(6); + gap: theme.spacing(6); + } + + .headerRow { + display: flex; + flex-flow: column nowrap; + gap: theme.spacing(2); + justify-content: space-between; + + @include theme.breakpoint("sm") { + flex-flow: row nowrap; + align-items: center; + gap: unset; + } + } + + .rightGroup { + display: flex; + flex-flow: row nowrap; + align-items: stretch; + gap: theme.spacing(4); + + & > * { + flex: 1 1 0; + width: 0; + + @include theme.breakpoint("sm") { + flex: unset; + width: unset; + } + } + } + + .priceFeedSelect { + display: none; + + @include theme.breakpoint("sm") { + display: block; + } + } + + .priceFeedTag { + @include theme.breakpoint("sm") { + display: none; + } + } +} diff --git a/apps/insights/src/components/PriceFeed/header.tsx b/apps/insights/src/components/PriceFeed/header.tsx new file mode 100644 index 0000000000..5af002186b --- /dev/null +++ b/apps/insights/src/components/PriceFeed/header.tsx @@ -0,0 +1,272 @@ +import { ListDashes } from "@phosphor-icons/react/dist/ssr/ListDashes"; +import { Breadcrumbs } from "@pythnetwork/component-library/Breadcrumbs"; +import { Button } from "@pythnetwork/component-library/Button"; +import { Skeleton } from "@pythnetwork/component-library/Skeleton"; +import { StatCard } from "@pythnetwork/component-library/StatCard"; +import { Suspense } from "react"; + +import styles from "./header.module.scss"; +import { PriceFeedSelect } from "./price-feed-select"; +import { ReferenceData } from "./reference-data"; +import { Cluster } from "../../services/pyth"; +import { AssetClassBadge } from "../AssetClassBadge"; +import { Cards } from "../Cards"; +import { Explain } from "../Explain"; +import { FeedKey } from "../FeedKey"; +import { LivePrice, LiveConfidence, LiveLastUpdated } from "../LivePrices"; +import { + YesterdaysPricesProvider, + PriceFeedChangePercent, +} from "../PriceFeedChangePercent"; +import { PriceFeedIcon } from "../PriceFeedIcon"; +import { PriceFeedTag } from "../PriceFeedTag"; +import { PriceName } from "../PriceName"; +import { getFeed } from "./get-feed"; + +type Props = { + params: Promise<{ + slug: string; + }>; +}; + +export const PriceFeedHeader = ({ params }: Props) => ( + }> + + +); + +const ResolvedPriceFeedHeader = async ({ params }: Props) => ( + +); + +type PriceFeedHeaderImplProps = + | { isLoading: true } + | ({ + isLoading?: false | undefined; + } & Awaited>); + +const PriceFeedHeaderImpl = (props: PriceFeedHeaderImplProps) => ( +
+
+ + ) : ( + props.feed.product.display_symbol + ), + }, + ]} + /> + {props.isLoading ? ( + + ) : ( + {props.feed.product.asset_type} + )} +
+
+ item.symbol !== props.symbol) + .map((item) => ({ + symbol: item.symbol, + assetClass: item.product.asset_type, + description: item.product.description, + displaySymbol: item.product.display_symbol, + key: item.product.price_account, + icon: ( + + ), + })), + })} + > + + ), + })} + /> + + + ), + })} + /> +
+ {props.isLoading ? ( + + ) : ( + + )} + +
+
+ + + ) : ( + <> + Aggregated{" "} + + + ) + } + stat={ + props.isLoading ? ( + + ) : ( + + ) + } + /> + + ) : ( + + ) + } + 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. +

+ +
+ } + /> + + ) : ( + <> + 1-Day {" "} + Change + + ) + } + stat={ + props.isLoading ? ( + + ) : ( + + + + ) + } + /> + + ) : ( + + ) + } + /> +
+
+); diff --git a/apps/insights/src/components/PriceFeed/layout.module.scss b/apps/insights/src/components/PriceFeed/layout.module.scss index ad50baf973..a637dc0bf8 100644 --- a/apps/insights/src/components/PriceFeed/layout.module.scss +++ b/apps/insights/src/components/PriceFeed/layout.module.scss @@ -1,64 +1,6 @@ @use "@pythnetwork/component-library/theme"; .priceFeedLayout { - .header { - margin-bottom: theme.spacing(4); - display: flex; - flex-flow: column nowrap; - gap: theme.spacing(4); - - @include theme.max-width; - - @include theme.breakpoint("sm") { - margin-bottom: theme.spacing(6); - gap: theme.spacing(6); - } - - .headerRow { - display: flex; - flex-flow: column nowrap; - gap: theme.spacing(2); - justify-content: space-between; - - @include theme.breakpoint("sm") { - flex-flow: row nowrap; - align-items: center; - gap: unset; - } - } - - .rightGroup { - display: flex; - flex-flow: row nowrap; - align-items: stretch; - gap: theme.spacing(4); - - & > * { - flex: 1 1 0; - width: 0; - - @include theme.breakpoint("sm") { - flex: unset; - width: unset; - } - } - } - - .priceFeedSelect { - display: none; - - @include theme.breakpoint("sm") { - display: block; - } - } - - .priceFeedTag { - @include theme.breakpoint("sm") { - display: none; - } - } - } - .priceComponentsTabLabel { display: inline-flex; flex-flow: row nowrap; @@ -72,11 +14,3 @@ @include theme.max-width; } } - -.confidenceDescription { - margin: 0; - - b { - font-weight: theme.font-weight("semibold"); - } -} diff --git a/apps/insights/src/components/PriceFeed/layout.tsx b/apps/insights/src/components/PriceFeed/layout.tsx index f9d0613848..c39aa94f36 100644 --- a/apps/insights/src/components/PriceFeed/layout.tsx +++ b/apps/insights/src/components/PriceFeed/layout.tsx @@ -1,206 +1,38 @@ -import { ListDashes } from "@phosphor-icons/react/dist/ssr/ListDashes"; -import { Badge } from "@pythnetwork/component-library/Badge"; -import { Breadcrumbs } from "@pythnetwork/component-library/Breadcrumbs"; -import { Button } from "@pythnetwork/component-library/Button"; -import { Drawer, DrawerTrigger } from "@pythnetwork/component-library/Drawer"; -import { StatCard } from "@pythnetwork/component-library/StatCard"; -import { notFound } from "next/navigation"; import type { ReactNode } from "react"; 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, - LiveConfidence, - LiveLastUpdated, - LiveValue, -} from "../LivePrices"; -import { - YesterdaysPricesProvider, - PriceFeedChangePercent, -} from "../PriceFeedChangePercent"; -import { PriceFeedTag } from "../PriceFeedTag"; -import { PriceName } from "../PriceName"; import { TabPanel, TabRoot, Tabs } from "../Tabs"; type Props = { + header: ReactNode; + feedCountBadge: ReactNode; children: ReactNode; - params: Promise<{ - slug: string; - }>; }; -export const PriceFeedLayout = async ({ children, params }: Props) => { - const [{ slug }, feeds] = await Promise.all([ - params, - getFeeds(Cluster.Pythnet), - ]); - const symbol = decodeURIComponent(slug); - const feed = feeds.find((item) => item.symbol === symbol); - - return feed ? ( -
-
-
- -
- - {feed.product.asset_type.toUpperCase()} - -
-
-
- - - - -
- - - - - - - -
-
- - - Aggregated - - } - stat={ - - } - /> - - } - 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. -

- -
- } - /> - - 1-Day Change - - } - stat={ - - - - } - /> - - } - /> -
-
- - - Publishers - - - -
- ), - }, - ]} - /> - {children} - - - ) : ( - notFound() - ); -}; +export const PriceFeedLayout = ({ + children, + feedCountBadge, + header, +}: Props) => ( +
+ {header} + + + Publishers + {feedCountBadge} +
+ ), + }, + ]} + /> + {children} + + +); diff --git a/apps/insights/src/components/PriceFeed/price-feed-select.tsx b/apps/insights/src/components/PriceFeed/price-feed-select.tsx index c4d9d89e7d..5adb418ead 100644 --- a/apps/insights/src/components/PriceFeed/price-feed-select.tsx +++ b/apps/insights/src/components/PriceFeed/price-feed-select.tsx @@ -21,92 +21,154 @@ import { useMemo, useState } from "react"; import { useCollator, useFilter } from "react-aria"; import styles from "./price-feed-select.module.scss"; -import { usePriceFeeds } from "../../hooks/use-price-feeds"; -import { Cluster } from "../../services/pyth"; -import { AssetClassTag } from "../AssetClassTag"; +import { AssetClassBadge } from "../AssetClassBadge"; import { PriceFeedTag } from "../PriceFeedTag"; type Props = { className: string | undefined; children: ReactNode; +} & ( + | { isLoading: true } + | { + isLoading?: false | undefined; + feeds: { + symbol: string; + displaySymbol: string; + assetClass: string; + key: string; + description: string; + icon: ReactNode; + }[]; + } +); + +export const PriceFeedSelect = (props: Props) => + props.isLoading ? ( + + ) : ( + + ); + +type ResolvedPriceFeedSelect = { + className: string | undefined; + children: ReactNode; + feeds: { + symbol: string; + displaySymbol: string; + assetClass: string; + key: string; + description: string; + icon: ReactNode; + }[]; }; -export const PriceFeedSelect = ({ children, className }: Props) => { - const feeds = usePriceFeeds(); +const ResolvedPriceFeedSelect = ({ + feeds, + ...props +}: ResolvedPriceFeedSelect) => { const collator = useCollator(); const filter = useFilter({ sensitivity: "base", usage: "search" }); const [search, setSearch] = useState(""); const filteredFeeds = useMemo( () => search === "" - ? // This is inefficient but Safari doesn't support `Iterator.filter`, see - // https://bugs.webkit.org/show_bug.cgi?id=248650 - [...feeds.entries()] - : [...feeds.entries()].filter( - ([, { displaySymbol, assetClass, key }]) => + ? feeds + : feeds.filter( + ({ displaySymbol, assetClass, key }) => filter.contains(displaySymbol, search) || filter.contains(assetClass, search) || - filter.contains(key[Cluster.Pythnet], search), + filter.contains(key, search), ), [feeds, search, filter], ); const sortedFeeds = useMemo( () => - // eslint-disable-next-line unicorn/no-useless-spread - [ - ...filteredFeeds.map(([symbol, { displaySymbol }]) => ({ - id: symbol, - displaySymbol, - })), - ].toSorted((a, b) => collator.compare(a.displaySymbol, b.displaySymbol)), + filteredFeeds.toSorted((a, b) => + collator.compare(a.displaySymbol, b.displaySymbol), + ), [filteredFeeds, collator], ); + return ( - - - - + + - {({ id, displaySymbol }) => ( - - - - - )} - - - - - + + + + + {({ symbol, displaySymbol, description, icon, assetClass }) => ( + + + {assetClass} + + )} + + + + + } + {...props} + /> ); }; + +type PriceFeedSelectImplProps = { + className: string | undefined; + children: ReactNode; +} & ( + | { isLoading: true } + | { + isLoading?: false | undefined; + menu: ReactNode; + } +); + +const PriceFeedSelectImpl = ({ + children, + className, + ...props +}: PriceFeedSelectImplProps) => ( + +); diff --git a/apps/insights/src/components/PriceFeed/publishers-card.tsx b/apps/insights/src/components/PriceFeed/publishers-card.tsx index 7b1a8afa05..e359e3acce 100644 --- a/apps/insights/src/components/PriceFeed/publishers-card.tsx +++ b/apps/insights/src/components/PriceFeed/publishers-card.tsx @@ -2,158 +2,114 @@ import { useLogger } from "@pythnetwork/app-logger"; import { Switch } from "@pythnetwork/component-library/Switch"; -import { useQueryState, parseAsString, parseAsBoolean } from "nuqs"; -import type { ComponentProps } from "react"; +import { useQueryState, parseAsBoolean } from "nuqs"; import { Suspense, useCallback, useMemo } from "react"; -import { Cluster, ClusterToName } from "../../services/pyth"; -import { PriceComponentDrawer } from "../PriceComponentDrawer"; -import { - PriceComponentsCardContents, - ResolvedPriceComponentsCard, -} from "../PriceComponentsCard"; +import { Cluster } from "../../services/pyth"; +import type { PriceComponent } from "../PriceComponentsCard"; +import { PriceComponentsCard } from "../PriceComponentsCard"; +import { PublisherTag } from "../PublisherTag"; -type Publisher = ComponentProps< - typeof ResolvedPriceComponentsCard ->["priceComponents"][number] & - Pick, "rank"> & { - firstEvaluation?: Date | undefined; - }; +type PublishersCardProps = + | { isLoading: true } + | (ResolvedPublishersCardProps & { + isLoading?: false | undefined; + }); -type Props = Omit< - ComponentProps, - "onPriceComponentAction" | "priceComponents" -> & { - priceComponents: Publisher[]; +export const PublishersCard = (props: PublishersCardProps) => + props.isLoading ? ( + + ) : ( + + + + ); + +type ResolvedPublishersCardProps = { symbol: string; displaySymbol: string; assetClass: string; + publishers: Omit[]; + metricsTime?: Date | undefined; }; -export const PublishersCard = ({ - priceComponents, - symbol, - displaySymbol, - ...props -}: Props) => ( - }> - - -); - const ResolvedPublishersCard = ({ - priceComponents, - symbol, - displaySymbol, - assetClass, + publishers, ...props -}: Props) => { +}: ResolvedPublishersCardProps) => { const logger = useLogger(); - const { handleClose, selectedPublisher, updateSelectedPublisherKey } = - usePublisherDrawer(priceComponents); - const onPriceComponentAction = useCallback( - ({ publisherKey, cluster }: Publisher) => { - updateSelectedPublisherKey( - [ClusterToName[cluster], publisherKey].join(":"), - ); - }, - [updateSelectedPublisherKey], - ); + const [includeTestFeeds, setIncludeTestFeeds] = useQueryState( "includeTestFeeds", parseAsBoolean.withDefault(false), ); - const componentsFilteredByCluster = useMemo( - () => - includeTestFeeds - ? priceComponents - : priceComponents.filter( - (component) => component.cluster === Cluster.Pythnet, - ), - [includeTestFeeds, priceComponents], - ); + const updateIncludeTestFeeds = useCallback( (newValue: boolean) => { setIncludeTestFeeds(newValue).catch((error: unknown) => { - logger.error( - "Failed to update include test components query param", - error, - ); + logger.error("Failed to update show quality", error); }); }, [setIncludeTestFeeds, logger], ); + const publishersFilteredByCluster = useMemo( + () => + includeTestFeeds + ? publishers + : publishers.filter( + (component) => component.cluster === Cluster.Pythnet, + ), + [includeTestFeeds, publishers], + ); + return ( - <> - - Include test publishers - - } - {...props} - /> - {selectedPublisher && ( - - )} - + ); }; -const usePublisherDrawer = (publishers: Publisher[]) => { - const logger = useLogger(); - const [selectedPublisherKey, setSelectedPublisher] = useQueryState( - "publisher", - parseAsString.withDefault("").withOptions({ - history: "push", - }), - ); - const updateSelectedPublisherKey = useCallback( - (newPublisherKey: string) => { - setSelectedPublisher(newPublisherKey).catch((error: unknown) => { - logger.error("Failed to update selected publisher", error); - }); - }, - [setSelectedPublisher, logger], - ); - const selectedPublisher = useMemo(() => { - const [cluster, publisherKey] = selectedPublisherKey.split(":"); - return publishers.find( - (publisher) => - publisher.publisherKey === publisherKey && - ClusterToName[publisher.cluster] === cluster, - ); - }, [selectedPublisherKey, publishers]); - const handleClose = useCallback(() => { - updateSelectedPublisherKey(""); - }, [updateSelectedPublisherKey]); +type PublishersCardImplProps = + | { isLoading: true } + | (ResolvedPublishersCardProps & { + isLoading?: false | undefined; + includeTestFeeds: boolean; + updateIncludeTestFeeds: (newValue: boolean) => void; + }); - return { selectedPublisher, handleClose, updateSelectedPublisherKey }; -}; +const PublishersCardImpl = (props: PublishersCardImplProps) => ( + } + identifiesPublisher + toolbarExtra={ + + Include test publishers + + } + {...(props.isLoading + ? { isLoading: true } + : { + assetClass: props.assetClass, + metricsTime: props.metricsTime, + priceComponents: props.publishers.map((feed) => ({ + ...feed, + symbol: props.symbol, + displaySymbol: props.displaySymbol, + assetClass: props.assetClass, + })), + })} + /> +); diff --git a/apps/insights/src/components/PriceFeed/publishers.tsx b/apps/insights/src/components/PriceFeed/publishers.tsx index 8e8b5091d1..ba7f19f9aa 100644 --- a/apps/insights/src/components/PriceFeed/publishers.tsx +++ b/apps/insights/src/components/PriceFeed/publishers.tsx @@ -1,7 +1,6 @@ import { lookup as lookupPublisher } from "@pythnetwork/known-publishers"; import { notFound } from "next/navigation"; -import { PublishersCard } from "./publishers-card"; import { getRankingsBySymbol } from "../../services/clickhouse"; import { Cluster, @@ -12,6 +11,7 @@ import { import { getStatus } from "../../status"; import { PublisherIcon } from "../PublisherIcon"; import { PublisherTag } from "../PublisherTag"; +import { PublishersCard } from "./publishers-card"; type Props = { params: Promise<{ @@ -46,14 +46,11 @@ export const Publishers = async ({ params }: Props) => { notFound() ) : ( } symbol={symbol} displaySymbol={feed.product.display_symbol} assetClass={feed.product.asset_type} - priceComponents={publishers.map( + publishers={publishers.map( ({ ranking, publisher, status, cluster, knownPublisher }) => ({ id: `${publisher}-${ClusterToName[cluster]}`, feedKey: @@ -86,6 +83,8 @@ export const Publishers = async ({ params }: Props) => { ); }; +export const PublishersLoading = () => ; + const getPublishers = async (cluster: Cluster, symbol: string) => { const [publishers, rankings] = await Promise.all([ getPublishersForFeed(cluster, symbol), diff --git a/apps/insights/src/components/PriceFeed/reference-data.tsx b/apps/insights/src/components/PriceFeed/reference-data.tsx index a82e0017fb..38a0c3a3ab 100644 --- a/apps/insights/src/components/PriceFeed/reference-data.tsx +++ b/apps/insights/src/components/PriceFeed/reference-data.tsx @@ -6,7 +6,7 @@ import { useCollator } from "react-aria"; import styles from "./reference-data.module.scss"; import { Cluster } from "../../services/pyth"; -import { AssetClassTag } from "../AssetClassTag"; +import { AssetClassBadge } from "../AssetClassBadge"; import { LiveValue } from "../LivePrices"; type Props = { @@ -43,7 +43,7 @@ export const ReferenceData = ({ feed }: Props) => { () => [ ...Object.entries({ - "Asset Type": , + "Asset Type": {feed.assetClass}, Base: feed.base, Description: feed.description, Symbol: feed.symbol, diff --git a/apps/insights/src/components/PriceFeedIcon/index.tsx b/apps/insights/src/components/PriceFeedIcon/index.tsx index ac3b1e317b..cb2148b87c 100644 --- a/apps/insights/src/components/PriceFeedIcon/index.tsx +++ b/apps/insights/src/components/PriceFeedIcon/index.tsx @@ -21,7 +21,7 @@ type Props = Omit & export const PriceFeedIcon = ({ assetClass, symbol, ...props }: Props) => { if (assetClass === "Crypto") { - const firstPart = symbol.split("/")[0]; + const firstPart = symbol.split(".")[1]?.split("/")[0]; const Icon = firstPart ? (icons as SVGRecord)[firstPart] : undefined; return Icon ? ( diff --git a/apps/insights/src/components/PriceFeedTag/index.module.scss b/apps/insights/src/components/PriceFeedTag/index.module.scss index 0ca6b99f12..2feb84876c 100644 --- a/apps/insights/src/components/PriceFeedTag/index.module.scss +++ b/apps/insights/src/components/PriceFeedTag/index.module.scss @@ -57,13 +57,6 @@ } } - &[data-compact] { - .icon { - width: theme.spacing(6); - height: theme.spacing(6); - } - } - &[data-loading] { .icon { border-radius: theme.border-radius("full"); diff --git a/apps/insights/src/components/PriceFeedTag/index.tsx b/apps/insights/src/components/PriceFeedTag/index.tsx index 741d8bd3ed..7b4df0506e 100644 --- a/apps/insights/src/components/PriceFeedTag/index.tsx +++ b/apps/insights/src/components/PriceFeedTag/index.tsx @@ -1,110 +1,63 @@ -"use client"; - import { Skeleton } from "@pythnetwork/component-library/Skeleton"; import clsx from "clsx"; import type { ComponentProps, ReactNode } from "react"; import { Fragment } from "react"; import styles from "./index.module.scss"; -import { usePriceFeeds } from "../../hooks/use-price-feeds"; import { omitKeys } from "../../omit-keys"; -type OwnProps = { compact?: boolean | undefined } & ( - | { isLoading: true } - | { - isLoading?: false; - symbol: string; - } -); - -type Props = Omit, keyof OwnProps> & OwnProps; - -export const PriceFeedTag = (props: Props) => { - return props.isLoading ? ( - - ) : ( - - ); -}; - -const LoadedPriceFeedTag = ({ - symbol, - ...props -}: Props & { isLoading?: false }) => { - const feed = usePriceFeeds().get(symbol); - if (feed) { - const [firstPart, ...rest] = feed.displaySymbol.split("/"); - return ( - - ); - } else { - throw new NoSuchFeedError(symbol); - } -}; - -type OwnImplProps = { compact?: boolean | undefined } & ( +type OwnProps = | { isLoading: true } | { isLoading?: false; - feedName: [string, ...string[]]; - icon: ReactNode; + icon: ReactNode | undefined; + displaySymbol: string; description: string; - } -); + }; -type ImplProps = Omit, keyof OwnImplProps> & OwnImplProps; +type Props = Omit, keyof OwnProps> & OwnProps; -const PriceFeedTagImpl = ({ className, compact, ...props }: ImplProps) => { - return ( -
- {props.isLoading ? ( - - ) : ( -
{props.icon}
- )} -
-
- {props.isLoading ? ( - - ) : ( - <> - {props.feedName[0]} - {props.feedName.slice(1).map((part, i) => ( - - / - {part} - - ))} - - )} -
- {!compact && ( -
- {props.isLoading ? ( - - ) : ( - props.description.split("/")[0] - )} -
+export const PriceFeedTag = ({ className, ...props }: Props) => ( +
+ {props.isLoading ? ( + + ) : ( +
{props.icon}
+ )} +
+
+ {props.isLoading ? ( + + ) : ( + + )} +
+
+ {props.isLoading ? ( + + ) : ( + props.description.split("/")[0] )}
+
+); + +const FeedName = ({ displaySymbol }: { displaySymbol: string }) => { + const [firstPart, ...rest] = displaySymbol.split("/"); + return ( + <> + {firstPart} + {rest.map((part, i) => ( + + / + {part} + + ))} + ); }; - -class NoSuchFeedError extends Error { - constructor(symbol: string) { - super(`No feed exists named ${symbol}`); - this.name = "NoSuchFeedError"; - } -} diff --git a/apps/insights/src/components/PriceFeeds/asset-classes-drawer.tsx b/apps/insights/src/components/PriceFeeds/asset-class-table.tsx similarity index 59% rename from apps/insights/src/components/PriceFeeds/asset-classes-drawer.tsx rename to apps/insights/src/components/PriceFeeds/asset-class-table.tsx index 1beb6189cb..8f56a7a09e 100644 --- a/apps/insights/src/components/PriceFeeds/asset-classes-drawer.tsx +++ b/apps/insights/src/components/PriceFeeds/asset-class-table.tsx @@ -2,12 +2,8 @@ import { useLogger } from "@pythnetwork/app-logger"; import { Badge } from "@pythnetwork/component-library/Badge"; -import { - CLOSE_DURATION_IN_MS, - Drawer, - DrawerTrigger, -} from "@pythnetwork/component-library/Drawer"; import { Table } from "@pythnetwork/component-library/Table"; +import { useDrawer } from "@pythnetwork/component-library/useDrawer"; import { usePathname } from "next/navigation"; import { parseAsString, @@ -15,56 +11,15 @@ import { useQueryStates, createSerializer, } from "nuqs"; -import type { ReactNode } from "react"; import { useMemo } from "react"; import { useCollator } from "react-aria"; type Props = { numFeedsByAssetClass: Record; - children: ReactNode; -}; - -export const AssetClassesDrawer = ({ - numFeedsByAssetClass, - children, -}: Props) => { - const numAssetClasses = useMemo( - () => Object.keys(numFeedsByAssetClass).length, - [numFeedsByAssetClass], - ); - - return ( - - {children} - - Asset Classes - {numAssetClasses} - - } - > - {({ state }) => ( - - )} - - - ); -}; - -type AssetClassTableProps = { - numFeedsByAssetClass: Record; - state: { close: () => void }; }; -const AssetClassTable = ({ - numFeedsByAssetClass, - state, -}: AssetClassTableProps) => { +export const AssetClassTable = ({ numFeedsByAssetClass }: Props) => { + const drawer = useDrawer(); const logger = useLogger(); const collator = useCollator(); const pathname = usePathname(); @@ -85,12 +40,12 @@ const AssetClassTable = ({ id: assetClass, href: `${pathname}${serialize(newQuery)}`, onAction: () => { - state.close(); - setTimeout(() => { - setQuery(newQuery).catch((error: unknown) => { - logger.error("Failed to update query", error); - }); - }, CLOSE_DURATION_IN_MS); + drawer.close().catch((error: unknown) => { + logger.error(error); + }); + setQuery(newQuery).catch((error: unknown) => { + logger.error("Failed to update query", error); + }); }, data: { assetClass, @@ -101,7 +56,7 @@ const AssetClassTable = ({ [ numFeedsByAssetClass, collator, - state, + drawer, pathname, setQuery, serialize, diff --git a/apps/insights/src/components/PriceFeeds/coming-soon-list.tsx b/apps/insights/src/components/PriceFeeds/coming-soon-list.tsx index ea2efa3e51..f4e06a00e8 100644 --- a/apps/insights/src/components/PriceFeeds/coming-soon-list.tsx +++ b/apps/insights/src/components/PriceFeeds/coming-soon-list.tsx @@ -3,41 +3,30 @@ import { SearchInput } from "@pythnetwork/component-library/SearchInput"; import { Select } from "@pythnetwork/component-library/Select"; import { Table } from "@pythnetwork/component-library/Table"; +import type { ReactNode } from "react"; import { useMemo, useState } from "react"; import { useCollator, useFilter } from "react-aria"; import styles from "./coming-soon-list.module.scss"; -import { usePriceFeeds } from "../../hooks/use-price-feeds"; -import { AssetClassTag } from "../AssetClassTag"; +import { AssetClassBadge } from "../AssetClassBadge"; import { NoResults } from "../NoResults"; import { PriceFeedTag } from "../PriceFeedTag"; type Props = { - comingSoonSymbols: string[]; + comingSoonFeeds: { + symbol: string; + assetClass: string; + displaySymbol: string; + description: string; + icon: ReactNode; + }[]; }; -export const ComingSoonList = ({ comingSoonSymbols }: Props) => { +export const ComingSoonList = ({ comingSoonFeeds }: Props) => { const [search, setSearch] = useState(""); const [assetClass, setAssetClass] = useState(""); const collator = useCollator(); const filter = useFilter({ sensitivity: "base", usage: "search" }); - const feeds = usePriceFeeds(); - const comingSoonFeeds = useMemo( - () => - comingSoonSymbols.map((symbol) => { - const feed = feeds.get(symbol); - if (feed) { - return { - symbol, - assetClass: feed.assetClass, - displaySymbol: feed.displaySymbol, - }; - } else { - throw new NoSuchFeedError(symbol); - } - }), - [feeds, comingSoonSymbols], - ); const assetClasses = useMemo( () => [ @@ -70,16 +59,25 @@ export const ComingSoonList = ({ comingSoonSymbols }: Props) => { ); const rows = useMemo( () => - filteredFeeds.map(({ symbol }) => ({ - id: symbol, - href: `/price-feeds/${encodeURIComponent(symbol)}`, - data: { - priceFeedName: , - assetClass: , - }, - })), + filteredFeeds.map( + ({ symbol, assetClass, description, displaySymbol, icon }) => ({ + id: symbol, + href: `/price-feeds/${encodeURIComponent(symbol)}`, + data: { + priceFeedName: ( + + ), + assetClass: {assetClass}, + }, + }), + ), [filteredFeeds], ); + return (
@@ -139,10 +137,3 @@ export const ComingSoonList = ({ comingSoonSymbols }: Props) => {
); }; - -class NoSuchFeedError extends Error { - constructor(symbol: string) { - super(`No feed exists named ${symbol}`); - this.name = "NoSuchFeedError"; - } -} diff --git a/apps/insights/src/components/PriceFeeds/index.tsx b/apps/insights/src/components/PriceFeeds/index.tsx index a4dece550e..5ec78e2142 100644 --- a/apps/insights/src/components/PriceFeeds/index.tsx +++ b/apps/insights/src/components/PriceFeeds/index.tsx @@ -7,7 +7,6 @@ import { Badge } from "@pythnetwork/component-library/Badge"; import { Button } from "@pythnetwork/component-library/Button"; import type { Props as CardProps } from "@pythnetwork/component-library/Card"; import { Card } from "@pythnetwork/component-library/Card"; -import { Drawer, DrawerTrigger } from "@pythnetwork/component-library/Drawer"; import { StatCard } from "@pythnetwork/component-library/StatCard"; import { TabList } from "@pythnetwork/component-library/TabList"; import { @@ -16,7 +15,7 @@ import { } from "@pythnetwork/component-library/unstyled/Tabs"; import type { ElementType } from "react"; -import { AssetClassesDrawer } from "./asset-classes-drawer"; +import { AssetClassTable } from "./asset-class-table"; import { ComingSoonList } from "./coming-soon-list"; import styles from "./index.module.scss"; import { PriceFeedsCard } from "./price-feeds-card"; @@ -29,6 +28,7 @@ import { YesterdaysPricesProvider, PriceFeedChangePercent, } from "../PriceFeedChangePercent"; +import { PriceFeedIcon } from "../PriceFeedIcon"; import { PriceFeedTag } from "../PriceFeedTag"; const PRICE_FEEDS_ANCHOR = "priceFeeds"; @@ -50,6 +50,7 @@ export const PriceFeeds = async () => { priceFeeds.activeFeeds, priceFeedsStaticConfig.featuredFeeds, ); + const numAssetClasses = Object.keys(numFeedsByAssetClass).length; return (
@@ -73,13 +74,23 @@ export const PriceFeeds = async () => { target="_blank" corner={} /> - - } - /> - + } + drawer={{ + fill: true, + title: ( + <> + Asset Classes + {numAssetClasses} + + ), + contents: ( + + ), + }} + />
{ ({ + key: feed.product.price_account, symbol: feed.symbol, exponent: feed.price.exponent, numQuoters: feed.price.numQuoters, + assetClass: feed.product.asset_type, + description: feed.product.description, + displaySymbol: feed.product.display_symbol, + icon: ( + + ), }))} />
@@ -108,9 +129,19 @@ export const PriceFeeds = async () => { ({ + key: feed.product.price_account, symbol: feed.symbol, exponent: feed.price.exponent, numQuoters: feed.price.numQuoters, + assetClass: feed.product.asset_type, + description: feed.product.description, + displaySymbol: feed.product.display_symbol, + icon: ( + + ), }))} /> @@ -129,7 +160,7 @@ export const PriceFeeds = async () => { type FeaturedFeedsProps = { featuredFeeds: FeaturedFeed[]; featuredComingSoon: FeaturedFeed[]; - allComingSoon: { symbol: string }[]; + allComingSoon: FeaturedFeed[]; }; const FeaturedFeeds = ({ @@ -159,25 +190,38 @@ const FeaturedFeeds = ({ feeds={featuredComingSoon} toolbarAlwaysOnTop toolbar={ - - - Coming Soon {allComingSoon.length} - } - > - symbol)} - /> - - + ), + contents: ( + ({ + assetClass: feed.product.asset_type, + description: feed.product.description, + displaySymbol: feed.product.display_symbol, + symbol: feed.symbol, + icon: ( + + ), + }))} + /> + ), + }} + > + Show all + } /> @@ -197,6 +241,7 @@ type FeaturedFeed = { display_symbol: string; price_account: string; description: string; + asset_type: string; }; }; @@ -214,7 +259,16 @@ const FeaturedFeedsCard = ({ href={`/price-feeds/${encodeURIComponent(feed.symbol)}`} >
- + + } + /> {showPrices && (
( @@ -59,32 +62,12 @@ const ResolvedPriceFeedsCard = ({ priceFeeds, ...props }: Props) => { "assetClass", parseAsString.withDefault(""), ); - const feeds = usePriceFeeds(); - const priceFeedsWithContextInfo = useMemo( - () => - priceFeeds.map((feed) => { - const contextFeed = feeds.get(feed.symbol); - if (contextFeed) { - return { - ...feed, - assetClass: contextFeed.assetClass, - displaySymbol: contextFeed.displaySymbol, - key: contextFeed.key[Cluster.Pythnet], - }; - } else { - throw new NoSuchFeedError(feed.symbol); - } - }), - [feeds, priceFeeds], - ); const feedsFilteredByAssetClass = useMemo( () => assetClass - ? priceFeedsWithContextInfo.filter( - (feed) => feed.assetClass === assetClass, - ) - : priceFeedsWithContextInfo, - [assetClass, priceFeedsWithContextInfo], + ? priceFeeds.filter((feed) => feed.assetClass === assetClass) + : priceFeeds, + [assetClass, priceFeeds], ); const { search, @@ -115,7 +98,16 @@ const ResolvedPriceFeedsCard = ({ priceFeeds, ...props }: Props) => { const rows = useMemo( () => paginatedItems.map( - ({ displaySymbol, symbol, exponent, numQuoters, key }) => ({ + ({ + displaySymbol, + symbol, + exponent, + numQuoters, + key, + description, + icon, + assetClass, + }) => ({ id: symbol, href: `/price-feeds/${encodeURIComponent(symbol)}`, textValue: displaySymbol, @@ -140,8 +132,14 @@ const ResolvedPriceFeedsCard = ({ priceFeeds, ...props }: Props) => { confidenceInterval: ( ), - priceFeedName: , - assetClass: , + priceFeedName: ( + + ), + assetClass: {assetClass}, priceFeedId: ( ), @@ -163,10 +161,10 @@ const ResolvedPriceFeedsCard = ({ priceFeeds, ...props }: Props) => { const assetClasses = useMemo( () => - [ - ...new Set(priceFeedsWithContextInfo.map((feed) => feed.assetClass)), - ].sort((a, b) => collator.compare(a, b)), - [priceFeedsWithContextInfo, collator], + [...new Set(priceFeeds.map((feed) => feed.assetClass))].sort((a, b) => + collator.compare(a, b), + ), + [priceFeeds, collator], ); return ( @@ -292,7 +290,7 @@ const PriceFeedsCardContents = ({ id, ...props }: PriceFeedsCardContents) => ( } + headerLoadingSkeleton={} fields={[ { id: "assetClass", name: "Asset Class" }, { id: "priceFeedId", name: "Price Feed ID" }, @@ -327,7 +325,7 @@ const PriceFeedsCardContents = ({ id, ...props }: PriceFeedsCardContents) => ( name: "PRICE FEED", isRowHeader: true, alignment: "left", - loadingSkeleton: , + loadingSkeleton: , allowsSorting: true, }, { @@ -392,10 +390,3 @@ const PriceFeedsCardContents = ({ id, ...props }: PriceFeedsCardContents) => ( /> ); - -class NoSuchFeedError extends Error { - constructor(symbol: string) { - super(`No feed exists named ${symbol}`); - this.name = "NoSuchFeedError"; - } -} diff --git a/apps/insights/src/components/Publisher/get-price-feeds.tsx b/apps/insights/src/components/Publisher/get-price-feeds.tsx index ee87dab147..3530ebf88f 100644 --- a/apps/insights/src/components/Publisher/get-price-feeds.tsx +++ b/apps/insights/src/components/Publisher/get-price-feeds.tsx @@ -14,9 +14,6 @@ export const getPriceFeeds = async (cluster: Cluster, key: string) => { ranking.symbol === feed.symbol && ranking.cluster === ClusterToName[cluster], ); - //if (!ranking) { - // console.log(`No ranking for feed: ${feed.symbol} in cluster ${ClusterToName[cluster]}`); - //} return { ranking, feed, diff --git a/apps/insights/src/components/Publisher/layout.tsx b/apps/insights/src/components/Publisher/layout.tsx index 685d5f33c2..c1a1d1cf0e 100644 --- a/apps/insights/src/components/Publisher/layout.tsx +++ b/apps/insights/src/components/Publisher/layout.tsx @@ -5,25 +5,22 @@ import { ShieldChevron } from "@phosphor-icons/react/dist/ssr/ShieldChevron"; import { Badge } from "@pythnetwork/component-library/Badge"; import { Breadcrumbs } from "@pythnetwork/component-library/Breadcrumbs"; import { Button } from "@pythnetwork/component-library/Button"; -import { DrawerTrigger, Drawer } from "@pythnetwork/component-library/Drawer"; import { InfoBox } from "@pythnetwork/component-library/InfoBox"; import { Link } from "@pythnetwork/component-library/Link"; +import { Skeleton } from "@pythnetwork/component-library/Skeleton"; import { StatCard } from "@pythnetwork/component-library/StatCard"; import { lookup } from "@pythnetwork/known-publishers"; import { notFound } from "next/navigation"; import type { ReactNode } from "react"; +import { Suspense } from "react"; -import { getPriceFeeds } from "./get-price-feeds"; -import styles from "./layout.module.scss"; -import { OisApyHistory } from "./ois-apy-history"; -import { PriceFeedDrawerProvider } from "./price-feed-drawer-provider"; import { getPublisherRankingHistory, getPublisherAverageScoreHistory, getPublishers, } from "../../services/clickhouse"; import { getPublisherCaps } from "../../services/hermes"; -import { Cluster, ClusterToName, parseCluster } from "../../services/pyth"; +import { ClusterToName, parseCluster, Cluster } from "../../services/pyth"; import { getPublisherPoolData } from "../../services/staking"; import { Cards } from "../Cards"; import { ChangePercent } from "../ChangePercent"; @@ -35,16 +32,19 @@ import { ExplainActive, ExplainInactive, } from "../Explanations"; -import { FormattedDate } from "../FormattedDate"; import { FormattedNumber } from "../FormattedNumber"; -import { FormattedTokens } from "../FormattedTokens"; -import { Meter } from "../Meter"; import { PublisherIcon } from "../PublisherIcon"; import { PublisherKey } from "../PublisherKey"; import { PublisherTag } from "../PublisherTag"; +import { getPriceFeeds } from "./get-price-feeds"; +import styles from "./layout.module.scss"; +import { FormattedDate } from "../FormattedDate"; +import { FormattedTokens } from "../FormattedTokens"; +import { Meter } from "../Meter"; import { SemicircleMeter } from "../SemicircleMeter"; import { TabPanel, TabRoot, Tabs } from "../Tabs"; import { TokenIcon } from "../TokenIcon"; +import { OisApyHistory } from "./ois-apy-history"; type Props = { children: ReactNode; @@ -54,48 +54,15 @@ type Props = { }>; }; -export const PublishersLayout = async ({ children, params }: Props) => { +export const PublisherLayout = async ({ children, params }: Props) => { const { cluster, key } = await params; const parsedCluster = parseCluster(cluster); if (parsedCluster === undefined) { notFound(); - } - - const [ - rankingHistory, - averageScoreHistory, - oisStats, - priceFeeds, - publishers, - ] = await Promise.all([ - getPublisherRankingHistory(parsedCluster, key), - getPublisherAverageScoreHistory(parsedCluster, key), - getOisStats(key), - getPriceFeeds(parsedCluster, key), - getPublishers(parsedCluster), - ]); - - const currentRanking = rankingHistory.at(-1); - const previousRanking = rankingHistory.at(-2); - - const currentAverageScore = averageScoreHistory.at(-1); - const previousAverageScore = averageScoreHistory.at(-2); - const knownPublisher = lookup(key); - const publisher = publishers.find((publisher) => publisher.key === key); - - return publisher && currentRanking && currentAverageScore ? ( - ({ - symbol: feed.symbol, - score: ranking?.final_score, - rank: ranking?.final_rank, - firstEvaluation: ranking?.first_ranking_time, - status, - }))} - > + } else { + const knownPublisher = lookup(key); + return (
@@ -118,283 +85,40 @@ export const PublishersLayout = async ({ children, params }: Props) => { })} /> - -

- Each Publisher receives a Ranking which is - derived from the number of price feeds the Publisher{" "} - is actively publishing. -

- - } - data={rankingHistory.map(({ timestamp, rank }) => ({ - x: timestamp, - y: rank, - displayX: ( - - - - ), - }))} - stat={currentRanking.rank} - {...(previousRanking && { - miniStat: ( - - {Math.abs(currentRanking.rank - previousRanking.rank)} - - ), - })} - /> - } - data={averageScoreHistory.map(({ time, averageScore }) => ({ - x: time, - y: averageScore, - displayX: ( - - - - ), - displayY: ( - - ), - }))} - stat={ - - } - {...(previousAverageScore && { - miniStat: ( - - ), - })} - /> - - Active Feeds - - - } - header2={ - <> - - Inactive Feeds - - } - stat1={ - - {publisher.activeFeeds} - - } - stat2={ - - {publisher.inactiveFeeds} - - } - miniStat1={ - <> - - % - - } - miniStat2={ - <> - - % - - } - > - - + }> + + + }> + + + }> + + {parsedCluster === Cluster.Pythnet && ( - - oisStats.maxPoolSize - ? "" - : undefined - } - > - - % - - } - corner={} - > - - - - - - - } - endLabel={ - - - - - - - } - /> - - - - - - } - > - - -
OIS Pool
-
- - -
OIS Pool
-
- - - - - } - /> - - - - - } - /> - - } - header="Oracle Integrity Staking (OIS)" - > - OIS allows anyone to help secure Pyth and protect DeFi. - Through decentralized staking rewards and slashing, OIS - incentivizes Pyth publishers to maintain high-quality data - contributions. PYTH holders can stake to publishers to - further reinforce oracle security. Rewards are - programmatically distributed to high quality publishers and - the stakers supporting them to strengthen oracle integrity. - -
-
+ }> + + )}
Price Feeds - {priceFeeds.length} + + +
), @@ -404,9 +128,195 @@ export const PublishersLayout = async ({ children, params }: Props) => { {children}
- - ) : ( - notFound() + ); + } +}; + +const NumFeeds = async ({ + cluster, + publisherKey, +}: { + cluster: Cluster; + publisherKey: string; +}) => { + const feeds = await getPriceFeeds(cluster, publisherKey); + return feeds.length; +}; + +const RankingCard = async ({ + cluster, + publisherKey, +}: { + cluster: Cluster; + publisherKey: string; +}) => { + const rankingHistory = await getPublisherRankingHistory( + cluster, + publisherKey, + ); + return ; +}; + +type RankingCardImplProps = + | { + isLoading: true; + } + | { + isLoading?: false | undefined; + rankingHistory: { + timestamp: Date; + rank: number; + }[]; + }; + +const RankingCardImpl = (props: RankingCardImplProps) => ( + +

+ Each Publisher receives a Ranking which is derived from + the number of price feeds the Publisher is actively publishing. +

+ + } + data={ + props.isLoading + ? [] + : props.rankingHistory.map(({ timestamp, rank }) => ({ + x: timestamp, + y: rank, + displayX: ( + + + + ), + })) + } + stat={ + props.isLoading ? ( + + ) : ( + props.rankingHistory.at(-1)?.rank + ) + } + miniStat={ + props.isLoading ? ( + + ) : ( + + ) + } + /> +); + +const RankingChange = ({ + rankingHistory, +}: { + rankingHistory: { rank: number }[]; +}) => { + const current = rankingHistory.at(-1)?.rank; + const prev = rankingHistory.at(-2)?.rank; + + // eslint-disable-next-line unicorn/no-null + return current === undefined || prev === undefined ? null : ( + + {Math.abs(current - prev)} + + ); +}; + +const ScoreCard = async ({ + cluster, + publisherKey, +}: { + cluster: Cluster; + publisherKey: string; +}) => { + const averageScoreHistory = await getPublisherAverageScoreHistory( + cluster, + publisherKey, + ); + return ; +}; + +type ScoreCardImplProps = + | { + isLoading: true; + } + | { + isLoading?: false | undefined; + averageScoreHistory: { + time: Date; + averageScore: number; + }[]; + }; + +const ScoreCardImpl = (props: ScoreCardImplProps) => ( + } + data={ + props.isLoading + ? [] + : props.averageScoreHistory.map(({ time, averageScore }) => ({ + x: time, + y: averageScore, + displayX: ( + + + + ), + displayY: ( + + ), + })) + } + stat={ + props.isLoading ? ( + + ) : ( + + ) + } + miniStat={ + props.isLoading ? ( + + ) : ( + + ) + } + /> +); + +const CurrentAverageScore = ({ + averageScoreHistory, +}: { + averageScoreHistory: { averageScore: number }[]; +}) => { + const currentAverageScore = averageScoreHistory.at(-1)?.averageScore; + + // eslint-disable-next-line unicorn/no-null + return currentAverageScore === undefined ? null : ( + + ); +}; + +const ScoreChange = ({ + averageScoreHistory, +}: { + averageScoreHistory: { averageScore: number }[]; +}) => { + const current = averageScoreHistory.at(-1)?.averageScore; + const prev = averageScoreHistory.at(-2)?.averageScore; + + // eslint-disable-next-line unicorn/no-null + return current === undefined || prev === undefined ? null : ( + ); }; @@ -420,24 +330,308 @@ const getChangeDirection = (previousValue: number, currentValue: number) => { } }; -const getOisStats = async (key: string) => { +const ActiveFeedsCard = async ({ + cluster, + publisherKey, +}: { + cluster: Cluster; + publisherKey: string; +}) => { + const [publishers, priceFeeds] = await Promise.all([ + getPublishers(cluster), + getPriceFeeds(cluster, publisherKey), + ]); + const publisher = publishers.find( + (publisher) => publisher.key === publisherKey, + ); + + return publisher ? ( + + ) : ( + notFound() + ); +}; + +type ActiveFeedsCardImplProps = + | { isLoading: true } + | { + isLoading?: false | undefined; + cluster: Cluster; + publisherKey: string; + activeFeeds: number; + inactiveFeeds: number; + allFeeds: number; + }; + +const ActiveFeedsCardImpl = (props: ActiveFeedsCardImplProps) => ( + + Active Feeds + + + } + header2={ + <> + + Inactive Feeds + + } + stat1={ + props.isLoading ? ( + + ) : ( + + {props.activeFeeds} + + ) + } + stat2={ + props.isLoading ? ( + + ) : ( + + {props.inactiveFeeds} + + ) + } + miniStat1={ + props.isLoading ? ( + + ) : ( + <> + + % + + ) + } + miniStat2={ + props.isLoading ? ( + + ) : ( + <> + + % + + ) + } + > + {!props.isLoading && ( + + )} + +); + +const OisPoolCard = async ({ publisherKey }: { publisherKey: string }) => { const [publisherPoolData, publisherCaps] = await Promise.all([ getPublisherPoolData(), getPublisherCaps(), ]); const publisher = publisherPoolData.find( - (publisher) => publisher.pubkey === key, + (publisher) => publisher.pubkey === publisherKey, ); - return { - apyHistory: publisher?.apyHistory, - poolUtilization: - (publisher?.totalDelegation ?? 0n) + - (publisher?.totalDelegationDelta ?? 0n), - maxPoolSize: - publisherCaps.parsed?.[0]?.publisher_stake_caps.find( - ({ publisher }) => publisher === key, - )?.cap ?? 0, - }; + return ( + publisher === publisherKey, + )?.cap ?? 0 + } + /> + ); }; + +type OisPoolCardImplProps = + | { isLoading: true } + | { + isLoading?: false | undefined; + apyHistory: { date: Date; apy: number }[]; + poolUtilization: bigint; + maxPoolSize: number; + }; + +const OisPoolCardImpl = (props: OisPoolCardImplProps) => ( + + + + + ), + contents: ( + <> + {!props.isLoading && ( + <> + + +
OIS Pool
+
+ + +
OIS Pool
+
+ + )} + + + {props.isLoading ? ( + + ) : ( + + )} + + } + /> + + + + {props.isLoading ? ( + + ) : ( + + )} + + } + /> + + } + header="Oracle Integrity Staking (OIS)" + > + OIS allows anyone to help secure Pyth and protect DeFi. Through + decentralized staking rewards and slashing, OIS incentivizes Pyth + publishers to maintain high-quality data contributions. PYTH holders + can stake to publishers to further reinforce oracle security. + Rewards are programmatically distributed to high quality publishers + and the stakers supporting them to strengthen oracle integrity. + + + ), + }} + stat={ + props.isLoading ? ( + + ) : ( + props.maxPoolSize ? "" : undefined + } + > + + % + + ) + } + corner={} + > + + + + {props.isLoading ? ( + + ) : ( + + )} + + + } + endLabel={ + + + + {props.isLoading ? ( + + ) : ( + + )} + + + } + /> + +); diff --git a/apps/insights/src/components/Publisher/performance.module.scss b/apps/insights/src/components/Publisher/performance.module.scss index b5148cda61..244646fc29 100644 --- a/apps/insights/src/components/Publisher/performance.module.scss +++ b/apps/insights/src/components/Publisher/performance.module.scss @@ -16,7 +16,7 @@ .publishersRankingCard { .publishersRankingList { - @include theme.breakpoint("sm") { + @include theme.breakpoint("lg") { display: none; } @@ -28,7 +28,7 @@ .publishersRankingTable { display: none; - @include theme.breakpoint("sm") { + @include theme.breakpoint("lg") { display: unset; } } diff --git a/apps/insights/src/components/Publisher/performance.tsx b/apps/insights/src/components/Publisher/performance.tsx index 748805e9d0..814b863b55 100644 --- a/apps/insights/src/components/Publisher/performance.tsx +++ b/apps/insights/src/components/Publisher/performance.tsx @@ -2,18 +2,18 @@ import { Broadcast } from "@phosphor-icons/react/dist/ssr/Broadcast"; import { Confetti } from "@phosphor-icons/react/dist/ssr/Confetti"; import { Network } from "@phosphor-icons/react/dist/ssr/Network"; import { SmileySad } from "@phosphor-icons/react/dist/ssr/SmileySad"; -import { Badge } from "@pythnetwork/component-library/Badge"; import { Card } from "@pythnetwork/component-library/Card"; 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 type { ReactNode, ComponentProps } from "react"; import { getPriceFeeds } from "./get-price-feeds"; import styles from "./performance.module.scss"; import { TopFeedsTable } from "./top-feeds-table"; import { getPublishers } from "../../services/clickhouse"; +import type { Cluster } from "../../services/pyth"; import { ClusterToName, parseCluster } from "../../services/pyth"; import { Status } from "../../status"; import { EntityList } from "../EntityList"; @@ -24,6 +24,7 @@ import { } from "../Explanations"; import type { Variant as NoResultsVariant } from "../NoResults"; import { NoResults } from "../NoResults"; +import { PriceFeedIcon } from "../PriceFeedIcon"; import { PriceFeedTag } from "../PriceFeedTag"; import { PublisherIcon } from "../PublisherIcon"; import { PublisherTag } from "../PublisherTag"; @@ -59,6 +60,7 @@ export const Performance = async ({ params }: Props) => { const knownPublisher = lookup(publisher.key); return { id: publisher.key, + prefetch: false, nameAsString: knownPublisher?.name ?? publisher.key, data: { ranking: ( @@ -70,6 +72,7 @@ export const Performance = async ({ params }: Props) => { {publisher.activeFeeds} @@ -78,6 +81,7 @@ export const Performance = async ({ params }: Props) => { {publisher.inactiveFeeds} @@ -119,101 +123,165 @@ export const Performance = async ({ params }: Props) => { return rows === undefined ? ( notFound() ) : ( -
- } - title="Publishers Ranking" - className={styles.publishersRankingCard ?? ""} - > - ({ - ...row, - textValue: row.nameAsString, - header: row.data.name, - }))} - /> - - ACTIVE FEEDS - - - ), - alignment: "center", - width: 30, - }, - { - id: "inactiveFeeds", - name: ( - <> - INACTIVE FEEDS - - - ), - alignment: "center", - width: 30, - }, - { - id: "averageScore", - name: ( - <> - AVERAGE SCORE - - - ), - alignment: "right", - width: PUBLISHER_SCORE_WIDTH, - }, - ]} - 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} - /> - + ); }; +export const PerformanceLoading = () => ; + +type PerformanceImplProps = + | { isLoading: true } + | { + isLoading?: false; + publisherKey: string; + cluster: Cluster; + publishers: (NonNullable< + ComponentProps< + typeof Table< + | "ranking" + | "averageScore" + | "activeFeeds" + | "inactiveFeeds" + | "name" + > + >["rows"] + >[number] & { + prefetch: boolean; + nameAsString: string; + })[]; + highPerformingFeeds: ReturnType; + lowPerformingFeeds: ReturnType; + averageScoreTime?: Date | undefined; + }; + +const PerformanceImpl = (props: PerformanceImplProps) => ( +
+ } + title="Publishers Ranking" + className={styles.publishersRankingCard ?? ""} + > + } + fields={[ + { id: "ranking", name: "Ranking" }, + { id: "averageScore", name: "Average Score" }, + { id: "activeFeeds", name: "Active Feeds" }, + { id: "inactiveFeeds", name: "Inactive Feeds" }, + ]} + {...(props.isLoading + ? { isLoading: true } + : { + rows: props.publishers.map((publisher) => ({ + ...publisher, + textValue: publisher.nameAsString, + header: publisher.data.name, + })), + })} + /> +
, + }, + { + id: "activeFeeds", + name: ( + <> + ACTIVE FEEDS + + + ), + alignment: "center", + width: 30, + }, + { + id: "inactiveFeeds", + name: ( + <> + INACTIVE FEEDS + + + ), + alignment: "center", + width: 30, + }, + { + id: "averageScore", + name: ( + <> + AVERAGE SCORE + + + ), + alignment: "right", + width: PUBLISHER_SCORE_WIDTH, + }, + ]} + {...(props.isLoading + ? { isLoading: true } + : { + rows: props.publishers, + })} + /> + + } + emptyHeader="Oh no!" + emptyBody="This publisher has no high performing feeds" + emptyVariant="error" + {...(props.isLoading + ? { isLoading: true } + : { + publisherKey: props.publisherKey, + cluster: props.cluster, + feeds: props.highPerformingFeeds, + })} + /> + } + emptyHeader="Looking good!" + emptyBody="This publisher has no low performing feeds" + emptyVariant="success" + {...(props.isLoading + ? { isLoading: true } + : { + publisherKey: props.publisherKey, + cluster: props.cluster, + feeds: props.lowPerformingFeeds, + })} + /> + +); + const getFeedRows = ( priceFeeds: (Omit< Awaited>, @@ -227,20 +295,23 @@ const getFeedRows = ( priceFeeds .filter((feed) => feed.status === Status.Active) .slice(0, 20) - .map(({ feed, ranking }) => ({ - id: feed.symbol, - textValue: feed.symbol, - data: { - asset: , - assetClass: ( - - {feed.product.asset_type.toUpperCase()} - - ), - score: ( - - ), - }, + .map(({ feed, ranking, status }) => ({ + key: feed.product.price_account, + symbol: feed.symbol, + displaySymbol: feed.product.display_symbol, + description: feed.product.description, + assetClass: feed.product.asset_type, + score: ranking.final_score, + rank: ranking.final_rank, + status, + firstEvaluation: ranking.first_ranking_time, + icon: ( + + ), + href: `/price-feeds/${encodeURIComponent(feed.symbol)}`, })); const sliceAround = ( @@ -271,8 +342,15 @@ type TopFeedsCardProps = { emptyHeader: string; emptyBody: string; emptyVariant: NoResultsVariant; - feeds: ReturnType; -}; +} & ( + | { isLoading: true } + | { + isLoading?: false | undefined; + publisherKey: string; + cluster: Cluster; + feeds: ReturnType; + } +); const TopFeedsCard = ({ title, @@ -280,22 +358,29 @@ const TopFeedsCard = ({ emptyHeader, emptyBody, emptyVariant, - feeds, + ...props }: TopFeedsCardProps) => ( } title={`${title} Feeds`}> - {feeds.length === 0 ? ( + {props.isLoading || props.feeds.length > 0 ? ( + } + {...(props.isLoading + ? { isLoading: true } + : { + feeds: props.feeds, + publisherKey: props.publisherKey, + cluster: props.cluster, + })} + /> + ) : ( - ) : ( - )} ); diff --git a/apps/insights/src/components/Publisher/price-feed-drawer-provider.tsx b/apps/insights/src/components/Publisher/price-feed-drawer-provider.tsx deleted file mode 100644 index 911740cd11..0000000000 --- a/apps/insights/src/components/Publisher/price-feed-drawer-provider.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; - -import { useLogger } from "@pythnetwork/app-logger"; -import { parseAsString, useQueryState } from "nuqs"; -import type { ComponentProps } from "react"; -import { Suspense, createContext, useMemo, useCallback, use } from "react"; - -import { usePriceFeeds } from "../../hooks/use-price-feeds"; -import type { Cluster } from "../../services/pyth"; -import type { Status } from "../../status"; -import { PriceComponentDrawer } from "../PriceComponentDrawer"; -import { PriceFeedTag } from "../PriceFeedTag"; - -const PriceFeedDrawerContext = createContext< - ((symbol: string) => void) | undefined ->(undefined); - -type PriceFeedDrawerProviderProps = Omit< - ComponentProps, - "value" -> & { - publisherKey: string; - cluster: Cluster; - priceFeeds: PriceFeed[]; -}; - -type PriceFeed = { - symbol: string; - score: number | undefined; - rank: number | undefined; - status: Status; - firstEvaluation: Date | undefined; -}; - -export const PriceFeedDrawerProvider = ( - props: PriceFeedDrawerProviderProps, -) => ( - - - -); - -const PriceFeedDrawerProviderImpl = ({ - publisherKey, - priceFeeds, - children, - cluster, -}: PriceFeedDrawerProviderProps) => { - const contextPriceFeeds = usePriceFeeds(); - const logger = useLogger(); - const [selectedSymbol, setSelectedSymbol] = useQueryState( - "price-feed", - parseAsString.withDefault("").withOptions({ - history: "push", - }), - ); - const updateSelectedSymbol = useCallback( - (newSymbol: string) => { - setSelectedSymbol(newSymbol).catch((error: unknown) => { - logger.error("Failed to update selected symbol", error); - }); - }, - [setSelectedSymbol, logger], - ); - const selectedFeed = useMemo(() => { - if (selectedSymbol === "") { - return; - } else { - const feed = priceFeeds.find((feed) => feed.symbol === selectedSymbol); - const contextFeed = contextPriceFeeds.get(selectedSymbol); - - return feed === undefined || contextFeed === undefined - ? undefined - : { - ...feed, - ...contextFeed, - feedKey: contextFeed.key[cluster], - }; - } - }, [selectedSymbol, priceFeeds, contextPriceFeeds, cluster]); - const handleClose = useCallback(() => { - updateSelectedSymbol(""); - }, [updateSelectedSymbol]); - const feedHref = useMemo( - () => `/price-feeds/${encodeURIComponent(selectedFeed?.symbol ?? "")}`, - [selectedFeed], - ); - - return ( - - {children} - {selectedFeed && ( - } - cluster={cluster} - assetClass={selectedFeed.assetClass} - /> - )} - - ); -}; - -export const useSelectPriceFeed = () => use(PriceFeedDrawerContext); diff --git a/apps/insights/src/components/Publisher/price-feeds-card.tsx b/apps/insights/src/components/Publisher/price-feeds-card.tsx deleted file mode 100644 index 8c14f46a5c..0000000000 --- a/apps/insights/src/components/Publisher/price-feeds-card.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client"; - -import type { ComponentProps } from "react"; -import { useCallback } from "react"; - -import { useSelectPriceFeed } from "./price-feed-drawer-provider"; -import { usePriceFeeds } from "../../hooks/use-price-feeds"; -import type { Cluster } from "../../services/pyth"; -import { AssetClassTag } from "../AssetClassTag"; -import { PriceComponentsCard } from "../PriceComponentsCard"; -import { PriceFeedTag } from "../PriceFeedTag"; - -type Props = Omit< - ComponentProps, - "onPriceComponentAction" | "priceComponents" -> & { - publisherKey: string; - cluster: Cluster; - priceFeeds: (Pick< - ComponentProps["priceComponents"][number], - "score" | "uptimeScore" | "deviationScore" | "stalledScore" | "status" - > & { - symbol: string; - })[]; -}; - -export const PriceFeedsCard = ({ - priceFeeds, - publisherKey, - cluster, - ...props -}: Props) => { - const feeds = usePriceFeeds(); - const selectPriceFeed = useSelectPriceFeed(); - const onPriceComponentAction = useCallback( - ({ symbol }: { symbol: string }) => selectPriceFeed?.(symbol), - [selectPriceFeed], - ); - return ( - { - const contextFeed = feeds.get(feed.symbol); - if (contextFeed) { - return { - id: contextFeed.key[cluster], - feedKey: contextFeed.key[cluster], - symbol: feed.symbol, - score: feed.score, - uptimeScore: feed.uptimeScore, - deviationScore: feed.deviationScore, - stalledScore: feed.stalledScore, - cluster, - status: feed.status, - publisherKey, - name: , - nameAsString: contextFeed.displaySymbol, - assetClass: , - }; - } else { - throw new NoSuchFeedError(feed.symbol); - } - })} - {...props} - /> - ); -}; - -class NoSuchFeedError extends Error { - constructor(symbol: string) { - super(`No feed exists named ${symbol}`); - this.name = "NoSuchFeedError"; - } -} diff --git a/apps/insights/src/components/Publisher/price-feeds.tsx b/apps/insights/src/components/Publisher/price-feeds.tsx index 76a91af420..c1dfb0313e 100644 --- a/apps/insights/src/components/Publisher/price-feeds.tsx +++ b/apps/insights/src/components/Publisher/price-feeds.tsx @@ -1,8 +1,12 @@ import { notFound } from "next/navigation"; import { getPriceFeeds } from "./get-price-feeds"; -import { PriceFeedsCard } from "./price-feeds-card"; +import type { Cluster } from "../../services/pyth"; import { parseCluster } from "../../services/pyth"; +import { AssetClassBadge } from "../AssetClassBadge"; +import type { PriceComponent } from "../PriceComponentsCard"; +import { PriceComponentsCard } from "../PriceComponentsCard"; +import { PriceFeedIcon } from "../PriceFeedIcon"; import { PriceFeedTag } from "../PriceFeedTag"; type Props = { @@ -19,26 +23,85 @@ export const PriceFeeds = async ({ params }: Props) => { if (parsedCluster === undefined) { notFound(); } + const feeds = await getPriceFeeds(parsedCluster, key); const metricsTime = feeds.find((feed) => feed.ranking !== undefined)?.ranking ?.time; return ( } publisherKey={key} cluster={parsedCluster} priceFeeds={feeds.map(({ ranking, feed, status }) => ({ symbol: feed.symbol, + name: ( + + } + /> + ), score: ranking?.final_score, + rank: ranking?.final_rank, uptimeScore: ranking?.uptime_score, deviationScore: ranking?.deviation_score, stalledScore: ranking?.stalled_score, status, + feedKey: feed.product.price_account, + nameAsString: feed.product.display_symbol, + id: feed.product.price_account, + assetClass: feed.product.asset_type, + displaySymbol: feed.product.display_symbol, + firstEvaluation: ranking?.first_ranking_time, }))} /> ); }; + +export const PriceFeedsLoading = () => ; + +type PriceFeedsCardProps = + | { isLoading: true } + | { + isLoading?: false | undefined; + publisherKey: string; + cluster: Cluster; + priceFeeds: Omit[]; + metricsTime?: Date | undefined; + }; + +const PriceFeedsCard = (props: PriceFeedsCardProps) => ( + } + extraColumns={[ + { + id: "assetClassBadge", + name: "ASSET CLASS", + alignment: "left", + allowsSorting: true, + }, + ]} + nameWidth={90} + {...(props.isLoading + ? { isLoading: true } + : { + metricsTime: props.metricsTime, + priceComponents: props.priceFeeds.map((feed) => ({ + ...feed, + cluster: props.cluster, + publisherKey: props.publisherKey, + assetClassBadge: ( + {feed.assetClass} + ), + })), + })} + /> +); diff --git a/apps/insights/src/components/Publisher/top-feeds-table.tsx b/apps/insights/src/components/Publisher/top-feeds-table.tsx index 186b2ade05..1231e3ec1e 100644 --- a/apps/insights/src/components/Publisher/top-feeds-table.tsx +++ b/apps/insights/src/components/Publisher/top-feeds-table.tsx @@ -2,76 +2,159 @@ import type { RowConfig } from "@pythnetwork/component-library/Table"; import { Table } from "@pythnetwork/component-library/Table"; +import type { ReactNode } from "react"; import { useMemo } from "react"; -import { useSelectPriceFeed } from "./price-feed-drawer-provider"; import styles from "./top-feeds-table.module.scss"; +import type { Cluster } from "../../services/pyth"; +import type { Status } from "../../status"; +import { AssetClassBadge } from "../AssetClassBadge"; import { EntityList } from "../EntityList"; +import { usePriceComponentDrawer } from "../PriceComponentDrawer"; +import { PriceFeedTag } from "../PriceFeedTag"; +import { Score } from "../Score"; -type Props = { - publisherScoreWidth: number; - rows: (RowConfig<"score" | "asset" | "assetClass"> & { textValue: string })[]; - label: string; -}; +type Props = + | LoadingTopFeedsTableImplProps + | (ResolvedTopFeedsTableProps & { isLoading?: false | undefined }); -export const TopFeedsTable = ({ publisherScoreWidth, rows, label }: Props) => { - const selectPriceFeed = useSelectPriceFeed(); +export const TopFeedsTable = (props: Props) => + props.isLoading ? ( + + ) : ( + + ); - const rowsWithAction = useMemo( +type ResolvedTopFeedsTableProps = BaseTopFeedsTableImplProps & { + publisherKey: string; + cluster: Cluster; + feeds: { + key: string; + symbol: string; + displaySymbol: string; + description: string; + assetClass: string; + score: number; + rank: number; + status: Status; + firstEvaluation: Date; + icon: ReactNode; + href: string; + }[]; +}; + +const ResolvedTopFeedsTable = ({ + cluster, + feeds, + publisherKey, + ...props +}: ResolvedTopFeedsTableProps) => { + const drawerComponents = useMemo( () => - rows.map((row) => ({ - ...row, - ...(selectPriceFeed && { - onAction: () => { - selectPriceFeed(row.id.toString()); - }, - }), + feeds.map((feed) => ({ + name: ( + + ), + publisherKey, + feedKey: feed.key, + cluster, + ...feed, })), - [selectPriceFeed, rows], + [feeds, cluster, publisherKey], ); - return ( - <> - ({ - ...row, - textValue: row.textValue, - header: row.data.asset, - }))} - /> -
- + const { selectComponent } = usePriceComponentDrawer({ + components: drawerComponents, + }); + + const rows = useMemo( + () => + drawerComponents.map((feed) => ({ + id: feed.symbol, + textValue: feed.symbol, + header: feed.name, + data: { + asset: feed.name, + assetClass: {feed.assetClass}, + score: , + }, + onAction: () => { + selectComponent(feed); + }, + })), + [drawerComponents, props.publisherScoreWidth, selectComponent], ); + + return ; +}; + +type BaseTopFeedsTableImplProps = { + publisherScoreWidth: number; + label: string; + nameLoadingSkeleton: ReactNode; +}; +type LoadingTopFeedsTableImplProps = BaseTopFeedsTableImplProps & { + isLoading: true; }; +type LoadedTopFeedsTableImplProps = BaseTopFeedsTableImplProps & { + isLoading?: false | undefined; + rows: (RowConfig<"score" | "asset" | "assetClass"> & { + textValue: string; + header: ReactNode; + })[]; +}; +type TopFeedsTableImplProps = + | LoadingTopFeedsTableImplProps + | LoadedTopFeedsTableImplProps; + +const TopFeedsTableImpl = ({ + publisherScoreWidth, + label, + nameLoadingSkeleton, + ...props +}: TopFeedsTableImplProps) => ( + <> + +
+ +); diff --git a/apps/insights/src/components/Publishers/publishers-card.tsx b/apps/insights/src/components/Publishers/publishers-card.tsx index 9383556e15..102f67ce4e 100644 --- a/apps/insights/src/components/Publishers/publishers-card.tsx +++ b/apps/insights/src/components/Publishers/publishers-card.tsx @@ -142,6 +142,7 @@ const ResolvedPublishersCard = ({ id, href: `/publishers/${cluster}/${id}`, textValue: publisher.name ?? id, + prefetch: false, data: { ranking: {ranking}, name: ( @@ -158,6 +159,7 @@ const ResolvedPublishersCard = ({ {activeFeeds} diff --git a/apps/insights/src/components/Root/footer.tsx b/apps/insights/src/components/Root/footer.tsx index 7f9325edd6..c98cf2e162 100644 --- a/apps/insights/src/components/Root/footer.tsx +++ b/apps/insights/src/components/Root/footer.tsx @@ -1,6 +1,5 @@ import type { Props as ButtonProps } from "@pythnetwork/component-library/Button"; import { Button } from "@pythnetwork/component-library/Button"; -import { DrawerTrigger } from "@pythnetwork/component-library/Drawer"; import { Link } from "@pythnetwork/component-library/Link"; import type { ComponentProps, ElementType } from "react"; @@ -19,10 +18,7 @@ export const Footer = () => (
- - Help - - + Help Documentation diff --git a/apps/insights/src/components/Root/header.tsx b/apps/insights/src/components/Root/header.tsx index fcaef4b109..1c21eac69b 100644 --- a/apps/insights/src/components/Root/header.tsx +++ b/apps/insights/src/components/Root/header.tsx @@ -1,6 +1,5 @@ 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"; @@ -31,18 +30,16 @@ export const Header = ({ className, tabs, ...props }: Props) => (
- - - - + { - const publishers = await Promise.all([ - getPublishersForSearchDialog(Cluster.Pythnet), - getPublishersForSearchDialog(Cluster.PythtestConformance), - ]); - +export const Root = ({ children }: Props) => { return ( - +
@@ -58,12 +52,29 @@ export const Root = async ({ children }: Props) => {
- + ); }; +const SearchButtonProvider = async ({ children }: { children: ReactNode }) => { + const [publishers, feeds] = await Promise.all([ + Promise.all([ + getPublishersForSearchDialog(Cluster.Pythnet), + getPublishersForSearchDialog(Cluster.PythtestConformance), + ]), + getFeedsForSearchDialog(Cluster.Pythnet), + ]); + + return ( + + {children} + + ); +}; + const getPublishersForSearchDialog = async (cluster: Cluster) => { + "use cache"; const publishers = await getPublishers(cluster); return publishers.map((publisher) => { const knownPublisher = lookupPublisher(publisher.key); @@ -80,37 +91,20 @@ const getPublishersForSearchDialog = async (cluster: Cluster) => { }); }; -const PriceFeedsProvider = async ({ children }: { children: ReactNode }) => { - const [pythnetFeeds, pythtestConformanceFeeds] = await Promise.all([ - getFeeds(Cluster.Pythnet), - getFeeds(Cluster.PythtestConformance), - ]); +const getFeedsForSearchDialog = async (cluster: Cluster) => { + "use cache"; + const feeds = await getFeeds(cluster); - const feedMap = new Map( - pythnetFeeds.map((feed) => [ - feed.symbol, - { - displaySymbol: feed.product.display_symbol, - icon: ( - - ), - description: feed.product.description, - key: { - [Cluster.Pythnet]: feed.product.price_account, - [Cluster.PythtestConformance]: - pythtestConformanceFeeds.find( - (conformanceFeed) => conformanceFeed.symbol === feed.symbol, - )?.product.price_account ?? "", - }, - assetClass: feed.product.asset_type, - }, - ]), - ); - - return ( - {children} - ); + return feeds.map((feed) => ({ + symbol: feed.symbol, + displaySymbol: feed.product.display_symbol, + assetClass: feed.product.asset_type, + description: feed.product.description, + icon: ( + + ), + })); }; diff --git a/apps/insights/src/components/Root/mobile-menu.tsx b/apps/insights/src/components/Root/mobile-menu.tsx index c9958aa242..0a12e4d543 100644 --- a/apps/insights/src/components/Root/mobile-menu.tsx +++ b/apps/insights/src/components/Root/mobile-menu.tsx @@ -1,10 +1,6 @@ -"use client"; - 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"; import styles from "./mobile-menu.module.scss"; import { SupportDrawer } from "./support-drawer"; @@ -14,65 +10,48 @@ type Props = { className?: string | undefined; }; -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]); +export const MobileMenu = ({ className }: Props) => ( + +); - return ( - <> - - - -
-
- - -
-
- Theme - -
-
-
-
- - - ); -}; +const MobileMenuContents = () => ( +
+
+ + +
+
+ Theme + +
+
+); diff --git a/apps/insights/src/components/Root/search-dialog.module.scss b/apps/insights/src/components/Root/search-button.module.scss similarity index 88% rename from apps/insights/src/components/Root/search-dialog.module.scss rename to apps/insights/src/components/Root/search-button.module.scss index 90c69506c5..af54faa385 100644 --- a/apps/insights/src/components/Root/search-dialog.module.scss +++ b/apps/insights/src/components/Root/search-button.module.scss @@ -1,26 +1,5 @@ @use "@pythnetwork/component-library/theme"; -.modalOverlay { - position: fixed; - inset: 0; - background: rgba(from black r g b / 30%); - z-index: 1; - - .searchMenu { - position: relative; - top: theme.spacing(32); - margin: 0 auto; - outline: none; - background: theme.color("background", "secondary"); - 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; @@ -101,7 +80,7 @@ .listbox { outline: none; - overflow: auto; + overflow-y: scroll; flex-grow: 1; .item { diff --git a/apps/insights/src/components/Root/search-button.tsx b/apps/insights/src/components/Root/search-button.tsx index 5ba7964f79..c93784996e 100644 --- a/apps/insights/src/components/Root/search-button.tsx +++ b/apps/insights/src/components/Root/search-button.tsx @@ -1,28 +1,140 @@ "use client"; import { MagnifyingGlass } from "@phosphor-icons/react/dist/ssr/MagnifyingGlass"; +import { XCircle } from "@phosphor-icons/react/dist/ssr/XCircle"; +import { useLogger } from "@pythnetwork/app-logger"; +import { Badge } from "@pythnetwork/component-library/Badge"; +import type { Props as ButtonProps } from "@pythnetwork/component-library/Button"; import { Button } from "@pythnetwork/component-library/Button"; +import { SearchInput } from "@pythnetwork/component-library/SearchInput"; +import { SingleToggleGroup } from "@pythnetwork/component-library/SingleToggleGroup"; import { Skeleton } from "@pythnetwork/component-library/Skeleton"; -import type { ComponentProps } from "react"; -import { useMemo } from "react"; -import { useIsSSR } from "react-aria"; +import { + Virtualizer, + ListLayout, +} from "@pythnetwork/component-library/Virtualizer"; +import type { Button as UnstyledButton } from "@pythnetwork/component-library/unstyled/Button"; +import { + ListBox, + ListBoxItem, +} from "@pythnetwork/component-library/unstyled/ListBox"; +import { useDrawer } from "@pythnetwork/component-library/useDrawer"; +import type { ReactNode, ComponentProps } from "react"; +import { + useMemo, + useCallback, + useEffect, + useState, + createContext, + use, +} from "react"; +import { useIsSSR, useCollator, useFilter } from "react-aria"; -import { useToggleSearchDialog } from "./search-dialog"; +import styles from "./search-button.module.scss"; +import { Cluster, ClusterToName } from "../../services/pyth"; +import { AssetClassBadge } from "../AssetClassBadge"; +import { NoResults } from "../NoResults"; +import { PriceFeedTag } from "../PriceFeedTag"; +import { PublisherTag } from "../PublisherTag"; +import { Score } from "../Score"; -type Props = ComponentProps; +const INPUTS = new Set(["input", "select", "button", "textarea"]); -export const SearchButton = (props: Props) => { - const toggleSearchDialog = useToggleSearchDialog(); +const SearchButtonContext = createContext void)>(undefined); - return ( - +
+
+ + { + setSearch(""); + }} + /> + } + > + {(result) => ( + +
+ {result.type === ResultType.PriceFeed ? ( + + ) : ( + + )} +
+
+
Type
+
+ + {result.type === ResultType.PriceFeed + ? "PRICE FEED" + : "PUBLISHER"} + +
+
+
+ {result.type === ResultType.PriceFeed ? ( + <> +
Asset Class
+
+ + {result.assetClass} + +
+ + ) : ( + <> +
Average Score
+
+ +
+ + )} +
+
+
+
+
+ + {result.type === ResultType.PriceFeed + ? "PRICE FEED" + : "PUBLISHER"} + +
+ {result.type === ResultType.PriceFeed ? ( + <> + + {result.assetClass} + + ) : ( + <> + + + + )} +
+
+ )} +
+
+
+
+ ); +}; + +enum ResultType { + PriceFeed, + Publisher, +} diff --git a/apps/insights/src/components/Root/search-dialog.tsx b/apps/insights/src/components/Root/search-dialog.tsx deleted file mode 100644 index c750d17c10..0000000000 --- a/apps/insights/src/components/Root/search-dialog.tsx +++ /dev/null @@ -1,468 +0,0 @@ -"use client"; - -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"; -import { - Virtualizer, - ListLayout, -} from "@pythnetwork/component-library/Virtualizer"; -import { - ListBox, - ListBoxItem, -} from "@pythnetwork/component-library/unstyled/ListBox"; -import { useMediaQuery } from "@react-hookz/web"; -import { useRouter } from "next/navigation"; -import type { ReactNode, ComponentProps } from "react"; -import { - useState, - useCallback, - useEffect, - createContext, - use, - useMemo, -} from "react"; -import { RouterProvider, useCollator, useFilter } from "react-aria"; - -import styles from "./search-dialog.module.scss"; -import { usePriceFeeds } from "../../hooks/use-price-feeds"; -import { Cluster, ClusterToName } from "../../services/pyth"; -import { AssetClassTag } from "../AssetClassTag"; -import { NoResults } from "../NoResults"; -import { PriceFeedTag } from "../PriceFeedTag"; -import { PublisherTag } from "../PublisherTag"; -import { Score } from "../Score"; - -const CLOSE_DURATION_IN_SECONDS = 0.1; -const CLOSE_DURATION_IN_MS = CLOSE_DURATION_IN_SECONDS * 1000; - -const INPUTS = new Set(["input", "select", "button", "textarea"]); - -const SearchDialogOpenContext = createContext< - ReturnType | undefined ->(undefined); - -type Props = { - children: ReactNode; - publishers: ({ - publisherKey: string; - averageScore: number; - cluster: Cluster; - } & ( - | { name: string; icon: ReactNode } - | { name?: undefined; icon?: undefined } - ))[]; -}; - -export const SearchDialogProvider = ({ children, publishers }: Props) => { - const searchDialogState = useSearchDialogStateContext(); - const [search, setSearch] = useState(""); - const [type, setType] = useState(""); - const collator = useCollator(); - const filter = useFilter({ sensitivity: "base", usage: "search" }); - const feeds = usePriceFeeds(); - - const close = useCallback( - () => - new Promise((resolve) => { - searchDialogState.close(); - setTimeout(() => { - setSearch(""); - setType(""); - resolve(); - }, CLOSE_DURATION_IN_MS); - }), - [searchDialogState, setSearch, setType], - ); - - const handleOpenChange = useCallback( - (isOpen: boolean) => { - if (!isOpen) { - close().catch(() => { - /* no-op since this actually can't fail */ - }); - } - }, - [close], - ); - - const router = useRouter(); - const handleOpenItem = useCallback( - (href: string) => { - close() - .then(() => { - router.push(href); - }) - .catch(() => { - /* no-op since this actually can't fail */ - }); - }, - [close, router], - ); - - const results = useMemo( - () => - [ - ...(type === ResultType.Publisher - ? [] - : // This is inefficient but Safari doesn't support `Iterator.filter`, - // see https://bugs.webkit.org/show_bug.cgi?id=248650 - [...feeds.entries()] - .filter(([, { displaySymbol }]) => - filter.contains(displaySymbol, search), - ) - .map(([symbol, { assetClass, displaySymbol }]) => ({ - type: ResultType.PriceFeed as const, - id: symbol, - assetClass, - displaySymbol, - }))), - ...(type === ResultType.PriceFeed - ? [] - : publishers - .filter( - (publisher) => - filter.contains(publisher.publisherKey, search) || - (publisher.name && filter.contains(publisher.name, search)), - ) - .map((publisher) => ({ - type: ResultType.Publisher as const, - id: [ - ClusterToName[publisher.cluster], - publisher.publisherKey, - ].join(":"), - ...publisher, - }))), - ].sort((a, b) => - collator.compare( - a.type === ResultType.PriceFeed - ? a.displaySymbol - : (a.name ?? a.publisherKey), - b.type === ResultType.PriceFeed - ? b.displaySymbol - : (b.name ?? b.publisherKey), - ), - ), - [feeds, publishers, collator, filter, search, type], - ); - - return ( - <> - - {children} - - -
-
-
- - -
- -
-
- - - { - 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, -} - -const useSearchDialogStateContext = () => { - const [isOpen, setIsOpen] = useState(false); - const toggleIsOpen = useCallback(() => { - setIsOpen((value) => !value); - }, [setIsOpen]); - const close = useCallback(() => { - setIsOpen(false); - }, [setIsOpen]); - const open = useCallback(() => { - setIsOpen(true); - }, [setIsOpen]); - - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { - const activeElement = document.activeElement; - const tagName = activeElement?.tagName.toLowerCase(); - const isEditing = - !tagName || - INPUTS.has(tagName) || - (activeElement !== null && - "isContentEditable" in activeElement && - activeElement.isContentEditable); - const isSlash = event.key === "/"; - // Meta key for mac, ctrl key for non-mac - const isCtrlK = event.key === "k" && (event.metaKey || event.ctrlKey); - - if (!isEditing && (isSlash || isCtrlK)) { - event.preventDefault(); - toggleIsOpen(); - } - }, - [toggleIsOpen], - ); - - useEffect(() => { - globalThis.addEventListener("keydown", handleKeyDown); - return () => { - globalThis.removeEventListener("keydown", handleKeyDown); - }; - }, [handleKeyDown]); - - return { - isOpen, - setIsOpen, - toggleIsOpen, - open, - close, - }; -}; - -const useSearchDialogState = () => { - const value = use(SearchDialogOpenContext); - if (value) { - return value; - } else { - throw new NotInitializedError(); - } -}; - -export const useToggleSearchDialog = () => useSearchDialogState().toggleIsOpen; - -class NotInitializedError extends Error { - constructor() { - super("This component must be contained within a "); - this.name = "NotInitializedError"; - } -} diff --git a/apps/insights/src/components/Root/support-drawer.tsx b/apps/insights/src/components/Root/support-drawer.tsx index 433b9e3425..c9a2bfe426 100644 --- a/apps/insights/src/components/Root/support-drawer.tsx +++ b/apps/insights/src/components/Root/support-drawer.tsx @@ -1,3 +1,5 @@ +"use client"; + import { BookOpenText } from "@phosphor-icons/react/dist/ssr/BookOpenText"; import { CaretRight } from "@phosphor-icons/react/dist/ssr/CaretRight"; import { Code } from "@phosphor-icons/react/dist/ssr/Code"; @@ -7,85 +9,12 @@ import { Plug } from "@phosphor-icons/react/dist/ssr/Plug"; import { ShieldChevron } from "@phosphor-icons/react/dist/ssr/ShieldChevron"; import type { Props as CardProps } from "@pythnetwork/component-library/Card"; import { Card } from "@pythnetwork/component-library/Card"; -import { Drawer } from "@pythnetwork/component-library/Drawer"; import type { Link as UnstyledLink } from "@pythnetwork/component-library/unstyled/Link"; -import type { ComponentProps, ReactNode } from "react"; +import type { ReactNode } from "react"; import { socialLinks } from "./social-links"; import styles from "./support-drawer.module.scss"; -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", - href: "https://docs.pyth.network/home/pyth-token#staking-pyth-for-governance", - }, - ]} - /> - ({ - href, - target: "_blank", - title: name, - description: href, - icon: , - }))} - /> - -); - type LinkListProps = { title: ReactNode; links: (Omit< @@ -115,3 +44,77 @@ const LinkList = ({ title, links }: LinkListProps) => ( ); + +export const SupportDrawer = { + title: "Support", + bodyClassName: styles.supportDrawer, + contents: ( + <> + , + 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, + target: "_blank", + title: name, + description: href, + icon: , + }))} + /> + + ), +}; diff --git a/apps/insights/src/components/Root/tabs.tsx b/apps/insights/src/components/Root/tabs.tsx index 21778c2ad0..6159503c2a 100644 --- a/apps/insights/src/components/Root/tabs.tsx +++ b/apps/insights/src/components/Root/tabs.tsx @@ -8,9 +8,6 @@ import { import { useSelectedLayoutSegment, usePathname } from "next/navigation"; import type { ComponentProps } from "react"; -import type { VariantArg } from "../LayoutTransition"; -import { LayoutTransition } from "../LayoutTransition"; - export const TabRoot = ( props: Omit, "selectedKey">, ) => { @@ -27,46 +24,10 @@ export const MainNavTabs = ( return ; }; -export const TabPanel = ({ - children, - ...props -}: Omit, "id">) => { +export const TabPanel = ( + props: Omit, "id">, +) => { const tabId = useSelectedLayoutSegment() ?? ""; - return ( - - {(args) => ( - ({ - opacity: 0, - x: isMovingLeft(custom) ? "-2%" : "2%", - }), - exit: (custom) => ({ - opacity: 0, - x: isMovingLeft(custom) ? "2%" : "-2%", - transition: { - x: { type: "spring", bounce: 0 }, - }, - }), - }} - initial="initial" - animate={{ - opacity: 1, - x: 0, - transition: { - x: { type: "spring", bounce: 0 }, - }, - }} - exit="exit" - > - {typeof children === "function" ? children(args) : children} - - )} - - ); + return ; }; - -const isMovingLeft = ({ segment, prevSegment }: VariantArg): boolean => - segment === null || - (segment === "publishers" && prevSegment === "price-feeds"); diff --git a/apps/insights/src/components/Tabs/index.tsx b/apps/insights/src/components/Tabs/index.tsx index 6ab5994f9f..415cfe3add 100644 --- a/apps/insights/src/components/Tabs/index.tsx +++ b/apps/insights/src/components/Tabs/index.tsx @@ -9,8 +9,6 @@ import { useSelectedLayoutSegment, usePathname } from "next/navigation"; import type { ComponentProps } from "react"; import { useMemo } from "react"; -import { LayoutTransition } from "../LayoutTransition"; - export const TabRoot = ( props: Omit, "selectedKey">, ) => { @@ -20,7 +18,7 @@ export const TabRoot = ( }; type TabsProps = Omit, "pathname" | "items"> & { - prefix: string; + prefix?: string; items: (Omit< ComponentProps["items"], "href" | "id" @@ -31,55 +29,31 @@ type TabsProps = Omit, "pathname" | "items"> & { export const Tabs = ({ prefix, items, ...props }: TabsProps) => { const pathname = usePathname(); + const segment = useSelectedLayoutSegment(); + const finalPrefix = useMemo( + () => + (prefix ?? segment === null) + ? pathname + : pathname.replace(new RegExp(`/${segment}$`), ""), + [prefix, pathname, segment], + ); const mappedItems = useMemo( () => items.map((item) => ({ ...item, - id: item.segment ?? "", - href: item.segment ? `${prefix}/${item.segment}` : prefix, + id: item.id ?? item.segment ?? "", + href: item.segment ? `${finalPrefix}/${item.segment}` : finalPrefix, })), - [items, prefix], + [items, finalPrefix], ); - return ; + return ; }; -export const TabPanel = ({ - children, - ...props -}: Omit, "id">) => { +export const TabPanel = ( + props: Omit, "id">, +) => { const tabId = useSelectedLayoutSegment() ?? ""; - return ( - - {(args) => ( - ({ - opacity: 0, - x: segment === null ? "-2%" : "2%", - }), - exit: ({ segment }) => ({ - opacity: 0, - x: segment === null ? "2%" : "-2%", - transition: { - x: { type: "spring", bounce: 0 }, - }, - }), - }} - initial="initial" - animate={{ - opacity: 1, - x: 0, - transition: { - x: { type: "spring", bounce: 0 }, - }, - }} - exit="exit" - > - {typeof children === "function" ? children(args) : children} - - )} - - ); + return ; }; diff --git a/apps/insights/src/components/ZoomLayoutTransition/index.tsx b/apps/insights/src/components/ZoomLayoutTransition/index.tsx deleted file mode 100644 index 16dd3cb534..0000000000 --- a/apps/insights/src/components/ZoomLayoutTransition/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; - -import type { VariantArg } from "../LayoutTransition"; -import { LayoutTransition } from "../LayoutTransition"; - -type Props = { - children: ReactNode; -}; - -export const ZoomLayoutTransition = ({ children }: Props) => ( - ({ - opacity: 0, - scale: getInitialScale(custom), - }), - exit: (custom) => ({ - opacity: 0, - scale: getExitScale(custom), - transition: { - scale: { type: "spring", bounce: 0 }, - }, - }), - }} - initial="initial" - animate={{ - opacity: 1, - scale: 1, - transition: { - scale: { type: "spring", bounce: 0 }, - }, - }} - style={{ transformOrigin: "top" }} - exit="exit" - > - {children} - -); - -const getInitialScale = ({ segment, prevSegment }: VariantArg) => { - if (segment === null) { - return 1.04; - } else if (prevSegment === null) { - return 0.96; - } else { - return 1; - } -}; - -const getExitScale = ({ segment, prevSegment }: VariantArg) => { - if (segment === null) { - return 0.96; - } else if (prevSegment === null) { - return 1.04; - } else { - return 1; - } -}; diff --git a/apps/insights/src/hooks/use-price-feeds.tsx b/apps/insights/src/hooks/use-price-feeds.tsx deleted file mode 100644 index 144e309459..0000000000 --- a/apps/insights/src/hooks/use-price-feeds.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import type { ReactNode, ComponentProps } from "react"; -import { createContext, use } from "react"; - -import type { Cluster } from "../services/pyth"; - -const PriceFeedsContext = createContext(undefined); - -export const PriceFeedsProvider = ( - props: ComponentProps, -) => ; - -export const usePriceFeeds = () => { - const value = use(PriceFeedsContext); - if (value) { - return value; - } else { - throw new PriceFeedsNotInitializedError(); - } -}; - -type PriceFeeds = Map; - -export type PriceFeed = { - displaySymbol: string; - icon: ReactNode; - description: string; - key: Record; - assetClass: string; -}; - -class PriceFeedsNotInitializedError extends Error { - constructor() { - super( - "This component must be a child of to use the `usePriceFeeds` hook", - ); - this.name = "PriceFeedsNotInitializedError"; - } -} diff --git a/apps/insights/src/services/pyth.ts b/apps/insights/src/services/pyth.ts index e13f9161e8..e442b4cee3 100644 --- a/apps/insights/src/services/pyth.ts +++ b/apps/insights/src/services/pyth.ts @@ -68,16 +68,11 @@ export const getPublishersForFeed = async ( export const getFeeds = async (cluster: Cluster) => { const data = await clients[cluster].getData(); return priceFeedsSchema.parse( - data.symbols - .filter( - (symbol) => - data.productFromSymbol.get(symbol)?.display_symbol !== undefined, - ) - .map((symbol) => ({ - symbol, - product: data.productFromSymbol.get(symbol), - price: data.productPrice.get(symbol), - })), + data.symbols.map((symbol) => ({ + symbol, + product: data.productFromSymbol.get(symbol), + price: data.productPrice.get(symbol), + })), ); }; diff --git a/apps/insights/turbo.json b/apps/insights/turbo.json index 1dc5300e29..0a5ec06ed6 100644 --- a/apps/insights/turbo.json +++ b/apps/insights/turbo.json @@ -29,6 +29,9 @@ "dependsOn": ["//#install:modules"], "cache": false }, + "start:prod": { + "dependsOn": ["//#install:modules", "build:vercel"] + }, "test:lint": { "dependsOn": ["test:lint:eslint", "test:lint:stylelint"] }, diff --git a/packages/component-library/.storybook/main.ts b/packages/component-library/.storybook/main.ts index b31e21def2..efe6868cd9 100644 --- a/packages/component-library/.storybook/main.ts +++ b/packages/component-library/.storybook/main.ts @@ -34,6 +34,7 @@ const config = { modules: { auto: true, localIdentName: "[name]__[local]--[hash:base64:5]", + exportLocalsConvention: "as-is", }, importLoaders: 1, esModule: false, diff --git a/packages/component-library/.storybook/preview.tsx b/packages/component-library/.storybook/preview.tsx index 3aeb7a4f28..b7b5ad3024 100644 --- a/packages/component-library/.storybook/preview.tsx +++ b/packages/component-library/.storybook/preview.tsx @@ -2,46 +2,35 @@ import { sans } from "@pythnetwork/fonts"; import { withThemeByClassName } from "@storybook/addon-themes"; import type { Preview, Decorator } from "@storybook/react"; import clsx from "clsx"; -import { useState } from "react"; import "../src/Html/base.scss"; import styles from "./storybook.module.scss"; -import { OverlayVisibleContext } from "../src/overlay-visible-context.js"; +import { MainContent } from "../src/MainContent"; const preview = { parameters: { + layout: "fullscreen", backgrounds: { - grid: { - cellSize: 4, - cellAmount: 4, - }, - options: { - primary: { name: "Primary", value: "var(--primary-background)" }, - secondary: { name: "Secondary", value: "var(--secondary-background)" }, - }, + disable: true, }, actions: { argTypesRegex: "^on[A-Z].*" }, }, - initialGlobals: { - backgrounds: { value: "primary" }, - }, } satisfies Preview; export default preview; export const decorators: Decorator[] = [ - (Story) => { - const overlayVisibleState = useState(false); - return ( - - - - ); - }, + (Story) => ( + + + + ), withThemeByClassName({ themes: { - Light: clsx(sans.className, styles.light), - Dark: clsx(sans.className, styles.dark), + Light: styles.light ?? "", + "Light (Secondary Background)": clsx(styles.light, styles.secondary), + Dark: styles.dark ?? "", + "Dark (Secondary Background)": clsx(styles.dark, styles.secondary), }, defaultTheme: "Light", }), diff --git a/packages/component-library/.storybook/storybook.module.scss b/packages/component-library/.storybook/storybook.module.scss index 9013bff84c..d2104bcdef 100644 --- a/packages/component-library/.storybook/storybook.module.scss +++ b/packages/component-library/.storybook/storybook.module.scss @@ -6,11 +6,6 @@ body { width: 100%; } -:root { - --primary-background: #{theme.color("background", "primary")}; - --secondary-background: #{theme.color("background", "secondary")}; -} - .light { color-scheme: light; } @@ -18,3 +13,11 @@ body { .dark { color-scheme: dark; } + +.secondary .mainContent { + background: theme.color("background", "secondary"); +} + +.mainContent { + padding: theme.spacing(10); +} diff --git a/packages/component-library/src/Button/index.stories.tsx b/packages/component-library/src/Button/index.stories.tsx index 89ad08d7d1..f4f4f95972 100644 --- a/packages/component-library/src/Button/index.stories.tsx +++ b/packages/component-library/src/Button/index.stories.tsx @@ -60,11 +60,6 @@ const meta = { category: "Contents", }, }, - onPress: { - table: { - category: "Behavior", - }, - }, isPending: { control: "boolean", table: { @@ -97,4 +92,43 @@ export const Button = { rounded: false, hideText: false, }, + argTypes: { + onPress: { + table: { + category: "Behavior", + }, + }, + }, +} satisfies StoryObj; + +export const DrawerButton = { + args: { + children: "Open Drawer", + variant: "primary", + size: "md", + isDisabled: false, + isPending: false, + rounded: false, + hideText: false, + drawer: { + title: "Hello world", + contents: "This is a drawer", + }, + }, +} satisfies StoryObj; + +export const AlertButton = { + args: { + children: "Open Alert", + variant: "primary", + size: "md", + isDisabled: false, + isPending: false, + rounded: false, + hideText: false, + alert: { + title: "Alert!", + contents: "This is an alert", + }, + }, } satisfies StoryObj; diff --git a/packages/component-library/src/Card/index.tsx b/packages/component-library/src/Card/index.tsx index 8b6d653ecd..54277e601f 100644 --- a/packages/component-library/src/Card/index.tsx +++ b/packages/component-library/src/Card/index.tsx @@ -42,7 +42,7 @@ export const Card = ( return
; } else if ("href" in props) { return ; - } else if (overlayState !== null || "onPress" in props) { + } else if (overlayState !== null || "onPress" in props || "drawer" in props) { return - - - ), - ], - argTypes: { - title: { - control: "text", - table: { - category: "Contents", - }, - }, - children: { - control: "text", - table: { - category: "Contents", - }, - }, - closeHref: { - table: { - disable: true, - }, - }, - }, -} satisfies Meta; -export default meta; - -export const Drawer = { - args: { - title: "A drawer", - children: "This is a drawer", - }, -} satisfies StoryObj; diff --git a/packages/component-library/src/Link/index.stories.tsx b/packages/component-library/src/Link/index.stories.tsx index 50e57874bf..aaef7f64e3 100644 --- a/packages/component-library/src/Link/index.stories.tsx +++ b/packages/component-library/src/Link/index.stories.tsx @@ -11,18 +11,6 @@ const meta = { category: "Contents", }, }, - href: { - control: "text", - table: { - category: "Link", - }, - }, - target: { - control: "text", - table: { - category: "Link", - }, - }, isDisabled: { control: "boolean", table: { @@ -47,4 +35,42 @@ export const Link = { isDisabled: false, invert: false, }, + argTypes: { + href: { + control: "text", + table: { + category: "Link", + }, + }, + target: { + control: "text", + table: { + category: "Link", + }, + }, + }, +} satisfies StoryObj; + +export const DrawerLink = { + args: { + children: "Open Drawer", + isDisabled: false, + invert: false, + drawer: { + title: "Hello world", + contents: "This is a drawer", + }, + }, +} satisfies StoryObj; + +export const AlertLink = { + args: { + children: "Open Alert", + isDisabled: false, + invert: false, + alert: { + title: "An alert", + contents: "This is an alert", + }, + }, } satisfies StoryObj; diff --git a/packages/component-library/src/Link/index.tsx b/packages/component-library/src/Link/index.tsx index 9664c9503c..e5230bec5e 100644 --- a/packages/component-library/src/Link/index.tsx +++ b/packages/component-library/src/Link/index.tsx @@ -8,7 +8,10 @@ import { Link as UnstyledLink } from "../unstyled/Link/index.js"; type OwnProps = { invert?: boolean | undefined; }; -type Props = Omit, keyof OwnProps> & +export type Props = Omit< + ComponentProps, + keyof OwnProps +> & OwnProps; export const Link = ( diff --git a/packages/component-library/src/MainContent/index.module.scss b/packages/component-library/src/MainContent/index.module.scss index 7392aaf9ec..2670c6078b 100644 --- a/packages/component-library/src/MainContent/index.module.scss +++ b/packages/component-library/src/MainContent/index.module.scss @@ -8,4 +8,5 @@ overflow: hidden auto; transform: scale(calc(100% - (var(--offset) * 5%))); height: 100dvh; + scrollbar-gutter: stable; } diff --git a/packages/component-library/src/MainContent/index.tsx b/packages/component-library/src/MainContent/index.tsx index b42bef9db5..a82c1c0d51 100644 --- a/packages/component-library/src/MainContent/index.tsx +++ b/packages/component-library/src/MainContent/index.tsx @@ -1,55 +1,30 @@ "use client"; import clsx from "clsx"; -import type { - ComponentProps, - CSSProperties, - Dispatch, - SetStateAction, -} from "react"; -import { createContext, useState, use } from "react"; +import type { ComponentProps, CSSProperties } from "react"; +import { useState } from "react"; import styles from "./index.module.scss"; import { OverlayVisibleContext } from "../overlay-visible-context.js"; - -const MainContentOffsetContext = createContext< - undefined | [number, Dispatch>] ->(undefined); +import { AlertProvider } from "../useAlert/index.js"; +import { DrawerProvider } from "../useDrawer/index.js"; export const MainContent = ({ className, ...props }: ComponentProps<"div">) => { const overlayVisibleState = useState(false); - const offset = useState(0); + const [offset, setOffset] = 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/ModalDialog/index.tsx b/packages/component-library/src/ModalDialog/index.tsx index fbfb6a9d68..035d7f4422 100644 --- a/packages/component-library/src/ModalDialog/index.tsx +++ b/packages/component-library/src/ModalDialog/index.tsx @@ -2,8 +2,21 @@ import type { PanInfo } from "motion/react"; import { motion } from "motion/react"; -import type { ComponentProps, Dispatch, SetStateAction } from "react"; -import { createContext, use, useCallback, useState, useEffect } from "react"; +import type { + ComponentProps, + ComponentType, + Dispatch, + ReactNode, + SetStateAction, +} from "react"; +import { + createContext, + use, + useCallback, + useState, + useEffect, + useRef, +} from "react"; import type { ModalRenderProps } from "react-aria-components"; import { Modal, @@ -72,6 +85,7 @@ type OwnProps = Pick, "children"> & overlayVariants?: | ComponentProps["variants"] | undefined; + onClose?: (() => void) | undefined; onCloseFinish?: (() => void) | undefined; onDragEnd?: ( e: MouseEvent | TouchEvent | PointerEvent, @@ -86,6 +100,7 @@ type Props = Omit, keyof OwnProps> & export const ModalDialog = ({ isOpen, onOpenChange, + onClose, onCloseFinish, overlayClassName, overlayVariants, @@ -102,6 +117,8 @@ export const ModalDialog = ({ const startAnimation = (animation: AnimationState) => { if (animation === "visible") { showOverlay(); + } else if (animation === "hidden") { + onClose?.(); } }; @@ -159,3 +176,114 @@ export const ModalDialog = ({ }; type AnimationState = "unmounted" | "hidden" | "visible"; + +export const createModalDialogContext = < + T extends Props, + U = Record, +>( + Component: ComponentType, +) => { + type ContextType = { + close: () => Promise; + open: (modalDialogProps: OpenArgs) => void; + }; + + const Context = createContext(undefined); + + return { + Provider: ({ children, ...ctxProps }: U & { children: ReactNode }) => { + const promiseCloseResolvers = useRef<(() => void)[]>([]); + const [isOpen, setIsOpen] = useState(false); + const [currentModalDialog, setModalDialog] = useState< + OpenArgs | undefined + >(undefined); + const close = useCallback(() => { + setIsOpen(false); + return new Promise((resolve) => { + promiseCloseResolvers.current.push(resolve); + }); + }, []); + const open = useCallback( + (props: OpenArgs) => { + if (currentModalDialog && currentModalDialog !== props) { + close() + .then(() => { + setTimeout(() => { + setModalDialog(props); + setIsOpen(true); + }); + }) + .catch((error: unknown) => { + throw error; + }); + } else if (!currentModalDialog) { + setModalDialog(props); + setIsOpen(true); + } + }, + [currentModalDialog, setModalDialog, close], + ); + const handleOpenChange = useCallback( + (newValue: boolean) => { + if (!newValue) { + setIsOpen(false); + } + }, + [setIsOpen], + ); + const handleCloseFinish = useCallback(() => { + const onCloseFinished = currentModalDialog?.onCloseFinished; + setModalDialog(undefined); + onCloseFinished?.(); + for (const resolver of promiseCloseResolvers.current) { + resolver(); + } + promiseCloseResolvers.current = []; + }, [setModalDialog, currentModalDialog]); + + return ( + + {children} + {currentModalDialog !== undefined && ( + // @ts-expect-error TODO typescript isn't validating this type + // properly. To be honest, I'm not sure why, but the code for + // `createModalDialogContext` is pretty messy and I think + // simplifying this would probably resolve the issue. I'll come + // back and refactor this eventually and see if this goes away... + + )} + + ); + }, + + useValue: () => { + const value = use(Context); + if (value === undefined) { + throw new ContextNotInitializedError(); + } else { + return value; + } + }, + }; +}; + +export type OpenArgs = Omit< + T, + "isOpen" | "onOpenChange" | "onCloseFinish" | keyof U +> & { + onClose?: (() => void) | undefined; + onCloseFinished?: (() => void) | undefined; +}; + +class ContextNotInitializedError extends Error { + constructor() { + super("This component must be contained within a provider"); + this.name = "ContextNotInitializedError"; + } +} diff --git a/packages/component-library/src/Paginator/index.module.scss b/packages/component-library/src/Paginator/index.module.scss index 25c692ef82..c6a174ec87 100644 --- a/packages/component-library/src/Paginator/index.module.scss +++ b/packages/component-library/src/Paginator/index.module.scss @@ -4,6 +4,7 @@ display: flex; flex-flow: row nowrap; justify-content: center; + align-items: center; @include theme.breakpoint("sm") { justify-content: space-between; diff --git a/packages/component-library/src/Select/index.stories.tsx b/packages/component-library/src/Select/index.stories.tsx index 89b29f4712..ed85cf58fe 100644 --- a/packages/component-library/src/Select/index.stories.tsx +++ b/packages/component-library/src/Select/index.stories.tsx @@ -4,7 +4,7 @@ import { Select as SelectComponent } from "./index.js"; import buttonMeta from "../Button/index.stories.js"; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const { children, beforeIcon, onPress, ...argTypes } = buttonMeta.argTypes; +const { children, beforeIcon, ...argTypes } = buttonMeta.argTypes; const meta = { component: SelectComponent, argTypes: { diff --git a/packages/component-library/src/Spinner/index.tsx b/packages/component-library/src/Spinner/index.tsx index cc1def279a..a0d2471a34 100644 --- a/packages/component-library/src/Spinner/index.tsx +++ b/packages/component-library/src/Spinner/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import clsx from "clsx"; import type { ComponentProps } from "react"; import { ProgressBar } from "react-aria-components"; diff --git a/packages/component-library/src/TabList/index.stories.tsx b/packages/component-library/src/TabList/index.stories.tsx index 65ba2e6100..19a540253c 100644 --- a/packages/component-library/src/TabList/index.stories.tsx +++ b/packages/component-library/src/TabList/index.stories.tsx @@ -11,7 +11,7 @@ const meta = { disable: true, }, }, - pathname: { + currentTab: { table: { disable: true, }, diff --git a/packages/component-library/src/TabList/index.tsx b/packages/component-library/src/TabList/index.tsx index 8410ba7c77..92f9870841 100644 --- a/packages/component-library/src/TabList/index.tsx +++ b/packages/component-library/src/TabList/index.tsx @@ -10,17 +10,17 @@ import { Tab, TabList as UnstyledTabList } from "../unstyled/Tabs/index.js"; type OwnProps = { label: string; - pathname?: string | undefined; + currentTab?: string | undefined; items: ComponentProps[]; }; type Props = Omit, keyof OwnProps> & OwnProps; -export const TabList = ({ label, className, pathname, ...props }: Props) => ( +export const TabList = ({ label, className, currentTab, ...props }: Props) => (
@@ -29,7 +29,7 @@ export const TabList = ({ label, className, pathname, ...props }: Props) => ( className={clsx(styles.tab, buttonStyles.button, tabClassName)} data-size="sm" data-variant="ghost" - data-selectable={pathname === tab.href ? undefined : ""} + data-selectable={currentTab === tab.id ? undefined : ""} {...tab} > {(args) => ( diff --git a/packages/component-library/src/unstyled/Button/index.tsx b/packages/component-library/src/unstyled/Button/index.tsx index 51c91c37c9..6770e4c27f 100644 --- a/packages/component-library/src/unstyled/Button/index.tsx +++ b/packages/component-library/src/unstyled/Button/index.tsx @@ -1,3 +1,60 @@ "use client"; -export { Button } from "react-aria-components"; +import type { ComponentProps } from "react"; +import { useCallback } from "react"; +import type { PressEvent } from "react-aria-components"; +import { Button as BaseButton } from "react-aria-components"; + +import type { OpenAlertArgs } from "../../useAlert/index.js"; +import { useAlert } from "../../useAlert/index.js"; +import type { OpenDrawerArgs } from "../../useDrawer/index.js"; +import { useDrawer } from "../../useDrawer/index.js"; + +export type Props = ComponentProps & { + alert?: OpenAlertArgs | undefined; + drawer?: OpenDrawerArgs | undefined; +}; + +export const Button = ({ drawer, alert, ...props }: Props) => { + if (drawer !== undefined) { + return ; + } else if (alert === undefined) { + return ; + } else { + return ; + } +}; + +const DrawerButton = ({ + onPress, + drawer: drawerConfig, + ...props +}: Props & { drawer: OpenDrawerArgs }) => { + const drawer = useDrawer(); + const openDrawer = useCallback( + (event: PressEvent) => { + onPress?.(event); + drawer.open(drawerConfig); + }, + [drawer, drawerConfig, onPress], + ); + + return ; +}; + +const AlertButton = ({ + onPress, + alert: alertConfig, + ...props +}: Props & { alert: OpenAlertArgs }) => { + const alert = useAlert(); + const openDrawer = useCallback( + (event: PressEvent) => { + onPress?.(event); + alert.open(alertConfig); + }, + [alert, alertConfig, onPress], + ); + + return ; +}; diff --git a/packages/component-library/src/unstyled/Link/index.tsx b/packages/component-library/src/unstyled/Link/index.tsx index 34122cdc77..35587b133e 100644 --- a/packages/component-library/src/unstyled/Link/index.tsx +++ b/packages/component-library/src/unstyled/Link/index.tsx @@ -5,7 +5,7 @@ import { Link as BaseLink } from "react-aria-components"; import { usePrefetch } from "../../use-prefetch.js"; -type Props = ComponentProps & { +export type Props = ComponentProps & { prefetch?: Parameters[0]["prefetch"]; }; diff --git a/packages/component-library/src/Alert/index.module.scss b/packages/component-library/src/useAlert/index.module.scss similarity index 100% rename from packages/component-library/src/Alert/index.module.scss rename to packages/component-library/src/useAlert/index.module.scss diff --git a/packages/component-library/src/Alert/index.stories.tsx b/packages/component-library/src/useAlert/index.stories.tsx similarity index 71% rename from packages/component-library/src/Alert/index.stories.tsx rename to packages/component-library/src/useAlert/index.stories.tsx index 787b0fe378..702d806f63 100644 --- a/packages/component-library/src/Alert/index.stories.tsx +++ b/packages/component-library/src/useAlert/index.stories.tsx @@ -1,19 +1,27 @@ import * as Icon from "@phosphor-icons/react/dist/ssr"; import type { Meta, StoryObj } from "@storybook/react"; -import { Alert as AlertComponent, AlertTrigger } from "./index.js"; +import { useAlert as useAlertImpl } from "./index.js"; import { Button } from "../Button/index.js"; +const ShowButton = ( + props: Parameters["open"]>[0], +) => { + const drawer = useAlertImpl(); + return ( + + ); +}; + const meta = { - component: AlertComponent, - decorators: [ - (Story) => ( - - - - - ), - ], + title: "hooks/useAlert", + component: ShowButton, argTypes: { icon: { control: "select", @@ -41,13 +49,14 @@ const meta = { }, }, }, -} satisfies Meta; +} satisfies Meta; export default meta; -export const Alert = { +export const useAlert = { + name: "useAlert", args: { title: "An Alert", children: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", }, -} satisfies StoryObj; +} satisfies StoryObj; diff --git a/packages/component-library/src/Alert/index.tsx b/packages/component-library/src/useAlert/index.tsx similarity index 52% rename from packages/component-library/src/Alert/index.tsx rename to packages/component-library/src/useAlert/index.tsx index 6e34bc396b..9e96d9e2fb 100644 --- a/packages/component-library/src/Alert/index.tsx +++ b/packages/component-library/src/useAlert/index.tsx @@ -7,17 +7,16 @@ import { Heading } from "react-aria-components"; import styles from "./index.module.scss"; import { Button } from "../Button/index.js"; -import { ModalDialog } from "../ModalDialog/index.js"; - -export { ModalDialogTrigger as AlertTrigger } from "../ModalDialog/index.js"; +import type { OpenArgs } from "../ModalDialog/index.js"; +import { ModalDialog, createModalDialogContext } from "../ModalDialog/index.js"; const CLOSE_DURATION_IN_S = 0.1; -export const CLOSE_DURATION_IN_MS = CLOSE_DURATION_IN_S * 1000; -type OwnProps = Pick, "children"> & { +type OwnProps = { icon?: ReactNode | undefined; title: ReactNode; bodyClassName?: string | undefined; + contents: ReactNode; }; type Props = Omit< @@ -26,10 +25,10 @@ type Props = Omit< > & OwnProps; -export const Alert = ({ +const Alert = ({ icon, title, - children, + contents, className, bodyClassName, ...props @@ -50,27 +49,27 @@ export const Alert = ({ className={clsx(styles.alert, className)} {...props} > - {(...args) => ( - <> - - - {icon &&
{icon}
} -
{title}
-
-
- {typeof children === "function" ? children(...args) : children} -
- - )} + + + {icon &&
{icon}
} +
{title}
+
+
{contents}
); + +const { Provider, useValue } = createModalDialogContext(Alert); + +export const AlertProvider = Provider; +export const useAlert = useValue; +export type OpenAlertArgs = OpenArgs; diff --git a/packages/component-library/src/Drawer/index.module.scss b/packages/component-library/src/useDrawer/index.module.scss similarity index 76% rename from packages/component-library/src/Drawer/index.module.scss rename to packages/component-library/src/useDrawer/index.module.scss index c23c025fa9..bebb3458d6 100644 --- a/packages/component-library/src/Drawer/index.module.scss +++ b/packages/component-library/src/useDrawer/index.module.scss @@ -26,15 +26,32 @@ overflow-y: hidden; @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"); + &[data-variant="dialog"] { + position: relative; + top: theme.spacing(32); + left: unset; + right: unset; + bottom: unset; + margin: 0 auto; + background: theme.color("background", "secondary"); + border: unset; + border-radius: theme.border-radius("2xl"); + padding: theme.spacing(1); + max-height: theme.spacing(120); + width: max-content; + } + + &[data-variant="default"] { + 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 { @@ -111,6 +128,7 @@ flex: 1; overflow-y: auto; padding: theme.spacing(4); + grid-auto-rows: minmax(0, max-content); @include theme.breakpoint("sm") { padding: theme.spacing(6); @@ -136,14 +154,8 @@ } } - &[data-hide-heading] { - .heading { - display: none; - - @include theme.breakpoint("sm") { - display: flex; - } - } + &[data-hide-heading] .heading { + @include theme.sr-only; } } } diff --git a/packages/component-library/src/useDrawer/index.stories.tsx b/packages/component-library/src/useDrawer/index.stories.tsx new file mode 100644 index 0000000000..89402f16ce --- /dev/null +++ b/packages/component-library/src/useDrawer/index.stories.tsx @@ -0,0 +1,52 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { useDrawer as useDrawerImpl } from "./index.js"; +import { Button } from "../Button/index.js"; + +const OpenButton = ( + props: Parameters["open"]>[0], +) => { + const drawer = useDrawerImpl(); + return ( + + ); +}; + +const meta = { + title: "hooks/useDrawer", + component: OpenButton, + argTypes: { + title: { + control: "text", + table: { + category: "Contents", + }, + }, + contents: { + control: "text", + table: { + category: "Contents", + }, + }, + onClose: { + table: { + category: "Behavior", + }, + }, + }, +} satisfies Meta; +export default meta; + +export const useDrawer = { + name: "useDrawer", + args: { + title: "A drawer", + contents: "This is a drawer", + }, +} satisfies StoryObj; diff --git a/packages/component-library/src/Drawer/index.tsx b/packages/component-library/src/useDrawer/index.tsx similarity index 55% rename from packages/component-library/src/Drawer/index.tsx rename to packages/component-library/src/useDrawer/index.tsx index 645ed84fed..f84ac72fb1 100644 --- a/packages/component-library/src/Drawer/index.tsx +++ b/packages/component-library/src/useDrawer/index.tsx @@ -1,5 +1,3 @@ -"use client"; - import { XCircle } from "@phosphor-icons/react/dist/ssr/XCircle"; import { useMediaQuery } from "@react-hookz/web"; import clsx from "clsx"; @@ -10,13 +8,10 @@ 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"; +import type { OpenArgs } from "../ModalDialog/index.js"; +import { ModalDialog, createModalDialogContext } from "../ModalDialog/index.js"; const CLOSE_DURATION_IN_S = 0.15; -export const CLOSE_DURATION_IN_MS = CLOSE_DURATION_IN_S * 1000; type OwnProps = { fill?: boolean | undefined; @@ -29,19 +24,26 @@ type OwnProps = { bodyClassName?: string | undefined; footerClassName?: string | undefined; hideHeading?: boolean | undefined; + setMainContentOffset: (value: number) => void; + contents: ReactNode | undefined; + variant?: "default" | "dialog" | undefined; }; type Props = Omit< ComponentProps, - keyof OwnProps | "overlayVariants" | "overlayClassName" | "variants" + | keyof OwnProps + | "overlayVariants" + | "overlayClassName" + | "variants" + | "children" > & OwnProps; -export const Drawer = ({ +const Drawer = ({ className, title, closeHref, - children, + contents, fill, footer, headingClassName, @@ -50,95 +52,30 @@ export const Drawer = ({ headingExtra, headingAfter, hideHeading, + setMainContentOffset, + variant = "default", ...props }: 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 { isDragging, props: animationProps } = useAnimationProps( + variant, + setMainContentOffset, ); - 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 { - 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} + {...animationProps} {...props} > {(...args) => ( @@ -183,9 +120,7 @@ export const Drawer = ({
{headingAfter}
-
- {typeof children === "function" ? children(...args) : children} -
+
{contents}
{footer && (
{footer}
)} @@ -195,6 +130,132 @@ export const Drawer = ({ ); }; +const useAnimationProps = ( + variant: Props["variant"], + setMainContentOffset: (value: number) => void, +): { + isDragging: boolean; + props: Partial>; +} => { + const modalRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + 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); + } + }); + const isLarge = useMediaQuery( + `(min-width: ${styles["breakpoint-sm"] ?? ""})`, + ); + + const commonProps = { + ref: modalRef, + }; + + return isLarge + ? { + isDragging: false, + props: { + ...commonProps, + variants: + variant === "dialog" + ? { + visible: { + y: 0, + transition: { type: "spring", duration: 0.8, bounce: 0.35 }, + }, + hidden: { + y: "calc(-100% - 8rem)", + transition: { + ease: "linear", + duration: CLOSE_DURATION_IN_S, + }, + }, + unmounted: { + y: "calc(-100% - 8rem)", + }, + } + : { + visible: { + x: 0, + transition: { type: "spring", duration: 1, bounce: 0.35 }, + }, + hidden: { + x: "calc(100% + 1rem)", + transition: { + ease: "linear", + duration: CLOSE_DURATION_IN_S, + }, + }, + unmounted: { + x: "calc(100% + 1rem)", + }, + }, + }, + } + : { + isDragging, + props: { + ...commonProps, + variants: { + visible: { + y: 0, + transition: { + duration: 0.5, + ease: [0.32, 0.72, 0, 1], + }, + }, + hidden: { + y: "100%", + transition: { ease: "linear", duration: CLOSE_DURATION_IN_S }, + }, + unmounted: { + y: "100%", + }, + }, + style: { y }, + drag: "y", + dragConstraints: { top: 0 }, + dragElastic: false, + dragPropagation: true, + onDragStart: () => { + setIsDragging(true); + }, + onDragEnd: (e, { velocity }, { state }) => { + setIsDragging(false); + if (e.type !== "pointercancel" && velocity.y > 10) { + state.close(); + } else { + animate(y, "0", { + type: "inertia", + bounceStiffness: 300, + bounceDamping: 40, + timeConstant: 300, + min: 0, + max: 0, + }); + } + }, + }, + }; +}; + +const { Provider, useValue } = createModalDialogContext< + Props, + Pick +>(Drawer); + +export const DrawerProvider = Provider; +export const useDrawer = useValue; +export type OpenDrawerArgs = OpenArgs< + Props, + Pick +>; + type OnResizeProps = { threshold: string | undefined; onResize: () => void; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbeec3effb..d874d60c4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,8 @@ catalogs: specifier: ^2.2.0 version: 2.2.0 '@next/third-parties': - specifier: ^15.2.4 - version: 15.2.4 + specifier: ^15.3.1 + version: 15.3.1 '@phosphor-icons/react': specifier: ^2.1.7 version: 2.1.7 @@ -175,11 +175,11 @@ catalogs: specifier: ^3.0.1 version: 3.0.1 motion: - specifier: ^12.6.3 - version: 12.6.3 + specifier: ^12.9.2 + version: 12.9.2 next: - specifier: ^15.2.4 - version: 15.2.4 + specifier: ^15.3.1 + version: 15.3.1 next-themes: specifier: ^0.4.6 version: 0.4.6 @@ -327,7 +327,7 @@ importers: version: 2.2.0(react@19.1.0) '@next/third-parties': specifier: 'catalog:' - version: 15.2.4(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0) + version: 15.3.1(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0) '@pythnetwork/client': specifier: 'catalog:' version: 2.22.1(@solana/web3.js@1.98.0(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) @@ -357,7 +357,7 @@ importers: version: 12.6.3(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: specifier: 'catalog:' - version: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + version: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -394,7 +394,7 @@ importers: version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2) '@cprussin/jest-config': specifier: 'catalog:' - version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10) + version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10) '@cprussin/prettier-config': specifier: 'catalog:' version: 2.2.2(prettier@3.5.3) @@ -469,7 +469,7 @@ importers: version: 0.487.0(react@19.1.0) next: specifier: 'catalog:' - version: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + version: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) react: specifier: 'catalog:' version: 19.1.0 @@ -494,7 +494,7 @@ importers: version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2) '@cprussin/jest-config': specifier: 'catalog:' - version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) + version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) '@cprussin/prettier-config': specifier: 'catalog:' version: 2.2.2(prettier@3.5.3) @@ -636,16 +636,16 @@ importers: version: 5.0.5 motion: specifier: 'catalog:' - version: 12.6.3(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 12.9.2(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: specifier: 'catalog:' - version: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + version: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) nuqs: specifier: 'catalog:' - version: 2.4.1(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0) + version: 2.4.1(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0) react: specifier: 'catalog:' version: 19.1.0 @@ -676,7 +676,7 @@ importers: version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2) '@cprussin/jest-config': specifier: 'catalog:' - version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10) + version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10) '@cprussin/prettier-config': specifier: 'catalog:' version: 2.2.2(prettier@3.5.3) @@ -881,7 +881,7 @@ importers: version: 2.2.0(react@19.1.0) '@next/third-parties': specifier: 'catalog:' - version: 15.2.4(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0) + version: 15.3.1(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0) '@pythnetwork/hermes-client': specifier: workspace:* version: link:../hermes/client/js @@ -929,7 +929,7 @@ importers: version: 0.2.0 next: specifier: 'catalog:' - version: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + version: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) pino: specifier: 'catalog:' version: 9.6.0 @@ -966,7 +966,7 @@ importers: version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2) '@cprussin/jest-config': specifier: 'catalog:' - version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10) + version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10) '@cprussin/prettier-config': specifier: 'catalog:' version: 2.2.2(prettier@3.5.3) @@ -1196,7 +1196,7 @@ importers: version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2) '@cprussin/jest-config': specifier: 'catalog:' - version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10) + version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10) '@cprussin/prettier-config': specifier: 'catalog:' version: 2.2.2(prettier@3.5.3) @@ -1509,13 +1509,13 @@ importers: version: link:../../../../pythnet/message_buffer next: specifier: 'catalog:' - version: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + version: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) next-seo: specifier: ^5.15.0 - version: 5.15.0(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 5.15.0(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) nuqs: specifier: 'catalog:' - version: 2.4.1(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0) + version: 2.4.1(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0) react: specifier: 'catalog:' version: 19.1.0 @@ -1684,7 +1684,7 @@ importers: version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2) '@cprussin/jest-config': specifier: 'catalog:' - version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) + version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) '@cprussin/prettier-config': specifier: 'catalog:' version: 2.2.2(prettier@3.5.3) @@ -1729,7 +1729,7 @@ importers: version: 3.0.1 motion: specifier: 'catalog:' - version: 12.6.3(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 12.9.2(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-aria: specifier: 'catalog:' version: 3.38.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1742,7 +1742,7 @@ importers: version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2) '@cprussin/jest-config': specifier: 'catalog:' - version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) + version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) '@cprussin/prettier-config': specifier: 'catalog:' version: 2.2.2(prettier@3.5.3) @@ -1766,7 +1766,7 @@ importers: version: 8.6.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.3)) '@storybook/nextjs': specifier: 'catalog:' - version: 8.6.12(esbuild@0.25.2)(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.3))(type-fest@4.39.0)(typescript@5.8.2)(webpack-hot-middleware@2.26.1)(webpack@5.98.0(esbuild@0.25.2)) + version: 8.6.12(esbuild@0.25.2)(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.3))(type-fest@4.39.0)(typescript@5.8.2)(webpack-hot-middleware@2.26.1)(webpack@5.98.0(esbuild@0.25.2)) '@storybook/react': specifier: 'catalog:' version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.3)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.3))(typescript@5.8.2) @@ -1790,7 +1790,7 @@ importers: version: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)) next: specifier: 'catalog:' - version: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + version: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) postcss: specifier: 'catalog:' version: 8.5.3 @@ -1832,7 +1832,7 @@ importers: version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2) '@cprussin/jest-config': specifier: 'catalog:' - version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) + version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) '@cprussin/prettier-config': specifier: 'catalog:' version: 2.2.2(prettier@3.5.3) @@ -1850,7 +1850,7 @@ importers: version: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)) next: specifier: 'catalog:' - version: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + version: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) prettier: specifier: 'catalog:' version: 3.5.3 @@ -1865,7 +1865,7 @@ importers: version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2) '@cprussin/jest-config': specifier: 'catalog:' - version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) + version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) '@cprussin/prettier-config': specifier: 'catalog:' version: 2.2.2(prettier@3.5.3) @@ -1907,7 +1907,7 @@ importers: version: 4.10.1 '@next/third-parties': specifier: 'catalog:' - version: 15.2.4(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0) + version: 15.3.1(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0) '@pythnetwork/app-logger': specifier: workspace:* version: link:../app-logger @@ -1926,7 +1926,7 @@ importers: version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2) '@cprussin/jest-config': specifier: 'catalog:' - version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) + version: 2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3) '@cprussin/prettier-config': specifier: 'catalog:' version: 2.2.2(prettier@3.5.3) @@ -1950,7 +1950,7 @@ importers: version: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)) next: specifier: 'catalog:' - version: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + version: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) prettier: specifier: 'catalog:' version: 3.5.3 @@ -5260,105 +5260,215 @@ packages: cpu: [arm64] os: [darwin] + '@img/sharp-darwin-arm64@0.34.1': + resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-x64@0.33.5': resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.34.1': + resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.0.4': resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.1.0': + resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.0.4': resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.1.0': + resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-linux-arm64@1.0.4': resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm64@1.1.0': + resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + '@img/sharp-libvips-linux-arm@1.1.0': + resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.1.0': + resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + cpu: [ppc64] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-s390x@1.1.0': + resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} + cpu: [s390x] + os: [linux] + '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linux-x64@1.1.0': + resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} + cpu: [x64] + os: [linux] + '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm64@0.34.1': + resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + '@img/sharp-linux-arm@0.34.1': + resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + '@img/sharp-linux-s390x@0.34.1': + resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linux-x64@0.34.1': + resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.1': + resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-x64@0.34.1': + resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] + '@img/sharp-wasm32@0.34.1': + resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + '@img/sharp-win32-ia32@0.33.5': resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-ia32@0.34.1': + resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-x64@0.33.5': resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.34.1': + resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@improbable-eng/grpc-web@0.14.1': resolution: {integrity: sha512-XaIYuunepPxoiGVLLHmlnVminUGzBTnXr8Wv7khzmLWbNw4TCwJKX09GSMJlKhu/TRk6gms0ySFxewaETSBqgw==} peerDependencies: @@ -5928,8 +6038,8 @@ packages: '@near-js/wallet-account@1.1.1': resolution: {integrity: sha512-NnoJKtogBQ7Qz+AP+LdF70BP8Az6UXQori7OjPqJLMo73bn6lh5Ywvegwd1EB7ZEVe4BRt9+f9QkbU5M8ANfAw==} - '@next/env@15.2.4': - resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==} + '@next/env@15.3.1': + resolution: {integrity: sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==} '@next/eslint-plugin-next@14.2.26': resolution: {integrity: sha512-SPEj1O5DAVTPaWD9XPupelfT2APNIgcDYD2OzEm328BEmHaglhmYNUvxhzfJYDr12AgAfW4V3UHSV93qaeELJA==} @@ -5937,56 +6047,56 @@ packages: '@next/eslint-plugin-next@15.2.4': resolution: {integrity: sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==} - '@next/swc-darwin-arm64@15.2.4': - resolution: {integrity: sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==} + '@next/swc-darwin-arm64@15.3.1': + resolution: {integrity: sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.2.4': - resolution: {integrity: sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==} + '@next/swc-darwin-x64@15.3.1': + resolution: {integrity: sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.2.4': - resolution: {integrity: sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==} + '@next/swc-linux-arm64-gnu@15.3.1': + resolution: {integrity: sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.2.4': - resolution: {integrity: sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==} + '@next/swc-linux-arm64-musl@15.3.1': + resolution: {integrity: sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.2.4': - resolution: {integrity: sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==} + '@next/swc-linux-x64-gnu@15.3.1': + resolution: {integrity: sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.2.4': - resolution: {integrity: sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==} + '@next/swc-linux-x64-musl@15.3.1': + resolution: {integrity: sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.2.4': - resolution: {integrity: sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==} + '@next/swc-win32-arm64-msvc@15.3.1': + resolution: {integrity: sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.2.4': - resolution: {integrity: sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==} + '@next/swc-win32-x64-msvc@15.3.1': + resolution: {integrity: sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@next/third-parties@15.2.4': - resolution: {integrity: sha512-a8GlPnMmPymxyLOiSnh5InUsG/hw7wjU3munGoHNB+oLCPruAeoplBa9Uf/xE83WMyutyK4cbi5Ixu4uyh96Mw==} + '@next/third-parties@15.3.1': + resolution: {integrity: sha512-8v1pAtRjcaCbs80qcYLLCrSsECgeSb0WMU0J3pMBYNavG3Y3yQOgFog18nVgiNrNB20HkyrScquWsy8gcdiGRA==} peerDependencies: next: ^13.0.0 || ^14.0.0 || ^15.0.0 react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -14352,6 +14462,20 @@ packages: react-dom: optional: true + framer-motion@12.9.2: + resolution: {integrity: sha512-R0O3Jdqbfwywpm45obP+8sTgafmdEcUoShQTAV+rB5pi+Y1Px/FYL5qLLRe5tPtBdN1J4jos7M+xN2VV2oEAbQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + framer-motion@6.5.1: resolution: {integrity: sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==} peerDependencies: @@ -16696,14 +16820,20 @@ packages: motion-dom@12.6.3: resolution: {integrity: sha512-gRY08RjcnzgFYLemUZ1lo/e9RkBxR+6d4BRvoeZDSeArG4XQXERSPapKl3LNQRu22Sndjf1h+iavgY0O4NrYqA==} + motion-dom@12.9.1: + resolution: {integrity: sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==} + motion-utils@12.6.3: resolution: {integrity: sha512-R/b3Ia2VxtTNZ4LTEO5pKYau1OUNHOuUfxuP0WFCTDYdHkeTBR9UtxR1cc8mDmKr8PEhmmfnTKGz3rSMjNRoRg==} + motion-utils@12.8.3: + resolution: {integrity: sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==} + motion@10.16.2: resolution: {integrity: sha512-p+PurYqfUdcJZvtnmAqu5fJgV2kR0uLFQuBKtLeFVTrYEVllI99tiOTSefVNYuip9ELTEkepIIDftNdze76NAQ==} - motion@12.6.3: - resolution: {integrity: sha512-zw/vqUgv5F5m9fkvOl/eCv2AF1+tkeZl3fu2uIlisIaip8sm5e0CouAl6GkdiRoF+G7s29CjqMdIyPMirwUGHA==} + motion@12.9.2: + resolution: {integrity: sha512-2hwi4wlOpt/zDHcDZATL2FFhYgj2n6t5Hd0UT91swMup6dx6KpFRkTydYJkkV0PUImT1QfC+WT5d0eRekTKpcg==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -16852,8 +16982,8 @@ packages: next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - next@15.2.4: - resolution: {integrity: sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==} + next@15.3.1: + resolution: {integrity: sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -18926,6 +19056,10 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.34.1: + resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -23734,7 +23868,7 @@ snapshots: - turbo - typescript - '@cprussin/jest-config@2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3)': + '@cprussin/jest-config@2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(esbuild@0.25.2)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3)': dependencies: '@cprussin/jest-runner-eslint': 0.0.1(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))) '@cprussin/jest-runner-prettier': 1.0.0(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(prettier@3.5.3) @@ -23745,7 +23879,7 @@ snapshots: ts-jest: 29.3.1(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.25.2)(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(typescript@5.8.2) typescript: 5.8.2 optionalDependencies: - next: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + next: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) transitivePeerDependencies: - '@babel/core' - '@jest/test-result' @@ -23760,7 +23894,7 @@ snapshots: - supports-color - utf-8-validate - '@cprussin/jest-config@2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10)': + '@cprussin/jest-config@2.0.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(bufferutil@4.0.9)(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10)': dependencies: '@cprussin/jest-runner-eslint': 0.0.1(eslint@9.23.0(jiti@1.21.7))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))) '@cprussin/jest-runner-prettier': 1.0.0(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(prettier@3.5.3) @@ -23771,7 +23905,7 @@ snapshots: ts-jest: 29.3.1(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.25.2)(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2)))(typescript@5.8.2) typescript: 5.8.2 optionalDependencies: - next: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + next: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) transitivePeerDependencies: - '@babel/core' - '@jest/test-result' @@ -25439,76 +25573,154 @@ snapshots: '@img/sharp-libvips-darwin-arm64': 1.0.4 optional: true + '@img/sharp-darwin-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.1.0 + optional: true + '@img/sharp-darwin-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.0.4 optional: true + '@img/sharp-darwin-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.1.0 + optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.1.0': + optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': optional: true + '@img/sharp-libvips-darwin-x64@1.1.0': + optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': optional: true + '@img/sharp-libvips-linux-arm64@1.1.0': + optional: true + '@img/sharp-libvips-linux-arm@1.0.5': optional: true + '@img/sharp-libvips-linux-arm@1.1.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.1.0': + optional: true + '@img/sharp-libvips-linux-s390x@1.0.4': optional: true + '@img/sharp-libvips-linux-s390x@1.1.0': + optional: true + '@img/sharp-libvips-linux-x64@1.0.4': optional: true + '@img/sharp-libvips-linux-x64@1.1.0': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + optional: true + '@img/sharp-linux-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.0.4 optional: true + '@img/sharp-linux-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.1.0 + optional: true + '@img/sharp-linux-arm@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.0.5 optional: true + '@img/sharp-linux-arm@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.1.0 + optional: true + '@img/sharp-linux-s390x@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.0.4 optional: true + '@img/sharp-linux-s390x@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.1.0 + optional: true + '@img/sharp-linux-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.0.4 optional: true + '@img/sharp-linux-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.1.0 + optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 optional: true + '@img/sharp-linuxmusl-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + optional: true + '@img/sharp-linuxmusl-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.0.4 optional: true + '@img/sharp-linuxmusl-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + optional: true + '@img/sharp-wasm32@0.33.5': dependencies: '@emnapi/runtime': 1.4.0 optional: true + '@img/sharp-wasm32@0.34.1': + dependencies: + '@emnapi/runtime': 1.4.0 + optional: true + '@img/sharp-win32-ia32@0.33.5': optional: true + '@img/sharp-win32-ia32@0.34.1': + optional: true + '@img/sharp-win32-x64@0.33.5': optional: true + '@img/sharp-win32-x64@0.34.1': + optional: true + '@improbable-eng/grpc-web@0.14.1(google-protobuf@3.21.4)': dependencies: browser-headers: 0.4.1 @@ -27256,7 +27468,7 @@ snapshots: transitivePeerDependencies: - encoding - '@next/env@15.2.4': {} + '@next/env@15.3.1': {} '@next/eslint-plugin-next@14.2.26': dependencies: @@ -27266,33 +27478,33 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.2.4': + '@next/swc-darwin-arm64@15.3.1': optional: true - '@next/swc-darwin-x64@15.2.4': + '@next/swc-darwin-x64@15.3.1': optional: true - '@next/swc-linux-arm64-gnu@15.2.4': + '@next/swc-linux-arm64-gnu@15.3.1': optional: true - '@next/swc-linux-arm64-musl@15.2.4': + '@next/swc-linux-arm64-musl@15.3.1': optional: true - '@next/swc-linux-x64-gnu@15.2.4': + '@next/swc-linux-x64-gnu@15.3.1': optional: true - '@next/swc-linux-x64-musl@15.2.4': + '@next/swc-linux-x64-musl@15.3.1': optional: true - '@next/swc-win32-arm64-msvc@15.2.4': + '@next/swc-win32-arm64-msvc@15.3.1': optional: true - '@next/swc-win32-x64-msvc@15.2.4': + '@next/swc-win32-x64-msvc@15.3.1': optional: true - '@next/third-parties@15.2.4(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0)': + '@next/third-parties@15.3.1(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0)': dependencies: - next: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + next: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) react: 19.1.0 third-party-capital: 1.0.20 @@ -32444,7 +32656,7 @@ snapshots: dependencies: storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.3) - '@storybook/nextjs@8.6.12(esbuild@0.25.2)(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.3))(type-fest@4.39.0)(typescript@5.8.2)(webpack-hot-middleware@2.26.1)(webpack@5.98.0(esbuild@0.25.2))': + '@storybook/nextjs@8.6.12(esbuild@0.25.2)(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.3))(type-fest@4.39.0)(typescript@5.8.2)(webpack-hot-middleware@2.26.1)(webpack@5.98.0(esbuild@0.25.2))': dependencies: '@babel/core': 7.26.10 '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.10) @@ -32470,7 +32682,7 @@ snapshots: find-up: 5.0.0 image-size: 1.2.1 loader-utils: 3.3.1 - next: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + next: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) node-polyfill-webpack-plugin: 2.0.1(webpack@5.98.0(esbuild@0.25.2)) pnp-webpack-plugin: 1.7.0(typescript@5.8.2) postcss: 8.5.3 @@ -40335,6 +40547,16 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + framer-motion@12.9.2(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + motion-dom: 12.9.1 + motion-utils: 12.8.3 + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 1.3.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + framer-motion@6.5.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@motionone/dom': 10.12.0 @@ -44018,8 +44240,14 @@ snapshots: dependencies: motion-utils: 12.6.3 + motion-dom@12.9.1: + dependencies: + motion-utils: 12.8.3 + motion-utils@12.6.3: {} + motion-utils@12.8.3: {} + motion@10.16.2: dependencies: '@motionone/animation': 10.18.0 @@ -44029,9 +44257,9 @@ snapshots: '@motionone/utils': 10.18.0 '@motionone/vue': 10.16.4 - motion@12.6.3(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + motion@12.9.2(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - framer-motion: 12.6.3(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + framer-motion: 12.9.2(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.3.1 @@ -44168,9 +44396,9 @@ snapshots: dependencies: ansi-regex: 2.1.1 - next-seo@5.15.0(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + next-seo@5.15.0(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - next: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + next: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -44181,9 +44409,9 @@ snapshots: next-tick@1.1.0: {} - next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1): + next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1): dependencies: - '@next/env': 15.2.4 + '@next/env': 15.3.1 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 @@ -44193,17 +44421,17 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.2.4 - '@next/swc-darwin-x64': 15.2.4 - '@next/swc-linux-arm64-gnu': 15.2.4 - '@next/swc-linux-arm64-musl': 15.2.4 - '@next/swc-linux-x64-gnu': 15.2.4 - '@next/swc-linux-x64-musl': 15.2.4 - '@next/swc-win32-arm64-msvc': 15.2.4 - '@next/swc-win32-x64-msvc': 15.2.4 + '@next/swc-darwin-arm64': 15.3.1 + '@next/swc-darwin-x64': 15.3.1 + '@next/swc-linux-arm64-gnu': 15.3.1 + '@next/swc-linux-arm64-musl': 15.3.1 + '@next/swc-linux-x64-gnu': 15.3.1 + '@next/swc-linux-x64-musl': 15.3.1 + '@next/swc-win32-arm64-msvc': 15.3.1 + '@next/swc-win32-x64-msvc': 15.3.1 '@opentelemetry/api': 1.9.0 sass: 1.86.1 - sharp: 0.33.5 + sharp: 0.34.1 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -44383,12 +44611,12 @@ snapshots: bn.js: 4.11.6 strip-hex-prefix: 1.0.0 - nuqs@2.4.1(next@15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0): + nuqs@2.4.1(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(react@19.1.0): dependencies: mitt: 3.0.1 react: 19.1.0 optionalDependencies: - next: 15.2.4(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) + next: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1) nwsapi@2.2.20: {} @@ -46806,6 +47034,34 @@ snapshots: '@img/sharp-win32-ia32': 0.33.5 '@img/sharp-win32-x64': 0.33.5 + sharp@0.34.1: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.1 + '@img/sharp-darwin-x64': 0.34.1 + '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-linux-arm': 0.34.1 + '@img/sharp-linux-arm64': 0.34.1 + '@img/sharp-linux-s390x': 0.34.1 + '@img/sharp-linux-x64': 0.34.1 + '@img/sharp-linuxmusl-arm64': 0.34.1 + '@img/sharp-linuxmusl-x64': 0.34.1 + '@img/sharp-wasm32': 0.34.1 + '@img/sharp-win32-ia32': 0.34.1 + '@img/sharp-win32-x64': 0.34.1 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 570dbff067..a3e9caf68c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -58,7 +58,7 @@ catalog: "@floating-ui/react": ^0.27.6 "@headlessui/react": ^2.2.0 "@heroicons/react": ^2.2.0 - "@next/third-parties": ^15.2.4 + "@next/third-parties": ^15.3.1 "@phosphor-icons/react": ^2.1.7 "@pythnetwork/client": ^2.22.1 "@pythnetwork/pyth-sdk-solidity": ^4.0.0 @@ -102,8 +102,8 @@ catalog: lightweight-charts: ^5.0.5 lucide-react: ^0.487.0 modern-normalize: ^3.0.1 - motion: ^12.6.3 - next: ^15.2.4 + motion: ^12.9.2 + next: ^15.3.1 next-themes: ^0.4.6 nuqs: ^2.4.1 pino: ^9.6.0