diff --git a/packages/interwovenkit-react/src/components/LoadMoreButton.module.css b/packages/interwovenkit-react/src/components/LoadMoreButton.module.css index 13223f4d..e212db32 100644 --- a/packages/interwovenkit-react/src/components/LoadMoreButton.module.css +++ b/packages/interwovenkit-react/src/components/LoadMoreButton.module.css @@ -10,11 +10,12 @@ transition: color var(--transition) ease; - &:hover { + &:hover:not([aria-disabled="true"]) { color: var(--gray-1); } - &:disabled { - color: var(--gray-6); + /* Loading keeps the label at full contrast; the spinner carries the state. */ + &[aria-disabled="true"] { + cursor: default; } } diff --git a/packages/interwovenkit-react/src/components/LoadMoreButton.tsx b/packages/interwovenkit-react/src/components/LoadMoreButton.tsx index d2d90438..1cc60759 100644 --- a/packages/interwovenkit-react/src/components/LoadMoreButton.tsx +++ b/packages/interwovenkit-react/src/components/LoadMoreButton.tsx @@ -1,21 +1,56 @@ -import { useEffect } from "react" +import clsx from "clsx" +import { useEffect, useEffectEvent } from "react" import { useInView } from "react-intersection-observer" import { IconChevronDown } from "@initia/icons-react" +import Loader from "./Loader" +import { usePageScrollRoot } from "./PageScrollContext" import styles from "./LoadMoreButton.module.css" -const LoadMoreButton = ({ onClick, disabled }: { onClick: () => void; disabled?: boolean }) => { - const { ref, inView } = useInView() +interface Props { + onClick: () => void + isLoading?: boolean + className?: string +} + +const LoadMoreButton = ({ onClick, isLoading, className }: Props) => { + const scrollRoot = usePageScrollRoot() + + // Load ahead of the button becoming visible so scrolling feels seamless. `root` must be + // the actual scrollable ancestor: rootMargin only expands the root's own rect, while + // intermediate scroll containers clip the target with no margin. Left at the default + // viewport root, the enclosing Scrollable would clip it and the lookahead would be lost. + const { ref, inView } = useInView({ root: scrollRoot, rootMargin: "200px 0px" }) + + const onVisible = useEffectEvent(() => { + if (!isLoading) onClick() + }) + // Only inView transitions load a page, so once a page is too short to push the button + // past the margin band, inView stays true and auto-loading stops. Clicking takes over + // from there, which is also what keeps a tail of short pages from loading all at once. useEffect(() => { - if (inView) { - onClick() - } - }, [inView, onClick]) + if (inView) onVisible() + }, [inView]) return ( - ) } diff --git a/packages/interwovenkit-react/src/components/Page.tsx b/packages/interwovenkit-react/src/components/Page.tsx index 427fc765..7364d111 100644 --- a/packages/interwovenkit-react/src/components/Page.tsx +++ b/packages/interwovenkit-react/src/components/Page.tsx @@ -1,5 +1,7 @@ +import { useState } from "react" import { IconBack } from "@initia/icons-react" import { Link, useHistory } from "@/lib/router" +import { PageScrollContext } from "./PageScrollContext" import Scrollable from "./Scrollable" import styles from "./Page.module.css" @@ -13,6 +15,7 @@ interface Props { const Page = ({ title, backButton, extra, children }: PropsWithChildren) => { const history = useHistory() + const [scrollRoot, setScrollRoot] = useState(null) return ( <> @@ -31,7 +34,9 @@ const Page = ({ title, backButton, extra, children }: PropsWithChildren) {extra} - {children} + + {children} + ) } diff --git a/packages/interwovenkit-react/src/components/PageScrollContext.tsx b/packages/interwovenkit-react/src/components/PageScrollContext.tsx new file mode 100644 index 00000000..8d9f6b82 --- /dev/null +++ b/packages/interwovenkit-react/src/components/PageScrollContext.tsx @@ -0,0 +1,7 @@ +import { createContext, useContext } from "react" + +export const PageScrollContext = createContext(null) + +export function usePageScrollRoot() { + return useContext(PageScrollContext) +} diff --git a/packages/interwovenkit-react/src/components/Scrollable.tsx b/packages/interwovenkit-react/src/components/Scrollable.tsx index d82e29ab..40c057b7 100644 --- a/packages/interwovenkit-react/src/components/Scrollable.tsx +++ b/packages/interwovenkit-react/src/components/Scrollable.tsx @@ -1,11 +1,11 @@ import clsx from "clsx" import styles from "./Scrollable.module.css" -import type { PropsWithChildren, RefObject } from "react" +import type { PropsWithChildren, Ref } from "react" interface Props { className?: string - ref?: RefObject + ref?: Ref } const Scrollable = ({ className, children, ref }: PropsWithChildren) => { diff --git a/packages/interwovenkit-react/src/pages/bridge/BridgeHistory.module.css b/packages/interwovenkit-react/src/pages/bridge/BridgeHistory.module.css index f0e59596..4731b0b4 100644 --- a/packages/interwovenkit-react/src/pages/bridge/BridgeHistory.module.css +++ b/packages/interwovenkit-react/src/pages/bridge/BridgeHistory.module.css @@ -33,3 +33,7 @@ display: grid; gap: 10px; } + +.loadMore { + margin: -8px 0 16px; +} diff --git a/packages/interwovenkit-react/src/pages/bridge/BridgeHistory.tsx b/packages/interwovenkit-react/src/pages/bridge/BridgeHistory.tsx index bc11bfc0..f16c9934 100644 --- a/packages/interwovenkit-react/src/pages/bridge/BridgeHistory.tsx +++ b/packages/interwovenkit-react/src/pages/bridge/BridgeHistory.tsx @@ -75,7 +75,7 @@ const BridgeHistory = () => { )} {filteredHistory.length > page * BRIDGE_HISTORY_LIMIT_PER_PAGE ? ( - setPage((page) => page + 1)} /> + setPage((page) => page + 1)} className={styles.loadMore} /> ) : ( history.length >= BRIDGE_HISTORY_LIMIT && ( diff --git a/packages/interwovenkit-react/src/pages/bridge/op/WithdrawalList.tsx b/packages/interwovenkit-react/src/pages/bridge/op/WithdrawalList.tsx index 152eddef..65ae9364 100644 --- a/packages/interwovenkit-react/src/pages/bridge/op/WithdrawalList.tsx +++ b/packages/interwovenkit-react/src/pages/bridge/op/WithdrawalList.tsx @@ -14,7 +14,7 @@ const WithdrawalList = ({ chain }: { chain: NormalizedChain }) => { const executorUrl = chain.metadata?.executor_uri if (!executorUrl) throw new Error("Executor URL is not defined") - const { data, fetchNextPage, hasNextPage, isFetching } = useWithdrawals(executorUrl) + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useWithdrawals(executorUrl) const list = useMemo(() => data?.pages.flat() ?? [], [data]) const { syncReminders } = useClaimableReminders() @@ -47,7 +47,9 @@ const WithdrawalList = ({ chain }: { chain: NormalizedChain }) => { ) })} - {hasNextPage && fetchNextPage()} disabled={isFetching} />} + {hasNextPage && ( + fetchNextPage()} isLoading={isFetchingNextPage} /> + )} ) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 539caa39..bce17c39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,7 +151,7 @@ importers: version: 1.8.0 '@privy-io/cross-app-connect': specifier: 0.2.3 - version: 0.2.3(@rainbow-me/rainbowkit@2.2.9(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.2(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(bufferutil@4.1.0)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.10)(@types/react@19.2.5)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + version: 0.2.3(@rainbow-me/rainbowkit@2.2.9(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.2(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(bufferutil@4.1.0)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.10)(@types/react@19.2.5)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@rainbow-me/rainbowkit': specifier: ^2.2.9 version: 2.2.9(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.2(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(bufferutil@4.1.0)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) @@ -205,7 +205,7 @@ importers: version: 7.66.0(react@19.2.0) react-intersection-observer: specifier: ^10.0.0 - version: 10.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 10.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-merge-refs: specifier: ^3.0.2 version: 3.0.2(react@19.2.0) @@ -5261,8 +5261,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-intersection-observer@10.0.0: - resolution: {integrity: sha512-JJRgcnFQoVXmbE5+GXr1OS1NDD1gHk0HyfpLcRf0575IbJz+io8yzs4mWVlfaqOQq1FiVjLvuYAdEEcrrCfveg==} + react-intersection-observer@10.1.0: + resolution: {integrity: sha512-V8HDu3+Llg6OEhOxx8LnUSS0t4VS+1Xk9ZatkI8Jct/H0CwKnqFTCu8NT3q7ghJTghTdIrEMPSWr2dkKPG+gdQ==} peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -7839,7 +7839,7 @@ snapshots: '@paulmillr/qr@0.2.1': {} - '@privy-io/cross-app-connect@0.2.3(@rainbow-me/rainbowkit@2.2.9(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.2(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(bufferutil@4.1.0)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.10)(@types/react@19.2.5)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@privy-io/cross-app-connect@0.2.3(@rainbow-me/rainbowkit@2.2.9(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.2(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(bufferutil@4.1.0)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)))(@wagmi/core@2.21.1(@tanstack/query-core@5.90.10)(@types/react@19.2.5)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@noble/curves': 1.9.7 '@noble/hashes': 1.3.2 @@ -7848,7 +7848,7 @@ snapshots: viem: 2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: '@rainbow-me/rainbowkit': 2.2.9(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.17.2(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@19.2.0))(@types/react@19.2.5)(bufferutil@4.1.0)(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) - '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.10)(@types/react@19.2.5)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@wagmi/core': 2.21.1(@tanstack/query-core@5.90.10)(@types/react@19.2.5)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.0))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@radix-ui/number@1.1.1': {} @@ -9704,22 +9704,6 @@ snapshots: - react - use-sync-external-store - '@wagmi/core@2.21.1(@tanstack/query-core@5.90.10)(@types/react@19.2.5)(react@19.2.0)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.0))(viem@2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': - dependencies: - eventemitter3: 5.0.1 - mipd: 0.0.7(typescript@5.9.3) - viem: 2.39.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.0(@types/react@19.2.5)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) - optionalDependencies: - '@tanstack/query-core': 5.90.10 - typescript: 5.9.3 - transitivePeerDependencies: - - '@types/react' - - immer - - react - - use-sync-external-store - optional: true - '@wallet-standard/base@1.1.0': {} '@wallet-standard/features@1.1.0': @@ -13028,7 +13012,7 @@ snapshots: dependencies: react: 19.2.0 - react-intersection-observer@10.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-intersection-observer@10.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 optionalDependencies: @@ -14366,13 +14350,6 @@ snapshots: react: 19.2.0 use-sync-external-store: 1.4.0(react@19.2.0) - zustand@5.0.0(@types/react@19.2.5)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)): - optionalDependencies: - '@types/react': 19.2.5 - react: 19.2.0 - use-sync-external-store: 1.6.0(react@19.2.0) - optional: true - zustand@5.0.3(@types/react@19.2.5)(react@19.2.0)(use-sync-external-store@1.4.0(react@19.2.0)): optionalDependencies: '@types/react': 19.2.5