diff --git a/app/autovault/[chainId]/[vaultAddress]/components/DepositToVaultModal.tsx b/app/autovault/[chainId]/[vaultAddress]/components/DepositToVaultModal.tsx index f6a1ddaf..d52dfd59 100644 --- a/app/autovault/[chainId]/[vaultAddress]/components/DepositToVaultModal.tsx +++ b/app/autovault/[chainId]/[vaultAddress]/components/DepositToVaultModal.tsx @@ -1,8 +1,10 @@ +"use client"; + import React from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; import { Address } from 'viem'; import { useAccount } from 'wagmi'; import { Button } from '@/components/common'; +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import Input from '@/components/Input/Input'; import AccountConnect from '@/components/layout/header/AccountConnect'; import { TokenIcon } from '@/components/TokenIcon'; @@ -62,98 +64,87 @@ export function DepositToVaultModal({ return ( <> -
{ + if (!open) onClose(); + }} + size="lg" + scrollBehavior="inside" + backdrop="blur" > -
-
- - -
-
-
- - Deposit {assetSymbol} -
- Deposit to {vaultName} -
+ + } + onClose={onClose} + /> + + {!isConnected ? ( +
+
+ ) : ( +
+
+
+ Deposit amount +

+ Balance: {formatBalance(tokenBalance ?? BigInt(0), assetDecimals)} {assetSymbol} +

+
- {!isConnected ? ( -
- -
- ) : ( - <> - {/* Deposit Input Section */} -
-
-
- Deposit amount -

- Balance: {formatBalance(tokenBalance ?? BigInt(0), assetDecimals)}{' '} - {assetSymbol} +

+
+ + {inputError && ( +

+ {inputError}

-
- -
-
- - {inputError && ( -

- {inputError} -

- )} -
- - {!permit2Authorized || (!usePermit2Setting && !isApproved) ? ( - - ) : ( - - )} -
+ )}
+ + {!permit2Authorized || (!usePermit2Setting && !isApproved) ? ( + + ) : ( + + )}
- - )} -
-
-
+
+
+ )} + + {showProcessModal && ( - - - - -
-

Deposit {assetSymbol}

-

- Depositing {formattedAmount} {assetSymbol} to {vaultName} -

- - {/* Steps */} -
- {steps.map((step) => { - const status = getStepStatus(step.key); - return ( -
-
- {status === 'done' ? ( - - ) : status === 'current' ? ( - - ) : ( - - )} -
-
-
{step.label}
-
{step.detail}
-
-
- ); - })} + { + if (!open) onClose(); + }} + size="lg" + isDismissable={false} + backdrop="blur" + > + } + onClose={onClose} + /> + + {steps.map((step) => { + const status = getStepStatus(step.key); + return ( +
+
+ {status === 'done' ? ( + + ) : status === 'current' ? ( + + ) : ( + + )} +
+
+
{step.label}
+
{step.detail}
+
-
- - - + ); + })} + + ); } diff --git a/app/autovault/[chainId]/[vaultAddress]/components/VaultInitializationModal.tsx b/app/autovault/[chainId]/[vaultAddress]/components/VaultInitializationModal.tsx index b6cce01c..fdd076c5 100644 --- a/app/autovault/[chainId]/[vaultAddress]/components/VaultInitializationModal.tsx +++ b/app/autovault/[chainId]/[vaultAddress]/components/VaultInitializationModal.tsx @@ -1,9 +1,12 @@ +"use client"; + import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/react'; +import { FiZap } from 'react-icons/fi'; import { Address, zeroAddress } from 'viem'; import { Button } from '@/components/common'; import { AddressDisplay } from '@/components/common/AddressDisplay'; import { AllocatorCard } from '@/components/common/AllocatorCard'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { Spinner } from '@/components/common/Spinner'; import { useDeployMorphoMarketV1Adapter } from '@/hooks/useDeployMorphoMarketV1Adapter'; import { useVaultV2 } from '@/hooks/useVaultV2'; @@ -194,7 +197,7 @@ function AgentSelectionStep({ export function VaultInitializationModal({ isOpen, - onClose, + onOpenChange, vaultAddress, marketAdapter, // address of MorphoMakretV1Aapater marketAdapterLoading, // @@ -206,7 +209,7 @@ export function VaultInitializationModal({ marketAdapter: Address; marketAdapterLoading: boolean; refetchMarketAdapter: () => void; - onClose: () => void; + onOpenChange: (open: boolean) => void; vaultAddress: Address; chainId: SupportedNetworks; onAdapterConfigured: () => void; @@ -260,14 +263,14 @@ export function VaultInitializationModal({ return; } onAdapterConfigured(); - onClose(); + onOpenChange(false); } catch (error) { console.error('Failed to complete initialization', error); } }, [ completeInitialization, onAdapterConfigured, - onClose, + onOpenChange, registryAddress, selectedAgent, marketAdapter, @@ -394,27 +397,22 @@ export function VaultInitializationModal({ return ( - - -
-

{stepTitle}

-

- {stepIndex < 3 - ? 'Complete these steps to activate your vault.' - : 'Optionally choose an agent now, or configure later in settings.'} -

-
-
- - + } + onClose={() => onOpenChange(false)} + /> + {currentStep === 'deploy' && ( )} - - - - {showBackButton && ( - - )} - {renderCta()} - -
- -
-
+ + + {showBackButton && ( + + )} + {renderCta()} + +
+ +
); } diff --git a/app/autovault/[chainId]/[vaultAddress]/components/VaultSettingsModal.tsx b/app/autovault/[chainId]/[vaultAddress]/components/VaultSettingsModal.tsx index 0da90183..78fb7be4 100644 --- a/app/autovault/[chainId]/[vaultAddress]/components/VaultSettingsModal.tsx +++ b/app/autovault/[chainId]/[vaultAddress]/components/VaultSettingsModal.tsx @@ -1,8 +1,10 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +"use client"; + +import { useCallback, useEffect, useState } from 'react'; import { ReloadIcon } from '@radix-ui/react-icons'; -import { createPortal } from 'react-dom'; -import { LuX } from 'react-icons/lu'; +import { FiSettings } from 'react-icons/fi'; import { Address } from 'viem'; +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { VaultV2Cap } from '@/data-sources/morpho-api/v2-vaults'; import { CapData } from '@/hooks/useVaultV2Data'; import { SupportedNetworks } from '@/utils/networks'; @@ -16,7 +18,7 @@ const TABS: { id: SettingsTab; label: string }[] = [ type VaultSettingsModalProps = { isOpen: boolean; - onClose: () => void; + onOpenChange: (open: boolean) => void; initialTab?: SettingsTab; isOwner: boolean; onUpdateMetadata: (values: { name?: string; symbol?: string }) => Promise; @@ -43,7 +45,7 @@ type VaultSettingsModalProps = { export function VaultSettingsModal({ isOpen, - onClose, + onOpenChange, initialTab = 'general', isOwner, onUpdateMetadata, @@ -68,52 +70,13 @@ export function VaultSettingsModal({ isRefreshing = false, }: VaultSettingsModalProps) { const [activeTab, setActiveTab] = useState(initialTab); - const [mounted, setMounted] = useState(false); - const wasOpenRef = useRef(false); - // Reset to initial tab when modal opens useEffect(() => { - const wasOpen = wasOpenRef.current; - - if (isOpen && !wasOpen) { + if (isOpen) { setActiveTab(initialTab); } - - wasOpenRef.current = isOpen; }, [initialTab, isOpen]); - // Handle mounting - useEffect(() => { - setMounted(true); - }, []); - - // Prevent body scroll when modal is open - useEffect(() => { - if (!isOpen) return; - const originalOverflow = document.body.style.overflow; - document.body.style.overflow = 'hidden'; - return () => { - document.body.style.overflow = originalOverflow; - }; - }, [isOpen]); - - // Handle ESC key - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape' && isOpen) { - onClose(); - } - }; - - if (isOpen) { - window.addEventListener('keydown', handleKeyDown); - } - - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [isOpen, onClose]); - const handleTabChange = useCallback((tab: SettingsTab) => { setActiveTab(tab); }, []); @@ -163,75 +126,67 @@ export function VaultSettingsModal({ } }; - if (!mounted || !isOpen) { + if (!isOpen) { return null; } - return createPortal( -
-
event.stopPropagation()} - > -
- {/* Header */} -
-

Vault Settings

-
- {onRefresh && ( + } + onClose={() => onOpenChange(false)} + auxiliaryAction={ + onRefresh + ? { + icon: ( + + ), + onClick: () => { + if (!isRefreshing) { + onRefresh(); + } + }, + ariaLabel: 'Refresh vault data', + } + : undefined + } + /> + +
+
+
+ {TABS.map((tab) => ( - )} - + ))}
- {/* Content */} -
- {/* Sidebar */} - - - {/* Tab Content */} -
-
{renderActiveTab()}
-
+
+
{renderActiveTab()}
-
-
, - document.body, + + ); } diff --git a/app/autovault/[chainId]/[vaultAddress]/components/settings/AddMarketCapModal.tsx b/app/autovault/[chainId]/[vaultAddress]/components/settings/AddMarketCapModal.tsx index b1a7d599..f195f7db 100644 --- a/app/autovault/[chainId]/[vaultAddress]/components/settings/AddMarketCapModal.tsx +++ b/app/autovault/[chainId]/[vaultAddress]/components/settings/AddMarketCapModal.tsx @@ -1,4 +1,7 @@ +"use client"; + import { Address } from 'viem'; + import { MarketSelectionModal } from '@/components/common/MarketSelectionModal'; import { SupportedNetworks } from '@/utils/networks'; import { Market } from '@/utils/types'; @@ -7,7 +10,7 @@ type AddMarketCapModalProps = { vaultAsset: Address; chainId: SupportedNetworks; existingMarketIds: Set; - onClose: () => void; + onOpenChange: (open: boolean) => void; onAdd: (markets: Market[]) => void; }; @@ -19,7 +22,7 @@ export function AddMarketCapModal({ vaultAsset, chainId, existingMarketIds, - onClose, + onOpenChange, onAdd, }: AddMarketCapModalProps) { return ( @@ -30,7 +33,7 @@ export function AddMarketCapModal({ chainId={chainId} excludeMarketIds={existingMarketIds} multiSelect - onClose={onClose} + onOpenChange={onOpenChange} onSelect={onAdd} confirmButtonText={undefined} // Use default dynamic text /> diff --git a/app/autovault/[chainId]/[vaultAddress]/components/settings/EditCaps.tsx b/app/autovault/[chainId]/[vaultAddress]/components/settings/EditCaps.tsx index faab78dd..bffce5e8 100644 --- a/app/autovault/[chainId]/[vaultAddress]/components/settings/EditCaps.tsx +++ b/app/autovault/[chainId]/[vaultAddress]/components/settings/EditCaps.tsx @@ -570,7 +570,7 @@ export function EditCaps({ vaultAsset={vaultAsset} chainId={chainId} existingMarketIds={existingMarketIds} - onClose={() => setShowAddMarketModal(false)} + onOpenChange={setShowAddMarketModal} onAdd={handleAddMarkets} /> )} diff --git a/app/autovault/[chainId]/[vaultAddress]/content.tsx b/app/autovault/[chainId]/[vaultAddress]/content.tsx index 41ba63b6..8b8c26bd 100644 --- a/app/autovault/[chainId]/[vaultAddress]/content.tsx +++ b/app/autovault/[chainId]/[vaultAddress]/content.tsx @@ -299,7 +299,7 @@ export default function VaultContent() { {/* Settings Modal */} setShowSettings(false)} + onOpenChange={setShowSettings} initialTab={settingsTab} isOwner={vault.isOwner} onUpdateMetadata={handleUpdateMetadata} @@ -330,7 +330,7 @@ export default function VaultContent() { {networkConfig?.vaultConfig?.marketV1AdapterFactory && ( setShowInitializationModal(false)} + onOpenChange={setShowInitializationModal} vaultAddress={vaultAddressValue} chainId={chainId} marketAdapter={vault.adapter} diff --git a/app/autovault/components/AutovaultContent.tsx b/app/autovault/components/AutovaultContent.tsx index 41fb7b75..01e6152b 100644 --- a/app/autovault/components/AutovaultContent.tsx +++ b/app/autovault/components/AutovaultContent.tsx @@ -84,7 +84,7 @@ export default function AutovaultContent() { {/* Deployment Modal */} setShowDeploymentModal(false)} + onOpenChange={setShowDeploymentModal} existingVaults={vaults} />
diff --git a/app/autovault/components/deployment/DeploymentModal.tsx b/app/autovault/components/deployment/DeploymentModal.tsx index 721547f4..ecd78694 100644 --- a/app/autovault/components/deployment/DeploymentModal.tsx +++ b/app/autovault/components/deployment/DeploymentModal.tsx @@ -1,7 +1,10 @@ +"use client"; + import { useEffect, useMemo, useState } from 'react'; -import { Checkbox, Modal, ModalContent, ModalHeader } from '@heroui/react'; -import { RxCross2 } from 'react-icons/rx'; +import { Checkbox } from '@heroui/react'; +import { FaCube } from 'react-icons/fa'; import { Button } from '@/components/common'; +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { Spinner } from '@/components/common/Spinner'; import { useMarkets } from '@/contexts/MarketsContext'; import { UserVaultV2 } from '@/data-sources/subgraph/v2-vaults'; @@ -16,11 +19,12 @@ const VAULT_SUPPORTED_NETWORKS: SupportedNetworks[] = ALL_SUPPORTED_NETWORKS.fil type DeploymentModalContentProps = { isOpen: boolean; - onClose: () => void; + + onOpenChange: (open: boolean) => void existingVaults: UserVaultV2[]; }; -function DeploymentModalContent({ isOpen, onClose, existingVaults }: DeploymentModalContentProps) { +function DeploymentModalContent({ isOpen, onOpenChange, existingVaults }: DeploymentModalContentProps) { const { selectedTokenAndNetwork, needSwitchChain, switchToNetwork, createVault, isDeploying } = useDeployment(); // Load balances and tokens at modal level @@ -54,29 +58,21 @@ function DeploymentModalContent({ isOpen, onClose, existingVaults }: DeploymentM return ( - - -
-

Deploy Autovault

-

Choose the token and network for your autovault

-
- -
- -
+ } + onClose={() => onOpenChange(false)} + /> + + +
@@ -90,7 +86,7 @@ function DeploymentModalContent({ isOpen, onClose, existingVaults }: DeploymentM
{selectedTokenAndNetwork && ( -
+
You can configure the vault to have caps, automation agents and more after you deploy the vault.
)} @@ -100,7 +96,7 @@ function DeploymentModalContent({ isOpen, onClose, existingVaults }: DeploymentM @@ -143,21 +139,21 @@ function DeploymentModalContent({ isOpen, onClose, existingVaults }: DeploymentM
- + ); } type DeploymentModalProps = { isOpen: boolean; - onClose: () => void; + onOpenChange: (open: boolean) => void; existingVaults: UserVaultV2[]; }; -export function DeploymentModal({ isOpen, onClose, existingVaults }: DeploymentModalProps) { +export function DeploymentModal({ isOpen, onOpenChange, existingVaults }: DeploymentModalProps) { return ( - + ); } diff --git a/app/market/[chainId]/[marketid]/components/CampaignModal.tsx b/app/market/[chainId]/[marketid]/components/CampaignModal.tsx index 1c8290e4..f25fd854 100644 --- a/app/market/[chainId]/[marketid]/components/CampaignModal.tsx +++ b/app/market/[chainId]/[marketid]/components/CampaignModal.tsx @@ -1,9 +1,10 @@ 'use client'; -import { Cross1Icon, ExternalLinkIcon } from '@radix-ui/react-icons'; +import { ExternalLinkIcon } from '@radix-ui/react-icons'; import Image from 'next/image'; import Link from 'next/link'; import { Button } from '@/components/common/Button'; +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { getMerklCampaignURL } from '@/utils/external'; import { SimplifiedCampaign } from '@/utils/merklTypes'; @@ -81,35 +82,26 @@ export function CampaignModal({ isOpen, onClose, campaigns }: CampaignModalProps if (!isOpen) return null; return ( -
{ + if (!open) onClose(); + }} + size="2xl" + scrollBehavior="inside" + backdrop="blur" > -
-
- - -
-
-
- Active Reward Campaigns -
-
-
- -
- {campaigns.map((campaign) => ( - - ))} -
-
-
-
+ } + onClose={onClose} + /> + + {campaigns.map((campaign) => ( + + ))} + + ); } diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index 7ed063cc..bbfbbc5e 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -213,7 +213,7 @@ function MarketContent() { {showSupplyModal && ( setShowSupplyModal(false)} + onOpenChange={setShowSupplyModal} position={userPosition} isMarketPage refetch={handleRefreshAllSync} @@ -223,7 +223,7 @@ function MarketContent() { {showBorrowModal && ( setShowBorrowModal(false)} + onOpenChange={setShowBorrowModal} oraclePrice={oraclePrice} refetch={handleRefreshAllSync} isRefreshing={isRefreshing} diff --git a/app/markets/components/BlacklistConfirmationModal.tsx b/app/markets/components/BlacklistConfirmationModal.tsx index 6ad20c9b..cbe72f00 100644 --- a/app/markets/components/BlacklistConfirmationModal.tsx +++ b/app/markets/components/BlacklistConfirmationModal.tsx @@ -1,22 +1,22 @@ 'use client'; import React from 'react'; -import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@heroui/react'; import { IoWarningOutline } from 'react-icons/io5'; import { Button } from '@/components/common'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { MarketIdentity } from '@/components/MarketIdentity'; import { Market } from '@/utils/types'; type BlacklistConfirmationModalProps = { isOpen: boolean; - onClose: () => void; + onOpenChange: (open: boolean) => void; onConfirm: () => void; market: Market | null; }; export function BlacklistConfirmationModal({ isOpen, - onClose, + onOpenChange, onConfirm, market, }: BlacklistConfirmationModalProps) { @@ -24,32 +24,25 @@ export function BlacklistConfirmationModal({ const handleConfirm = () => { onConfirm(); - onClose(); + onOpenChange(false); }; return ( - - - - Blacklist Market - - + } + title="Blacklist Market" + description="Confirm removal of this market from your view" + className="border-b border-primary/10" + onClose={() => onOpenChange(false)} + /> +
-

- Are you sure you want to blacklist this market? -

-
@@ -63,17 +56,16 @@ export function BlacklistConfirmationModal({ blacklisted markets later in Settings.

-
- - - - - - +
+
+ + + +
); } diff --git a/app/markets/components/MarketActionsDropdown.tsx b/app/markets/components/MarketActionsDropdown.tsx index 9ca4bfb0..cee03052 100644 --- a/app/markets/components/MarketActionsDropdown.tsx +++ b/app/markets/components/MarketActionsDropdown.tsx @@ -144,7 +144,7 @@ export function MarketActionsDropdown({ setIsConfirmModalOpen(false)} + onOpenChange={setIsConfirmModalOpen} onConfirm={handleConfirmBlacklist} market={market} /> diff --git a/app/markets/components/MarketSettingsModal.tsx b/app/markets/components/MarketSettingsModal.tsx index c2b55c2c..e38206d3 100644 --- a/app/markets/components/MarketSettingsModal.tsx +++ b/app/markets/components/MarketSettingsModal.tsx @@ -1,15 +1,9 @@ import React from 'react'; -import { - Modal, - ModalContent, - ModalHeader, - ModalBody, - ModalFooter, - Input, - Divider, -} from '@heroui/react'; +import { Input, Divider } from '@heroui/react'; +import { FiSliders } from 'react-icons/fi'; import { Button } from '@/components/common'; import { IconSwitch } from '@/components/common/IconSwitch'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { TrustedByCell } from '@/components/vaults/TrustedVaultBadges'; import { defaultTrustedVaults, type TrustedVault } from '@/constants/vaults/known_vaults'; import { useMarkets } from '@/hooks/useMarkets'; @@ -22,7 +16,7 @@ import { type MarketSettingsModalProps = { isOpen: boolean; - onOpenChange: () => void; + onOpenChange: (isOpen: boolean) => void; usdFilters: { minSupply: string; minBorrow: string; @@ -94,18 +88,17 @@ export default function MarketSettingsModal({ onOpenChange={onOpenChange} backdrop="blur" size="xl" - classNames={{ wrapper: 'z-[2300]', backdrop: 'z-[2290]' }} + zIndex="settings" > - - {(onClose) => ( - <> - - Market Preferences - - Fine-tune filter thresholds, pagination, and column visibility. - - - + {(onClose) => ( + <> + } + onClose={onClose} + /> +

Filter Thresholds

@@ -263,15 +256,14 @@ export default function MarketSettingsModal({

-
- - - - - )} - + + + + + + )} ); } diff --git a/app/markets/components/markets.tsx b/app/markets/components/markets.tsx index fef52b9c..24a0575e 100644 --- a/app/markets/components/markets.tsx +++ b/app/markets/components/markets.tsx @@ -462,7 +462,7 @@ export default function Markets({

Markets

{showSupplyModal && selectedMarket && ( - setShowSupplyModal(false)} /> + )} setIsTrustedVaultsModalOpen((prev) => !prev)} + onOpenChange={setIsTrustedVaultsModalOpen} userTrustedVaults={userTrustedVaults} setUserTrustedVaults={setUserTrustedVaults} /> diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index 2d742fef..71d45b41 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -18,7 +18,7 @@ import { SupplyModalV2 } from '@/components/SupplyModalV2'; import { useMarkets } from '@/hooks/useMarkets'; import useUserPositionsSummaryData from '@/hooks/useUserPositionsSummaryData'; import { MarketPosition } from '@/utils/types'; -import { OnboardingModal } from './onboarding/Modal'; +import { OnboardingModal } from './onboarding/OnboardingModal'; import { PositionsSummaryTable } from './PositionsSummaryTable'; export default function Positions() { @@ -99,10 +99,7 @@ export default function Positions() { { - setShowWithdrawModal(false); - setSelectedPosition(null); - }} + onOpenChange={setShowWithdrawModal} refetch={() => void refetch()} isMarketPage={false} defaultMode="withdraw" @@ -113,10 +110,7 @@ export default function Positions() { { - setShowSupplyModal(false); - setSelectedPosition(null); - }} + onOpenChange={setShowSupplyModal} refetch={() => void refetch()} isMarketPage={false} /> @@ -124,7 +118,7 @@ export default function Positions() { setShowOnboardingModal(false)} + onOpenChange={setShowOnboardingModal} /> {loading ? ( diff --git a/app/positions/components/PositionsSummaryTable.tsx b/app/positions/components/PositionsSummaryTable.tsx index b7d57a03..7dc01503 100644 --- a/app/positions/components/PositionsSummaryTable.tsx +++ b/app/positions/components/PositionsSummaryTable.tsx @@ -474,7 +474,7 @@ export function PositionsSummaryTable({ {showRebalanceModal && selectedGroupedPosition && ( setShowRebalanceModal(false)} + onOpenChange={setShowRebalanceModal} isOpen={showRebalanceModal} refetch={refetch} isRefetching={isRefetching} diff --git a/app/positions/components/RebalanceModal.tsx b/app/positions/components/RebalanceModal.tsx index feb5b1c6..89b5b25d 100644 --- a/app/positions/components/RebalanceModal.tsx +++ b/app/positions/components/RebalanceModal.tsx @@ -1,10 +1,11 @@ import React, { useState, useMemo, useCallback } from 'react'; -import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@heroui/react'; import { ReloadIcon } from '@radix-ui/react-icons'; import { parseUnits, formatUnits } from 'viem'; import { Button } from '@/components/common'; import { MarketSelectionModal } from '@/components/common/MarketSelectionModal'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { Spinner } from '@/components/common/Spinner'; +import { TokenIcon } from '@/components/TokenIcon'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useMarkets } from '@/hooks/useMarkets'; @@ -20,7 +21,7 @@ import { RebalanceProcessModal } from './RebalanceProcessModal'; type RebalanceModalProps = { groupedPosition: GroupedPosition; isOpen: boolean; - onClose: () => void; + onOpenChange: (open: boolean) => void; refetch: (onSuccess?: () => void) => void; isRefetching: boolean; }; @@ -28,7 +29,7 @@ type RebalanceModalProps = { export function RebalanceModal({ groupedPosition, isOpen, - onClose, + onOpenChange, refetch, isRefetching, }: RebalanceModalProps) { @@ -280,37 +281,45 @@ export function RebalanceModal({ <> - - -
- Rebalance {groupedPosition.loanAsset ?? 'Unknown'} Position + + + Rebalance {groupedPosition.loanAsset ?? 'Unknown'} Position + {isRefetching && }
- -
- -
-

- Click on your existing position to rebalance {groupedPosition.loanAsset} to a new market. You can batch actions. -

-
+ } + description={`Click on your existing position to rebalance ${ + groupedPosition.loanAssetSymbol ?? groupedPosition.loanAsset ?? 'this token' + } to a new market. You can batch actions.`} + mainIcon={ + + } + onClose={()=>onOpenChange(false)} + auxiliaryAction={{ + icon: ( + + ), + onClick: () => { + if (!isRefetching) { + handleManualRefresh(); + } + }, + ariaLabel: 'Refresh position data', + }} + /> + - - - - - -
+ + + + +
{showProcessModal && ( setShowProcessModal(false)} + onOpenChange={setShowProcessModal} tokenSymbol={groupedPosition.loanAsset} actionsCount={rebalanceActions.length} /> @@ -384,7 +392,7 @@ export function RebalanceModal({ vaultAsset={groupedPosition.loanAssetAddress as `0x${string}`} chainId={groupedPosition.chainId} multiSelect={false} - onClose={() => setShowToModal(false)} + onOpenChange={setShowToModal} onSelect={(selectedMarkets) => { if (selectedMarkets.length > 0) { setSelectedToMarketUniqueKey(selectedMarkets[0].uniqueKey); diff --git a/app/positions/components/RebalanceProcessModal.tsx b/app/positions/components/RebalanceProcessModal.tsx index 052a523b..e2e88908 100644 --- a/app/positions/components/RebalanceProcessModal.tsx +++ b/app/positions/components/RebalanceProcessModal.tsx @@ -1,12 +1,14 @@ import React, { useMemo } from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; import { FaCheckCircle, FaCircle } from 'react-icons/fa'; +import { LuArrowRightLeft } from "react-icons/lu"; + +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { RebalanceStepType } from '@/hooks/useRebalance'; type RebalanceProcessModalProps = { currentStep: RebalanceStepType; isPermit2Flow: boolean; - onClose: () => void; + onOpenChange: (open: boolean) => void; tokenSymbol: string; actionsCount: number; }; @@ -14,7 +16,7 @@ type RebalanceProcessModalProps = { export function RebalanceProcessModal({ currentStep, isPermit2Flow, - onClose, + onOpenChange, tokenSymbol, actionsCount, }: RebalanceProcessModalProps): JSX.Element { @@ -86,52 +88,46 @@ export function RebalanceProcessModal({ }; return ( -
-
- - -
- Rebalancing {tokenSymbol} Positions -
- -
- {steps - .filter((step) => step.key !== 'idle') - .map((step, index) => ( -
-
- {getStepStatus(step.key as RebalanceStepType) === 'done' && ( - - )} - {getStepStatus(step.key as RebalanceStepType) === 'current' && ( -
- )} - {getStepStatus(step.key as RebalanceStepType) === 'undone' && ( + + } + onClose={() => onOpenChange(false)} + /> + + {steps + .filter((step) => step.key !== 'idle') + .map((step) => { + const status = getStepStatus(step.key as RebalanceStepType); + return ( +
+
+ {status === 'done' ? ( + + ) : status === 'current' ? ( +
+ ) : ( )}
-
-
{step.label}
- {currentStep === step.key && step.detail && ( -
- {step.detail} -
+
+
{step.label}
+ {status === 'current' && step.detail && ( +
{step.detail}
)}
- {index < steps.length - 2 &&
}
- ))} -
-
-
+ ); + })} +
+
); } diff --git a/app/positions/components/agent/Main.tsx b/app/positions/components/agent/Main.tsx deleted file mode 100644 index 02203e8b..00000000 --- a/app/positions/components/agent/Main.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { Tooltip } from '@heroui/react'; -import { motion } from 'framer-motion'; -import Image from 'next/image'; -import Link from 'next/link'; -import { GrStatusGood } from 'react-icons/gr'; -import { Button } from '@/components/common'; -import { TokenIcon } from '@/components/TokenIcon'; -import { TooltipContent } from '@/components/TooltipContent'; -import { useMarkets } from '@/contexts/MarketsContext'; -import { getExplorerURL } from '@/utils/external'; -import { findAgent } from '@/utils/monarch-agent'; -import { getNetworkName } from '@/utils/networks'; -import { UserRebalancerInfo } from '@/utils/types'; - -const img = require('../../../../src/imgs/agent/agent-detailed.png') as string; - -type MainProps = { - onNext: () => void; - userRebalancerInfos: UserRebalancerInfo[]; -}; - -export function Main({ onNext, userRebalancerInfos }: MainProps) { - const { markets } = useMarkets(); - - const activeAgentInfos = userRebalancerInfos - .map((info) => ({ - info, - agent: findAgent(info.rebalancer), - })) - .filter((item) => item.agent !== undefined); - - if (activeAgentInfos.length === 0) { - return ( -
-

No active agent found for the configured networks.

- -
- ); - } - - return ( -
- - Monarch Agent - - - {activeAgentInfos.map(({ info, agent }) => { - if (!agent) return null; - - const networkName = getNetworkName(info.network); - - const authorizedMarkets = markets.filter( - (market) => - market.morphoBlue.chain.id === info.network && - info.marketCaps.some( - (cap) => cap.marketId.toLowerCase() === market.uniqueKey.toLowerCase(), - ), - ); - - const loanAssetGroups = authorizedMarkets.reduce( - (acc, market) => { - const address = market.loanAsset.address.toLowerCase(); - if (!acc[address]) { - acc[address] = { - address, - chainId: market.morphoBlue.chain.id, - markets: [], - symbol: market.loanAsset.symbol, - }; - } - acc[address].markets.push(market); - return acc; - }, - {} as Record< - string, - { address: string; chainId: number; symbol: string; markets: typeof authorizedMarkets } - >, - ); - - const explorerUrl = getExplorerURL(agent.address, info.network); - - return ( -
-
-
-

{agent.name}

- - {networkName} - - - {agent.address.slice(0, 6) + '...' + agent.address.slice(-4)} - -
- } - title="Agent Active" - detail={`Agent is active on ${networkName}`} - /> - } - > -
-
- Active -
- -
- -
-
-

Strategy

-

{agent.strategyDescription}

-
- -
-

Monitoring Positions

-
- {Object.values(loanAssetGroups).map( - ({ address, chainId, markets: marketsForLoanAsset, symbol }) => { - return ( -
- - - {symbol ?? 'Unknown'} ({marketsForLoanAsset.length}) - -
- ); - }, - )} - {Object.values(loanAssetGroups).length === 0 && ( -

- No markets currently configured for this agent. -

- )} -
-
-
-
- ); - })} - - -
- ); -} diff --git a/app/positions/components/agent/SetupAgent.tsx b/app/positions/components/agent/SetupAgent.tsx deleted file mode 100644 index be40210f..00000000 --- a/app/positions/components/agent/SetupAgent.tsx +++ /dev/null @@ -1,516 +0,0 @@ -import { useState, useMemo, useCallback, useEffect } from 'react'; -import { - Checkbox, - Dropdown, - DropdownTrigger, - DropdownMenu, - DropdownItem, - SharedSelection, -} from '@heroui/react'; -import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; -import { motion, AnimatePresence } from 'framer-motion'; -import Image from 'next/image'; -import { formatUnits, maxUint256 } from 'viem'; -import { AgentSetupProcessModal } from '@/components/AgentSetupProcessModal'; -import { Button } from '@/components/common/Button'; -import { MarketInfoBlockCompact } from '@/components/common/MarketInfoBlock'; -import { TokenIcon } from '@/components/TokenIcon'; -import { MarketCap, useAuthorizeAgent } from '@/hooks/useAuthorizeAgent'; -import { findAgent, KnownAgents } from '@/utils/monarch-agent'; -import { - getNetworkName, - getNetworkImg, - SupportedNetworks, - isAgentAvailable, -} from '@/utils/networks'; -import { Market, MarketPosition, UserRebalancerInfo } from '@/utils/types'; - -type MarketGroup = { - network: SupportedNetworks; - loanAsset: { - address: string; - symbol: string; - }; - - // setup already: this includes markets that have been authorized for agent - authorizedMarkets: Market[]; - // have not setup yet, but currently acitve so should be consider priority - activeMarkets: Market[]; - historicalMarkets: Market[]; - otherMarkets: Market[]; -}; - -type SetupAgentProps = { - positions: MarketPosition[]; - allMarkets: Market[]; - userRebalancerInfos: UserRebalancerInfo[]; - pendingCaps: MarketCap[]; - addToPendingCaps: (market: Market, cap: bigint) => void; - removeFromPendingCaps: (market: Market) => void; - onBack: () => void; - onNext: () => void; - account?: string; -}; - -// Helper component for market rows -function MarketRow({ - market, - isSelected, - onToggle, - isDisabled, -}: { - market: Market; - isSelected: boolean; - onToggle: (selected: boolean) => void; - isDisabled: boolean; -}) { - return ( -
-
- !isDisabled && onToggle(selected)} - size="sm" - color="primary" - className="mr-0" - isDisabled={isDisabled} - /> - -
-
- ); -} - -export function SetupAgent({ - positions, - allMarkets, - userRebalancerInfos, - pendingCaps, - addToPendingCaps, - removeFromPendingCaps, - onBack, - onNext, -}: SetupAgentProps) { - const [hasPreselected, setHasPreselected] = useState(false); - const [expandedGroups, setExpandedGroups] = useState([]); - const [showAllMarkets, setShowAllMarkets] = useState(false); - const [showProcessModal, setShowProcessModal] = useState(false); - - const defaultNetwork = useMemo(() => { - const networks = Array.from(new Set(positions.map((p) => p.market.morphoBlue.chain.id))) - .filter(isAgentAvailable) - .sort(); - return networks[0] ?? SupportedNetworks.Base; - }, [positions]); - - const [targetNetwork, setTargetNetwork] = useState(defaultNetwork); - - const currentUserRebalancerInfo = useMemo(() => { - return userRebalancerInfos.find((info) => info.network === targetNetwork); - }, [userRebalancerInfos, targetNetwork]); - - const availableNetworks = useMemo(() => { - const networkSet = new Set(); - positions.forEach((p) => { - if (isAgentAvailable(p.market.morphoBlue.chain.id)) { - networkSet.add(p.market.morphoBlue.chain.id); - } - }); - userRebalancerInfos.forEach((info) => { - if (isAgentAvailable(info.network)) { - networkSet.add(info.network); - } - }); - return Array.from(networkSet).sort(); - }, [positions, userRebalancerInfos]); - - useEffect(() => { - if (!availableNetworks.includes(targetNetwork)) { - setTargetNetwork(availableNetworks[0] ?? defaultNetwork); - setHasPreselected(false); - } - }, [availableNetworks, targetNetwork, defaultNetwork]); - - const isInPending = (market: Market) => - pendingCaps.some((cap) => cap.market.uniqueKey === market.uniqueKey && cap.amount > 0); - - const isInPendingRemove = (market: Market) => - pendingCaps.some( - (cap) => cap.market.uniqueKey === market.uniqueKey && cap.amount === BigInt(0), - ); - - const groupedMarkets = useMemo(() => { - const groups: Record = {}; - const activeLoanAssets = new Set(); - - positions.forEach((position) => { - if (BigInt(position.state.supplyShares) > 0) { - activeLoanAssets.add(position.market.loanAsset.address.toLowerCase()); - } - }); - - allMarkets.forEach((market) => { - if (market.morphoBlue.chain.id !== targetNetwork) return; - - const loanAssetKey = market.loanAsset.address.toLowerCase(); - - if (!activeLoanAssets.has(loanAssetKey)) return; - - if (!groups[loanAssetKey]) { - groups[loanAssetKey] = { - network: market.morphoBlue.chain.id, - loanAsset: market.loanAsset, - authorizedMarkets: [], - activeMarkets: [], - historicalMarkets: [], - otherMarkets: [], - }; - } - - const authorized = currentUserRebalancerInfo?.marketCaps.find( - (c) => c.marketId.toLowerCase() === market.uniqueKey.toLowerCase(), - )?.cap; - - if (authorized) { - groups[loanAssetKey].authorizedMarkets.push(market); - return; - } - - const position = positions.find((p) => p.market.uniqueKey === market.uniqueKey); - if (position) { - const supply = parseFloat( - formatUnits(BigInt(position.state.supplyAssets), position.market.loanAsset.decimals), - ); - if (supply > 0) { - groups[loanAssetKey].activeMarkets.push(market); - } else { - groups[loanAssetKey].historicalMarkets.push(market); - } - } else { - groups[loanAssetKey].otherMarkets.push(market); - } - }); - - return Object.values(groups).sort((a, b) => { - return a.loanAsset.symbol.localeCompare(b.loanAsset.symbol); - }); - }, [allMarkets, positions, currentUserRebalancerInfo, targetNetwork]); - - useEffect(() => { - let mounted = true; - if (!hasPreselected && groupedMarkets.length > 0 && targetNetwork) { - groupedMarkets.forEach((group) => { - group.activeMarkets.forEach((market) => { - if (!isInPending(market) && !isInPendingRemove(market)) { - addToPendingCaps(market, maxUint256); - } - }); - }); - if (mounted) { - setHasPreselected(true); - } - } - - return () => { - mounted = false; - }; - }, [ - hasPreselected, - groupedMarkets, - isInPending, - isInPendingRemove, - addToPendingCaps, - targetNetwork, - ]); - - const toggleGroup = (key: string) => { - setExpandedGroups((prev) => - prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key], - ); - }; - - const hasSetupAgent = - !!currentUserRebalancerInfo && findAgent(currentUserRebalancerInfo.rebalancer) !== undefined; - - const agent = useMemo(() => { - return currentUserRebalancerInfo - ? findAgent(currentUserRebalancerInfo.rebalancer) - : findAgent(KnownAgents.MAX_APY ?? ''); - }, [currentUserRebalancerInfo]); - - const { executeBatchSetupAgent, currentStep } = useAuthorizeAgent( - KnownAgents.MAX_APY, - pendingCaps.filter((cap) => cap.market.morphoBlue.chain.id === targetNetwork), - targetNetwork, - onNext, - ); - - const handleExecute = useCallback(() => { - setShowProcessModal(true); - void executeBatchSetupAgent(() => setShowProcessModal(false)); - }, [executeBatchSetupAgent]); - - const handleNetworkChange = (keys: SharedSelection) => { - const selectedKey = Array.from(keys)[0]; - const newNetwork = Number(selectedKey) as SupportedNetworks; - if (newNetwork !== targetNetwork) { - setTargetNetwork(newNetwork); - setHasPreselected(false); - setExpandedGroups([]); - } - }; - - return ( -
- {!hasSetupAgent && agent && ( -
-

{agent.name}

-

{agent.strategyDescription}

-
- )} -
- - The agent can only reallocate funds among approved markets for the selected network. - - {availableNetworks.length > 1 && ( - - - - - - {availableNetworks.map((networkId) => ( - -
- {getNetworkName(networkId) - {getNetworkName(networkId) ?? `Network ${networkId}`} -
-
- ))} -
-
- )} -
- -
- {groupedMarkets.length === 0 && ( -

- No active supplied markets found for {getNetworkName(targetNetwork)}. -

- )} - {groupedMarkets.map((group) => { - const groupKey = `${group.loanAsset.address}-${group.network}`; - const isExpanded = expandedGroups.includes(groupKey); - - const numMarketsToAdd = [ - ...group.activeMarkets, - ...group.historicalMarkets, - ...group.otherMarkets, - ].filter(isInPending).length; - - const numMarketsToRemove = group.authorizedMarkets.filter(isInPendingRemove).length; - - return ( -
- {showProcessModal && ( - setShowProcessModal(false)} - /> - )} - - - - - {isExpanded && ( - -
- {group.authorizedMarkets.length > 0 && ( -
-

Authorized

- {group.authorizedMarkets.map((market) => ( - - selected - ? removeFromPendingCaps(market) - : addToPendingCaps(market, BigInt(0)) - } - isDisabled={false} - /> - ))} -
- )} - - {group.activeMarkets.length > 0 && ( -
-

Active Markets

- {group.activeMarkets.map((market) => ( - - selected - ? addToPendingCaps(market, maxUint256) - : removeFromPendingCaps(market) - } - isDisabled={false} - /> - ))} -
- )} - - {group.historicalMarkets.length > 0 && ( -
-

Previously Used

- {group.historicalMarkets.map((market) => ( - - selected - ? addToPendingCaps(market, maxUint256) - : removeFromPendingCaps(market) - } - isDisabled={false} - /> - ))} -
- )} - - {group.otherMarkets.length > 0 && !showAllMarkets && ( - - )} - - {showAllMarkets && group.otherMarkets.length > 0 && ( -
-

Other Available

- {group.otherMarkets.map((market) => ( - - selected - ? addToPendingCaps(market, maxUint256) - : removeFromPendingCaps(market) - } - isDisabled={false} - /> - ))} -
- )} -
-
- )} -
-
- ); - })} -
- -
- - -
-
- ); -} diff --git a/app/positions/components/agent/SetupAgentModal.tsx b/app/positions/components/agent/SetupAgentModal.tsx deleted file mode 100644 index 5c1820a6..00000000 --- a/app/positions/components/agent/SetupAgentModal.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { useState } from 'react'; -import { Modal, ModalContent, ModalHeader } from '@heroui/react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Address } from 'viem'; -import { useMarkets } from '@/contexts/MarketsContext'; -import { MarketCap } from '@/hooks/useAuthorizeAgent'; -import useUserPositions from '@/hooks/useUserPositions'; -import { findAgent } from '@/utils/monarch-agent'; -import { isAgentAvailable } from '@/utils/networks'; -import { Market, UserRebalancerInfo } from '@/utils/types'; -import { Main as MainContent } from './Main'; -import { SetupAgent } from './SetupAgent'; -import { Success as SuccessContent } from './Success'; -import { Welcome as WelcomeContent } from './Welcome'; - -export enum SetupStep { - Main = 'main', - Setup = 'setup', - Success = 'success', -} - -const SETUP_STEPS = [ - { - id: SetupStep.Main, - title: 'Welcome to Monarch Agent', - description: 'Bee-bee-bee, Monarch Agent is here!', - }, - { - id: SetupStep.Setup, - title: 'Setup Markets', - description: 'Choose which markets you want Monarch Agent to monitor', - }, - { - id: SetupStep.Success, - title: 'Setup Complete', - description: 'Your Monarch Agent is ready to go', - }, -] as const; - -function StepIndicator({ currentStep }: { currentStep: SetupStep }) { - return ( -
- {SETUP_STEPS.map((step, index) => { - const isCurrent = step.id === currentStep; - const isPast = SETUP_STEPS.findIndex((s) => s.id === currentStep) > index; - - return ( -
-
-
- ); - })} -
- ); -} - -type SetupAgentModalProps = { - account?: Address; - isOpen: boolean; - onClose: () => void; - userRebalancerInfos: UserRebalancerInfo[]; -}; - -export function SetupAgentModal({ - account, - isOpen, - onClose, - userRebalancerInfos, -}: SetupAgentModalProps) { - const [currentStep, setCurrentStep] = useState(SetupStep.Main); - const [pendingCaps, setPendingCaps] = useState([]); - - const { data: positions } = useUserPositions(account, true); - - // Use computed markets based on user setting - const { allMarkets } = useMarkets(); - - const currentStepIndex = SETUP_STEPS.findIndex((s) => s.id === currentStep); - - const handleNext = () => { - setCurrentStep((prev) => { - const currentIndex = SETUP_STEPS.findIndex((step) => step.id === prev); - const nextStep = SETUP_STEPS[currentIndex + 1]; - return nextStep?.id || prev; - }); - }; - - const handleBack = () => { - setCurrentStep((prev) => { - const currentIndex = SETUP_STEPS.findIndex((step) => step.id === prev); - const prevStep = SETUP_STEPS[currentIndex - 1]; - return prevStep?.id || prev; - }); - }; - - const handleReset = () => { - setCurrentStep(SetupStep.Main); - }; - - const handleClose = () => { - onClose(); - // Reset step after modal is closed - setTimeout(() => { - setCurrentStep(SetupStep.Main); - }, 300); - }; - - const addToPendingCaps = (market: Market, cap: bigint) => { - setPendingCaps((prev) => [ - ...prev, - { - market, - amount: cap, - }, - ]); - }; - - const removeFromPendingCaps = (market: Market) => { - setPendingCaps((prev) => prev.filter((cap) => cap.market.uniqueKey !== market.uniqueKey)); - }; - - const hasSetupAgent = userRebalancerInfos.some( - (info) => findAgent(info.rebalancer) !== undefined, - ); - - return ( - - - -
-

{SETUP_STEPS[currentStepIndex].title}

-

- {SETUP_STEPS[currentStepIndex].description} -

-
-
- -
- - - {/* Step Content */} - {currentStep === SetupStep.Main && !hasSetupAgent && ( - - )} - {currentStep === SetupStep.Main && hasSetupAgent && ( - - )} - {currentStep === SetupStep.Setup && ( - isAgentAvailable(m.morphoBlue.chain.id))} - userRebalancerInfos={userRebalancerInfos} - pendingCaps={pendingCaps} - addToPendingCaps={addToPendingCaps} - removeFromPendingCaps={removeFromPendingCaps} - onNext={handleNext} - onBack={handleBack} - account={account} - /> - )} - {currentStep === SetupStep.Success && ( - - )} - - -
- - {/* Footer with Step Indicator */} -
- -
-
-
- ); -} diff --git a/app/positions/components/agent/Success.tsx b/app/positions/components/agent/Success.tsx deleted file mode 100644 index b5d85279..00000000 --- a/app/positions/components/agent/Success.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { motion } from 'framer-motion'; -import Image from 'next/image'; -import { Button } from '@/components/common'; - -const img = require('../../../../src/imgs/agent/agent.png') as string; - -type SuccessProps = { - onClose: () => void; - onDone: () => void; -}; - -export function Success({ onClose, onDone }: SuccessProps) { - const handleDone = () => { - onDone(); - onClose(); - }; - - return ( -
- - Success - - -
-

Setup Complete!

-

- Your Monarch Agent is now ready to manage your positions. You can always update your - settings later. -

-
- - -
- ); -} diff --git a/app/positions/components/agent/Welcome.tsx b/app/positions/components/agent/Welcome.tsx deleted file mode 100644 index 534e4be0..00000000 --- a/app/positions/components/agent/Welcome.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { motion } from 'framer-motion'; -import Image from 'next/image'; -import { Button } from '@/components/common'; - -const img = require('../../../../src/imgs/agent/agent-detailed.png') as string; - -type WelcomeProps = { - onNext: () => void; -}; - -export function Welcome({ onNext }: WelcomeProps) { - return ( -
- - Monarch Agent - - -
-

Automate Your Position Management

-

- Monarch Agent is a smart automation tool that helps you manage your positions across - different markets. It can automatically reallocate your assets to optimize your returns - while maintaining your risk preferences. -

-
- - -
- ); -} diff --git a/app/positions/components/onboarding/Modal.tsx b/app/positions/components/onboarding/OnboardingModal.tsx similarity index 59% rename from app/positions/components/onboarding/Modal.tsx rename to app/positions/components/onboarding/OnboardingModal.tsx index 05d456e2..5b2b21fc 100644 --- a/app/positions/components/onboarding/Modal.tsx +++ b/app/positions/components/onboarding/OnboardingModal.tsx @@ -1,6 +1,6 @@ -import { Modal, ModalContent, ModalHeader, Button } from '@heroui/react'; import { motion, AnimatePresence } from 'framer-motion'; -import { RxCross2 } from 'react-icons/rx'; +import { FaCompass } from 'react-icons/fa'; +import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/components/common/Modal'; import { AssetSelection } from './AssetSelection'; import { MarketSelectionOnboarding } from './MarketSelectionOnboarding'; import { useOnboarding } from './OnboardingContext'; @@ -42,7 +42,7 @@ function StepIndicator({ currentStep }: { currentStep: string }) { ); } -export function OnboardingModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { +export function OnboardingModal({ isOpen, onOpenChange }: { isOpen: boolean; onOpenChange: (open: boolean) => void }) { const { step } = useOnboarding(); const currentStepIndex = ONBOARDING_STEPS.findIndex((s) => s.id === step); @@ -51,54 +51,39 @@ export function OnboardingModal({ isOpen, onClose }: { isOpen: boolean; onClose: return ( - - {/* Header */} - -
-

- {ONBOARDING_STEPS[currentStepIndex].title} -

-

- {ONBOARDING_STEPS[currentStepIndex].description} -

-
- -
+ } + onClose={() => onOpenChange(false)} + /> - {/* Content */} -
+ +
- + onOpenChange(false)} />
+
- {/* Footer with Step Indicator */} -
- -
- + + + ); } diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index f25802f9..fc713f2b 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -294,7 +294,7 @@ export default function Rewards() { setShowProcessModal(false)} + onOpenChange={setShowProcessModal} /> )}
diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 0e848311..099f6137 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -55,7 +55,7 @@ export default function SettingsPage() { return (
-
+

Settings

@@ -249,7 +249,7 @@ export default function SettingsPage() { {/* Trusted Vaults Modal */} setIsTrustedVaultsModalOpen(!isTrustedVaultsModalOpen)} + onOpenChange={setIsTrustedVaultsModalOpen} userTrustedVaults={userTrustedVaults} setUserTrustedVaults={setUserTrustedVaults} /> @@ -257,7 +257,7 @@ export default function SettingsPage() { {/* Blacklisted Markets Modal */} setIsBlacklistedMarketsModalOpen(!isBlacklistedMarketsModalOpen)} + onOpenChange={setIsBlacklistedMarketsModalOpen} />
); diff --git a/docs/Styling.md b/docs/Styling.md index 41980dc8..6eefe0aa 100644 --- a/docs/Styling.md +++ b/docs/Styling.md @@ -5,9 +5,237 @@ Use these shared components instead of raw HTML elements: - `Button`: Import from `@/components/common/Button` for all clickable actions -- `Modal`: For all modal dialogs +- `Modal`: For **all** modal dialogs (always import from `@/components/common/Modal`) - `Card`: For contained content sections +## Modal Guidelines + +**IMPORTANT**: Always use our custom Modal components from `@/components/common/Modal`. Never import HeroUI modals directly. The shared wrapper applies Monarch typography, corner radius, background, blur, and z-index rules automatically. + +All modals MUST follow consistent styling standards for typography, spacing, and structure. There are two modal patterns based on use case. + +### Modal Types + +**1. Standard Modal** - For settings, management, and primary workflows +- Large settings modals (Trusted Vaults, Blacklisted Markets, Market Settings) +- Transaction modals (Rebalance, Market Selection) +- Onboarding and setup flows + +**2. Compact Modal** - For filters, confirmations, and secondary dialogs +- Filter modals (Supply Asset Filter) +- Confirmation dialogs (Blacklist Confirmation) + +### Using Our Modal Components + +Import the primitives from our shared entry point (and nowhere else): + +```tsx +import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; +import { Button } from '@/components/common/Button'; +``` + +### Standard Modal Pattern + +Use this pattern for primary workflows, settings, and management interfaces: + +```tsx + + + + + {/* Modal content - spacing and font-zen applied automatically */} + + + + + + + +``` + +**With Icon Support:** + +```tsx +import { TokenIcon } from '@/components/TokenIcon'; + + + + } + /> + {/* content */} + +``` + +**With Actions in Header:** + +```tsx + + Refresh + + } +/> +``` + +**Auto-Applied Standards:** +- **Spacing**: Automatically applied based on variant (no extra padding needed) +- **Typography**: `font-zen` and text scales handled for you +- **Icon + actions slots**: Header provides `mainIcon`, `actions`, and an always-on close button +- **Z-Index**: Managed through named layers (base, process, selection, settings) +- **Backdrops**: Unified blur/opacity, so you get consistent overlays everywhere +- **Portal**: All modals render to `document.body` to avoid stacking bugs + +### Compact Modal Pattern + +Use this pattern for filters, confirmations, and quick actions: + +```tsx + + + + + {/* Modal content - tighter spacing applied automatically */} + + + + + + + +``` + +**Auto-Applied Differences from Standard:** +- **Smaller padding**: `px-6 pt-4` vs `px-10 pt-6` +- **Smaller title**: `text-base` vs `text-lg` +- **Tighter spacing**: `gap-4` vs `gap-5` + +### Z-Index Management + +Our Modal component manages z-index automatically through named layers. This prevents conflicts when multiple modals are open: + +```tsx +// Z-Index Layers (from lowest to highest): +zIndex="base" // z-50 - Standard modals (Supply, Borrow, Campaign) +zIndex="process" // z-1100 - Process/transaction modals +zIndex="selection" // z-2200 - Market/item selection modals +zIndex="settings" // z-2300 - Settings modals (HIGHEST - always on top) +``` + +**Usage Example:** + +```tsx +// Settings modal (should be on top of everything) + + + {/* ... */} + + +// Market selection modal (opened from settings) + + + {/* ... */} + + +// Base modal (standard use case) + + + {/* ... */} + +``` + + +### Typography Rules + +Typography is automatically handled by our Modal components. You don't need to specify font weights or sizes manually - just use the title/description props. + +**IMPORTANT**: Never manually add bold or semibold font weights in modal headings/labels; rely on the shared components. + +```tsx +// ✅ Correct - let the component handle typography + + +// ❌ Incorrect - don't override with manual styles +Settings} /> +``` + +Use color and size to create hierarchy, not font weight: +- **Primary text**: `text-primary` +- **Secondary text**: `text-secondary` +- **Title size**: Automatically set based on variant +- **Description size**: Automatically set to `text-sm` + +### Section Headers in Modal Body + +For section headers within modal content, use consistent styling: + +```tsx +// ✅ Correct +

Section Title

+ +// ❌ Incorrect +

Section Title

+

Section Title

+``` + +### Custom Modals (Non-HeroUI) + +For custom modals using `framer-motion`, apply `font-zen` to the outer container: + +```tsx +// ✅ Correct - font-zen on the modal overlay +
+
+ {/* Modal content */} +
+
+ +// With framer-motion + + + + {/* Modal content */} + + + +``` + ## Component Guidelines ### Rounding diff --git a/src/components/AgentSetupProcessModal.tsx b/src/components/AgentSetupProcessModal.tsx deleted file mode 100644 index 3b98cd6f..00000000 --- a/src/components/AgentSetupProcessModal.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; -import { motion, AnimatePresence } from 'framer-motion'; -import { FaCheckCircle, FaCircle } from 'react-icons/fa'; -import { AuthorizeAgentStep } from '@/hooks/useAuthorizeAgent'; - -type AgentSetupModalProps = { - currentStep: AuthorizeAgentStep; - onClose: () => void; -}; - -const steps = [ - { - key: AuthorizeAgentStep.Authorize, - label: 'Authorize Monarch Agent', - detail: - 'Sign a signature to authorize the Monarch Agent contract to reallocate your positions.', - }, - { - key: AuthorizeAgentStep.Execute, - label: 'Execute Transaction', - detail: 'Confirm transaction in wallet to complete the setup', - }, -]; - -export function AgentSetupProcessModal({ - currentStep, - onClose, -}: AgentSetupModalProps): JSX.Element { - const getStepStatus = (stepKey: string) => { - const currentIndex = steps.findIndex((step) => step.key === currentStep); - const stepIndex = steps.findIndex((step) => step.key === stepKey); - - if (stepIndex < currentIndex) { - return 'done'; - } - if (stepKey === currentStep) { - return 'current'; - } - return 'undone'; - }; - - return ( - - - - - -
-

Setup Monarch Agent

-

Setup Rebalance market caps

- - {/* Steps */} -
- {steps.map((step) => { - const status = getStepStatus(step.key); - return ( -
-
- {status === 'done' ? ( - - ) : status === 'current' ? ( - - ) : ( - - )} -
-
-
{step.label}
-
{step.detail}
-
-
- ); - })} -
-
-
-
-
- ); -} diff --git a/src/components/Borrow/AddCollateralAndBorrow.tsx b/src/components/Borrow/AddCollateralAndBorrow.tsx index 229b6e00..dff270c1 100644 --- a/src/components/Borrow/AddCollateralAndBorrow.tsx +++ b/src/components/Borrow/AddCollateralAndBorrow.tsx @@ -255,8 +255,8 @@ export function AddCollateralAndBorrow({ {/* Collateral Input Section */}
-

Add Collateral

-

+

Add Collateral

+

Balance:{' '} {useEth ? formatBalance(ethBalance ? ethBalance : '0', 18) @@ -270,7 +270,7 @@ export function AddCollateralAndBorrow({ {isWrappedNativeToken(market.collateralAsset.address, market.morphoBlue.chain.id) && (

-
Use {getNativeTokenSymbol(market.morphoBlue.chain.id)} instead
+
Use {getNativeTokenSymbol(market.morphoBlue.chain.id)} instead
-

Borrow

-

+

Borrow

+

Available:{' '} {formatReadable( formatBalance(market.state.liquidityAssets, market.loanAsset.decimals), diff --git a/src/components/BorrowModal.tsx b/src/components/BorrowModal.tsx index 48933678..cec97ed3 100644 --- a/src/components/BorrowModal.tsx +++ b/src/components/BorrowModal.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; -import { FaArrowRightArrowLeft } from 'react-icons/fa6'; +import { LuArrowRightLeft } from "react-icons/lu"; import { useAccount, useBalance } from 'wagmi'; +import { Button } from '@/components/common/Button'; +import { Modal, ModalHeader, ModalBody } from '@/components/common/Modal'; import { Market, MarketPosition } from '@/utils/types'; import { AddCollateralAndBorrow } from './Borrow/AddCollateralAndBorrow'; import { WithdrawCollateralAndRepay } from './Borrow/WithdrawCollateralAndRepay'; @@ -9,7 +10,7 @@ import { TokenIcon } from './TokenIcon'; type BorrowModalProps = { market: Market; - onClose: () => void; + onOpenChange: (open: boolean) => void; oraclePrice: bigint; refetch?: () => void; isRefreshing?: boolean; @@ -18,7 +19,7 @@ type BorrowModalProps = { export function BorrowModal({ market, - onClose, + onOpenChange, oraclePrice, refetch, isRefreshing = false, @@ -49,90 +50,77 @@ export function BorrowModal({ position && (BigInt(position.state.borrowAssets) > 0n || BigInt(position.state.collateral) > 0n); - return ( -

-
-
- - -
-
-
-
- -
- -
-
-
-
- {market.loanAsset.symbol} - / {market.collateralAsset.symbol} -
- - {mode === 'borrow' ? 'Borrow against collateral' : 'Repay borrowed assets'} - -
-
-
- - {hasPosition && ( - - )} -
- - {mode === 'borrow' ? ( - - ) : ( - - )} -
+ const mainIcon = ( +
+ +
+
); + + return ( + + onOpenChange(false)} + title={ +
+ {market.loanAsset.symbol} + / {market.collateralAsset.symbol} +
+ } + description={mode === 'borrow' ? 'Borrow against collateral' : 'Repay borrowed assets'} + actions={ + hasPosition ? ( + + ) : undefined + } + /> + + {mode === 'borrow' ? ( + + ) : ( + + )} + +
+ ); } diff --git a/src/components/BorrowProcessModal.tsx b/src/components/BorrowProcessModal.tsx index e21cdfda..45778992 100644 --- a/src/components/BorrowProcessModal.tsx +++ b/src/components/BorrowProcessModal.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; -import { motion, AnimatePresence } from 'framer-motion'; import { FaCheckCircle, FaCircle } from 'react-icons/fa'; +import { FiArrowDownCircle } from 'react-icons/fi'; +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { BorrowStepType } from '@/hooks/useBorrowTransaction'; import { Market } from '@/utils/types'; import { MarketInfoBlock } from './common/MarketInfoBlock'; @@ -103,69 +103,54 @@ export function BorrowProcessModal({ }; return ( - - - - + { + if (!open) onClose(); + }} + size="lg" + isDismissable={false} + backdrop="blur" + > + } + onClose={onClose} + /> + + -
-

Borrow {borrow.market.loanAsset.symbol}

-

Using {tokenSymbol} as collateral

- - {/* Market details */} -
- -
- - {/* Steps */} -
- {steps.map((step) => { - const status = getStepStatus(step.key); - return ( -
-
- {status === 'done' ? ( - - ) : status === 'current' ? ( - - ) : ( - - )} -
-
-
{step.label}
-
{step.detail}
-
-
- ); - })} -
-
-
-
-
+
+ {steps.map((step) => { + const status = getStepStatus(step.key); + return ( +
+
+ {status === 'done' ? ( + + ) : status === 'current' ? ( + + ) : ( + + )} +
+
+
{step.label}
+
{step.detail}
+
+
+ ); + })} +
+ + ); } diff --git a/src/components/RepayProcessModal.tsx b/src/components/RepayProcessModal.tsx index 3e94014a..11f9f563 100644 --- a/src/components/RepayProcessModal.tsx +++ b/src/components/RepayProcessModal.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; -import { motion, AnimatePresence } from 'framer-motion'; import { FaCheckCircle, FaCircle } from 'react-icons/fa'; +import { FiRepeat } from 'react-icons/fi'; +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { Market } from '@/utils/types'; import { MarketInfoBlock } from './common/MarketInfoBlock'; @@ -74,75 +74,58 @@ export function RepayProcessModal({ }; return ( - - - - + { + if (!open) onClose(); + }} + size="lg" + isDismissable={false} + backdrop="blur" + > + 0n ? 'Withdraw & Repay' : 'Repay'} ${tokenSymbol}`} + description={ + withdrawAmount > 0n + ? 'Withdrawing collateral and repaying loan' + : 'Repaying loan to market' + } + mainIcon={} + onClose={onClose} + /> + + -
-

- {withdrawAmount > 0n ? 'Withdraw & Repay' : 'Repay'} {tokenSymbol} -

-

- {withdrawAmount > 0n - ? 'Withdrawing collateral and repaying loan' - : 'Repaying loan to market'} -

- - {/* Market details */} -
- -
- - {/* Steps */} -
- {steps.map((step) => { - const status = getStepStatus(step.key); - return ( -
-
- {status === 'done' ? ( - - ) : status === 'current' ? ( - - ) : ( - - )} -
-
-
{step.label}
-
{step.detail}
-
-
- ); - })} -
-
-
-
-
+
+ {steps.map((step) => { + const status = getStepStatus(step.key); + return ( +
+
+ {status === 'done' ? ( + + ) : status === 'current' ? ( + + ) : ( + + )} +
+
+
{step.label}
+
{step.detail}
+
+
+ ); + })} +
+ + ); } diff --git a/src/components/RiskNotificationModal.tsx b/src/components/RiskNotificationModal.tsx index f71aeccf..be2fa7aa 100644 --- a/src/components/RiskNotificationModal.tsx +++ b/src/components/RiskNotificationModal.tsx @@ -1,17 +1,11 @@ 'use client'; import { useState, useEffect } from 'react'; -import { - Modal, - ModalContent, - ModalHeader, - ModalBody, - ModalFooter, - Button, - Checkbox, -} from '@heroui/react'; +import { Button, Checkbox } from '@heroui/react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { IoWarningOutline } from 'react-icons/io5'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; export default function RiskNotificationModal() { const [isOpen, setIsOpen] = useState(false); @@ -37,12 +31,20 @@ export default function RiskNotificationModal() { } return ( - {}} hideCloseButton size="3xl" scrollBehavior="inside"> - - - Welcome to Monarch - - + + } + onClose={() => setIsOpen(false)} + /> +

Monarch enables direct lending to the Morpho Blue protocol. Before proceeding, it's important to understand the key aspects of this approach. For a comprehensive overview, @@ -83,17 +85,16 @@ export default function RiskNotificationModal() {

- - - - - + + + + ); } diff --git a/src/components/SupplyModalV2.tsx b/src/components/SupplyModalV2.tsx index cd1fd2ee..b84f2367 100644 --- a/src/components/SupplyModalV2.tsx +++ b/src/components/SupplyModalV2.tsx @@ -1,16 +1,15 @@ import React, { useState } from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; -import { FaArrowRightArrowLeft } from 'react-icons/fa6'; +import { LuArrowRightLeft } from "react-icons/lu"; +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { Market, MarketPosition } from '@/utils/types'; import { MarketDetailsBlock } from './common/MarketDetailsBlock'; import { SupplyModalContent } from './SupplyModalContent'; import { TokenIcon } from './TokenIcon'; import { WithdrawModalContent } from './WithdrawModalContent'; - type SupplyModalV2Props = { market: Market; position?: MarketPosition | null; - onClose: () => void; + onOpenChange: (open: boolean) => void; refetch?: () => void; isMarketPage?: boolean; defaultMode?: 'supply' | 'withdraw'; @@ -19,7 +18,7 @@ type SupplyModalV2Props = { export function SupplyModalV2({ market, position, - onClose, + onOpenChange, refetch, isMarketPage, defaultMode = 'supply', @@ -31,87 +30,74 @@ export function SupplyModalV2({ const hasPosition = position && BigInt(position.state.supplyAssets) > 0n; return ( -
-
-
- - -
-
-
- - - {mode === 'supply' ? 'Supply' : 'Withdraw'} {market.loanAsset.symbol} - -
- - {mode === 'supply' ? 'Supply to earn interest' : 'Withdraw your supplied assets'} - -
- - {hasPosition && ( - - )} -
- - {/* Market Details Block - includes position overview and collapsible details */} -
- -
+ + } + onClose={() => onOpenChange(false)} + actions={ + hasPosition ? ( + + ) : undefined + } + /> + + - {mode === 'supply' ? ( - {})} - onAmountChange={setSupplyPreviewAmount} - /> - ) : ( - {})} - onAmountChange={setWithdrawPreviewAmount} - /> - )} -
-
-
+ {mode === 'supply' ? ( + onOpenChange(false)} + refetch={refetch ?? (() => {})} + onAmountChange={setSupplyPreviewAmount} + /> + ) : ( + onOpenChange(false)} + refetch={refetch ?? (() => {})} + onAmountChange={setWithdrawPreviewAmount} + /> + )} + + ); } diff --git a/src/components/SupplyProcessModal.tsx b/src/components/SupplyProcessModal.tsx index 8ccb0a89..02055f8d 100644 --- a/src/components/SupplyProcessModal.tsx +++ b/src/components/SupplyProcessModal.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; -import { motion, AnimatePresence } from 'framer-motion'; import { FaCheckCircle, FaCircle } from 'react-icons/fa'; +import { FiUpload } from 'react-icons/fi'; +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { Market } from '@/utils/types'; import { MarketInfoBlock } from './common/MarketInfoBlock'; @@ -89,79 +89,64 @@ export function SupplyProcessModal({ const isMultiMarket = supplies.length > 1; return ( - - - - + { + if (!open) onClose(); + }} + size="lg" + isDismissable={false} + backdrop="blur" + > + } + onClose={onClose} + /> + +
+ {supplies.map((supply) => ( + + ))} +
-
-

Supply {tokenSymbol}

-

- {isMultiMarket ? `Supplying to ${supplies.length} markets` : 'Supplying to market'} -

- - {/* Market details */} -
- {supplies.map((supply) => { - return ( - - ); - })} -
- - {/* Steps */} -
- {steps.map((step) => { - const status = getStepStatus(step.key); - return ( -
-
- {status === 'done' ? ( - - ) : status === 'current' ? ( - - ) : ( - - )} -
-
-
{step.label}
-
{step.detail}
-
-
- ); - })} -
-
-
-
-
+
+ {steps.map((step) => { + const status = getStepStatus(step.key); + return ( +
+
+ {status === 'done' ? ( + + ) : status === 'current' ? ( + + ) : ( + + )} +
+
+
{step.label}
+
{step.detail}
+
+
+ ); + })} +
+ + ); } diff --git a/src/components/WrapProcessModal.tsx b/src/components/WrapProcessModal.tsx index 337d5466..45716c93 100644 --- a/src/components/WrapProcessModal.tsx +++ b/src/components/WrapProcessModal.tsx @@ -1,20 +1,21 @@ import React, { useMemo } from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; import { motion, AnimatePresence } from 'framer-motion'; import { FaCheckCircle, FaCircle } from 'react-icons/fa'; +import { LuArrowRightLeft } from "react-icons/lu"; +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { WrapStep } from '@/hooks/useWrapLegacyMorpho'; import { formatBalance } from '@/utils/balance'; type WrapProcessModalProps = { amount: bigint; currentStep: WrapStep; - onClose: () => void; + onOpenChange: (opened: boolean) => void; }; export function WrapProcessModal({ amount, currentStep, - onClose, + onOpenChange, }: WrapProcessModalProps): JSX.Element { const steps = useMemo( () => [ @@ -33,65 +34,47 @@ export function WrapProcessModal({ ); return ( -
- -
- - -
+ + } + /> + + + {steps.map((step, index) => { + const isActive = currentStep === step.key; + const isPassed = steps.findIndex((s) => s.key === currentStep) > index; -
- - {steps.map((step, index) => { - const isActive = currentStep === step.key; - const isPassed = steps.findIndex((s) => s.key === currentStep) > index; - - return ( - -
- {isPassed ? ( - - ) : ( - - )} -
-
-

{step.label}

-

{step.detail}

-
-
- ); - })} -
-
-
-
+ return ( + +
+ {isPassed ? ( + + ) : ( + + )} +
+
+

{step.label}

+

{step.detail}

+
+
+ ); + })} + + + ); } diff --git a/src/components/common/MarketDetailsBlock.tsx b/src/components/common/MarketDetailsBlock.tsx index e44808ba..3cf23360 100644 --- a/src/components/common/MarketDetailsBlock.tsx +++ b/src/components/common/MarketDetailsBlock.tsx @@ -71,7 +71,7 @@ export function MarketDetailsBlock({
{/* Collapsible Market Details */}
!disableExpansion && setIsExpanded(!isExpanded)} onKeyDown={(e) => { if (!disableExpansion && (e.key === 'Enter' || e.key === ' ')) { diff --git a/src/components/common/MarketSelectionModal.tsx b/src/components/common/MarketSelectionModal.tsx index 8c6671a1..eb3f0bf2 100644 --- a/src/components/common/MarketSelectionModal.tsx +++ b/src/components/common/MarketSelectionModal.tsx @@ -1,8 +1,9 @@ import { useState, useMemo } from 'react'; -import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/react'; +import { FiSearch } from 'react-icons/fi'; import { Address } from 'viem'; import { Button } from '@/components/common/Button'; import { MarketsTableWithSameLoanAsset } from '@/components/common/MarketsTableWithSameLoanAsset'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { Spinner } from '@/components/common/Spinner'; import { useMarkets } from '@/hooks/useMarkets'; import { SupportedNetworks } from '@/utils/networks'; @@ -16,7 +17,7 @@ type MarketSelectionModalProps = { excludeMarketIds?: Set; multiSelect?: boolean; isOpen?: boolean; - onClose: () => void; + onOpenChange: (open: boolean) => void; onSelect: (markets: Market[]) => void; confirmButtonText?: string; }; @@ -33,7 +34,7 @@ export function MarketSelectionModal({ excludeMarketIds, multiSelect = true, isOpen = true, - onClose, + onOpenChange, onSelect, confirmButtonText, }: MarketSelectionModalProps) { @@ -68,7 +69,7 @@ export function MarketSelectionModal({ const market = availableMarkets.find((m) => m.uniqueKey === marketId); if (market) { onSelect([market]); - onClose(); + onOpenChange(false); } return; } @@ -90,7 +91,7 @@ export function MarketSelectionModal({ selectedMarkets.has(m.uniqueKey) ); onSelect(marketsToReturn); - onClose(); + onOpenChange(false); }; const selectedCount = selectedMarkets.size; @@ -103,26 +104,20 @@ export function MarketSelectionModal({ return ( - - <> - -

{title}

-

{description}

-
- - + } + onClose={() => onOpenChange(false)} + /> + {marketsLoading ? (
@@ -145,16 +140,15 @@ export function MarketSelectionModal({ showSelectColumn={multiSelect} /> )} - - - + + {multiSelect ? ( <>

{selectedCount} market{selectedCount !== 1 ? 's' : ''} selected

-
)} -
- - +
); } diff --git a/src/components/common/MarketsTableWithSameLoanAsset.tsx b/src/components/common/MarketsTableWithSameLoanAsset.tsx index ac97f038..ac4f2380 100644 --- a/src/components/common/MarketsTableWithSameLoanAsset.tsx +++ b/src/components/common/MarketsTableWithSameLoanAsset.tsx @@ -31,6 +31,16 @@ import { MarketIdBadge } from '../MarketIdBadge'; import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '../MarketIdentity'; import { MarketIndicators } from '../MarketIndicators'; +const ZERO_DISPLAY_THRESHOLD = 1e-6; + +function formatAmountDisplay(value: bigint | string, decimals: number) { + const numericValue = formatBalance(value, decimals); + if (!Number.isFinite(numericValue) || Math.abs(numericValue) < ZERO_DISPLAY_THRESHOLD) { + return '-'; + } + return formatReadable(numericValue); +} + export type MarketWithSelection = { market: Market; isSelected: boolean; @@ -480,21 +490,21 @@ function MarketRow({ {columnVisibility.totalSupply && (

- {formatReadable(formatBalance(market.state.supplyAssets, market.loanAsset.decimals))} + {formatAmountDisplay(market.state.supplyAssets, market.loanAsset.decimals)}

)} {columnVisibility.totalBorrow && (

- {formatReadable(formatBalance(market.state.borrowAssets, market.loanAsset.decimals))} + {formatAmountDisplay(market.state.borrowAssets, market.loanAsset.decimals)}

)} {columnVisibility.liquidity && (

- {formatReadable(formatBalance(market.state.liquidityAssets, market.loanAsset.decimals))} + {formatAmountDisplay(market.state.liquidityAssets, market.loanAsset.decimals)}

)} @@ -1034,7 +1044,7 @@ export function MarketsTableWithSameLoanAsset({ {showSettingsModal && ( setShowSettingsModal(false)} + onOpenChange={setShowSettingsModal} usdFilters={usdFilters} setUsdFilters={setUsdFilters} entriesPerPage={entriesPerPage} @@ -1050,7 +1060,7 @@ export function MarketsTableWithSameLoanAsset({ {showTrustedVaultsModal && ( setShowTrustedVaultsModal(false)} + onOpenChange={setShowTrustedVaultsModal} userTrustedVaults={userTrustedVaults} setUserTrustedVaults={setUserTrustedVaults} /> diff --git a/src/components/common/Modal/Modal.tsx b/src/components/common/Modal/Modal.tsx new file mode 100644 index 00000000..317c7f9c --- /dev/null +++ b/src/components/common/Modal/Modal.tsx @@ -0,0 +1,78 @@ +import React, { useEffect, useState } from 'react'; +import { Modal as HeroModal, ModalContent } from '@heroui/react'; + +export type ModalVariant = 'standard' | 'compact' | 'custom'; +export type ModalZIndex = 'base' | 'process' | 'selection' | 'settings' | 'custom'; + +type ModalProps = { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + children: React.ReactNode | ((onClose: () => void) => React.ReactNode); + zIndex?: ModalZIndex; + size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | 'full'; + isDismissable?: boolean; + hideCloseButton?: boolean; + scrollBehavior?: 'inside' | 'outside' | 'normal'; + backdrop?: 'transparent' | 'opaque' | 'blur'; + className?: string; +}; + +const Z_INDEX_MAP: Record = { + base: { wrapper: 'z-[2000]', backdrop: 'z-[1990]' }, + process: { wrapper: 'z-[2600]', backdrop: 'z-[2590]' }, + selection: { wrapper: 'z-[3000]', backdrop: 'z-[2990]' }, + settings: { wrapper: 'z-[3200]', backdrop: 'z-[3190]' }, + custom: { wrapper: '', backdrop: '' }, +}; + +export function Modal({ + isOpen, + onOpenChange, + children, + zIndex = 'base', + size = 'xl', + isDismissable = true, + hideCloseButton = true, + scrollBehavior = 'inside', + backdrop = 'blur', + className = '', +}: ModalProps) { + const [portalContainer, setPortalContainer] = useState(null); + useEffect(() => { + setPortalContainer(document.body); + }, []); + + const zIndexClasses = Z_INDEX_MAP[zIndex]; + const backdropStyle = + backdrop === 'transparent' + ? 'bg-transparent' + : backdrop === 'opaque' + ? 'bg-black/70' + : 'bg-black/70 backdrop-blur-md'; + + return ( + + + {/* eslint-disable-next-line @typescript-eslint/promise-function-async */} + {(closeModal) => + typeof children === 'function' ? children(closeModal) : children} + + + ); +} diff --git a/src/components/common/Modal/ModalBody.tsx b/src/components/common/Modal/ModalBody.tsx new file mode 100644 index 00000000..a9558b51 --- /dev/null +++ b/src/components/common/Modal/ModalBody.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { ModalBody as HeroModalBody } from '@heroui/react'; +import { twMerge } from 'tailwind-merge'; + +export type ModalBodyVariant = 'standard' | 'compact'; + +type ModalBodyProps = { + children: React.ReactNode; + variant?: ModalBodyVariant; + className?: string; +}; + +export function ModalBody({ + children, + variant = 'standard', + className = '', +}: ModalBodyProps) { + const isStandard = variant === 'standard'; + const paddingClass = isStandard ? 'px-6 pb-6 pt-2' : 'px-6 pb-4 pt-2'; + const gapClass = isStandard ? 'gap-5' : 'gap-4'; + + return ( + + {children} + + ); +} diff --git a/src/components/common/Modal/ModalFooter.tsx b/src/components/common/Modal/ModalFooter.tsx new file mode 100644 index 00000000..5eb6a309 --- /dev/null +++ b/src/components/common/Modal/ModalFooter.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { ModalFooter as HeroModalFooter } from '@heroui/react'; + +type ModalFooterProps = { + children: React.ReactNode; + className?: string; +}; + +export function ModalFooter({ children, className = '' }: ModalFooterProps) { + return ( + + {children} + + ); +} diff --git a/src/components/common/Modal/ModalHeader.tsx b/src/components/common/Modal/ModalHeader.tsx new file mode 100644 index 00000000..23415597 --- /dev/null +++ b/src/components/common/Modal/ModalHeader.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { ModalHeader as HeroModalHeader } from '@heroui/react'; +import { Cross1Icon } from '@radix-ui/react-icons'; +import { twMerge } from 'tailwind-merge'; + +export type ModalHeaderVariant = 'standard' | 'compact'; + +export type ModalHeaderAction = { + icon: React.ReactNode; + onClick: () => void; + ariaLabel: string; +}; + +type ModalHeaderProps = { + title: string | React.ReactNode; + description?: string | React.ReactNode; + mainIcon?: React.ReactNode; + variant?: ModalHeaderVariant; + children?: React.ReactNode; + className?: string; + actions?: React.ReactNode; + onClose?: () => void; + showCloseButton?: boolean; + closeButtonAriaLabel?: string; + auxiliaryAction?: ModalHeaderAction; +}; + +export function ModalHeader({ + title, + description, + mainIcon, + variant = 'standard', + children, + className = '', + actions, + onClose, + showCloseButton = true, + closeButtonAriaLabel = 'Close modal', + auxiliaryAction, +}: ModalHeaderProps) { + const isStandard = variant === 'standard'; + const paddingClass = isStandard ? 'px-6 pt-6 pb-4' : 'px-5 pt-4 pb-3'; + const titleSizeClass = isStandard ? 'text-2xl' : 'text-lg'; + const descriptionSizeClass = isStandard ? 'text-sm' : 'text-xs'; + const showCloseIcon = Boolean(onClose) && showCloseButton; + const topRightControls = Boolean(actions || auxiliaryAction || showCloseIcon); + const controlPositionClass = isStandard ? 'top-6 right-6' : 'top-4 right-4'; + const contentRightPadding = topRightControls ? (isStandard ? 'pr-14' : 'pr-10') : ''; + const handleAuxiliaryClick = auxiliaryAction?.onClick; + const handleClose = onClose; + const iconButtonBaseClass = + 'flex h-8 w-8 items-center justify-center rounded-full text-secondary transition hover:text-primary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/70'; + + // If children are provided, use them directly (for custom layouts) + if (children) { + return ( + + {children} + + ); + } + + // Standard layout with title, description, and optional icon + return ( + +
+
+ {mainIcon &&
{mainIcon}
} + {title} +
+ {description && ( +
+ {description} +
+ )} +
+ {topRightControls && ( +
+ {actions &&
{actions}
} + {auxiliaryAction && handleAuxiliaryClick && ( + + )} + {showCloseIcon && handleClose && ( + + )} +
+ )} +
+ ); +} diff --git a/src/components/common/Modal/index.ts b/src/components/common/Modal/index.ts new file mode 100644 index 00000000..d03656b3 --- /dev/null +++ b/src/components/common/Modal/index.ts @@ -0,0 +1,10 @@ +export { Modal } from './Modal'; +export type { ModalVariant, ModalZIndex } from './Modal'; + +export { ModalHeader } from './ModalHeader'; +export type { ModalHeaderVariant, ModalHeaderAction } from './ModalHeader'; + +export { ModalBody } from './ModalBody'; +export type { ModalBodyVariant } from './ModalBody'; + +export { ModalFooter } from './ModalFooter'; diff --git a/src/components/common/SuppliedAssetFilterCompactSwitch.tsx b/src/components/common/SuppliedAssetFilterCompactSwitch.tsx index 4efc4e2d..2cbd9531 100644 --- a/src/components/common/SuppliedAssetFilterCompactSwitch.tsx +++ b/src/components/common/SuppliedAssetFilterCompactSwitch.tsx @@ -1,11 +1,12 @@ 'use client'; import { useMemo } from 'react'; -import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, Divider, Tooltip, useDisclosure } from '@heroui/react'; +import { Divider, Tooltip, useDisclosure } from '@heroui/react'; import { FiFilter } from 'react-icons/fi'; import { Button } from '@/components/common/Button'; import { FilterRow, FilterSection } from '@/components/common/FilterComponents'; import { IconSwitch } from '@/components/common/IconSwitch'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { TooltipContent } from '@/components/TooltipContent'; import { MONARCH_PRIMARY } from '@/constants/chartColors'; import { formatReadable } from '@/utils/balance'; @@ -105,21 +106,18 @@ export function SuppliedAssetFilterCompactSwitch({ onOpenChange={onOpenChange} size="md" backdrop="opaque" - classNames={{ - wrapper: 'z-[2400]', - backdrop: 'z-[2390]', - }} + zIndex="settings" > - - {(close) => ( - <> - - Filters -

- Quickly toggle the visibility filters that power the markets table. -

-
- + {(close) => ( + <> + } + onClose={close} + /> + - - - - - - - )} -
+ + + + + + + )}
); diff --git a/src/components/settings/BlacklistedMarketsModal.tsx b/src/components/settings/BlacklistedMarketsModal.tsx index ccf15b54..d64c3e6f 100644 --- a/src/components/settings/BlacklistedMarketsModal.tsx +++ b/src/components/settings/BlacklistedMarketsModal.tsx @@ -1,17 +1,18 @@ 'use client'; import React, { useMemo } from 'react'; -import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Divider } from '@heroui/react'; +import { Divider } from '@heroui/react'; import { FiPlus, FiX } from 'react-icons/fi'; -import { IoWarningOutline } from 'react-icons/io5'; +import { MdBlockFlipped } from "react-icons/md"; import { Button } from '@/components/common'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '@/components/MarketIdentity'; import { useMarkets } from '@/contexts/MarketsContext'; import type { Market } from '@/utils/types'; type BlacklistedMarketsModalProps = { isOpen: boolean; - onOpenChange: () => void; + onOpenChange: (opened: boolean) => void; }; const ITEMS_PER_PAGE = 20; @@ -87,39 +88,24 @@ export function BlacklistedMarketsModal({ isOpen, onOpenChange }: BlacklistedMar onOpenChange={onOpenChange} backdrop="blur" size="3xl" - classNames={{ - wrapper: 'z-[2300]', - backdrop: 'z-[2290]', - }} + zIndex="settings" scrollBehavior="inside" > - - {(onClose) => ( - <> - - Manage Blacklisted Markets - - - {/* Info Section */} -
-

- Block specific markets from appearing in your view. Blacklisted markets will be - completely hidden from all market lists and filters. -

-
- -

- Some markets are blacklisted by default due to security concerns or issues. - These cannot be removed from the blacklist. -

-
-
- + {(onClose) => ( + <> + } + onClose={onClose} + /> + + {/* Blacklisted Markets Section */} {blacklistedMarkets.length > 0 && ( <> -
-

+
+

Blacklisted Markets ({blacklistedMarkets.length})

@@ -171,9 +157,9 @@ export function BlacklistedMarketsModal({ isOpen, onOpenChange }: BlacklistedMar )} {/* Available Markets Section */} -
+
-

Add Markets to Blacklist

+

Add Markets to Blacklist

{filteredAvailableMarkets.length > 0 && ( {filteredAvailableMarkets.length} result @@ -191,7 +177,7 @@ export function BlacklistedMarketsModal({ isOpen, onOpenChange }: BlacklistedMar
{/* Available Markets List */} -
+
{searchQuery.trim().length === 0 ? (
Start typing to search for markets to blacklist. @@ -267,15 +253,14 @@ export function BlacklistedMarketsModal({ isOpen, onOpenChange }: BlacklistedMar )}
- - - - - - )} - + + + + + + )} ); } diff --git a/src/components/settings/CustomRpcSettings.tsx b/src/components/settings/CustomRpcSettings.tsx index 3f8c0841..b86d706c 100644 --- a/src/components/settings/CustomRpcSettings.tsx +++ b/src/components/settings/CustomRpcSettings.tsx @@ -1,9 +1,9 @@ 'use client'; import { useState } from 'react'; -import { Cross1Icon } from '@radix-ui/react-icons'; import Image from 'next/image'; import { Button } from '@/components/common/Button'; +import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import { Spinner } from '@/components/common/Spinner'; import { useStyledToast } from '@/hooks/useStyledToast'; import { SupportedNetworks, networks } from '@/utils/networks'; @@ -175,50 +175,43 @@ function RpcModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) setError(''); }; - if (!isOpen) return null; + if (!isOpen) { + return null; + } return ( -
{ + if (!open) handleClose(); + }} + size="2xl" + scrollBehavior="inside" > -
-
- - -
-
-

Configure RPC Endpoints

-

- Set custom RPC URLs for blockchain networks -

-
- - -
- - {/* Network List */} -
- {networks.map((network) => { - const chainId = network.network; - const isCustom = isUsingCustomRpc(chainId); - const isSelected = selectedNetwork === chainId; - - return ( - +
); })} -
+
+ + {selectedNetwork && ( +
+
+
+ n.network === selectedNetwork)?.logo || ''} + alt={networks.find((n) => n.network === selectedNetwork)?.name || ''} + width={20} + height={20} + className="rounded-full" + /> + + Configure {networks.find((n) => n.network === selectedNetwork)?.name} RPC + +
- {/* Edit Area */} - {selectedNetwork && ( -
-
-
- n.network === selectedNetwork)?.logo || ''} - alt={networks.find((n) => n.network === selectedNetwork)?.name || ''} - width={20} - height={20} - className="rounded-full" +
+
+ handleInputChange(e.target.value)} + className={`bg-hovered h-10 w-full truncate rounded p-2 pr-16 text-sm focus:border-primary focus:outline-none ${ + error ? 'border border-red-500 focus:border-red-500' : '' + }`} /> - - Configure {networks.find((n) => n.network === selectedNetwork)?.name} RPC - +
+ {error &&

{error}

} +
-
-
- handleInputChange(e.target.value)} - className={`bg-hovered h-10 w-full truncate rounded p-2 pr-16 text-sm focus:border-primary focus:outline-none ${ - error ? 'border border-red-500 focus:border-red-500' : '' - }`} - /> - -
- {error &&

{error}

} + {isUsingCustomRpc(selectedNetwork) && ( +
+
- - {isUsingCustomRpc(selectedNetwork) && ( -
- -
- )} -
+ )}
- )} -
-
-
+
+ )} + + ); } diff --git a/src/components/settings/TrustedVaultsModal.tsx b/src/components/settings/TrustedVaultsModal.tsx index d3240add..6d5a1162 100644 --- a/src/components/settings/TrustedVaultsModal.tsx +++ b/src/components/settings/TrustedVaultsModal.tsx @@ -1,21 +1,13 @@ 'use client'; import React, { useMemo, useState } from 'react'; -import { - Modal, - ModalContent, - ModalHeader, - ModalBody, - ModalFooter, - Divider, - Input, - Spinner, -} from '@heroui/react'; +import { Divider, Input, Spinner } from '@heroui/react'; import { FiChevronDown, FiChevronUp } from 'react-icons/fi'; import { GoShield, GoShieldCheck } from 'react-icons/go'; import { IoWarningOutline } from 'react-icons/io5'; import { Button } from '@/components/common'; import { IconSwitch } from '@/components/common/IconSwitch'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { NetworkIcon } from '@/components/common/NetworkIcon'; import { VaultIdentity } from '@/components/vaults/VaultIdentity'; import { @@ -27,7 +19,7 @@ import { useAllMorphoVaults } from '@/hooks/useAllMorphoVaults'; type TrustedVaultsModalProps = { isOpen: boolean; - onOpenChange: () => void; + onOpenChange: (isOpen: boolean) => void; userTrustedVaults: TrustedVault[]; setUserTrustedVaults: React.Dispatch>; }; @@ -158,25 +150,20 @@ export default function TrustedVaultsModal({ onOpenChange={onOpenChange} backdrop="blur" size="3xl" - classNames={{ - wrapper: 'z-[2300]', - backdrop: 'z-[2290]', - }} + zIndex="settings" scrollBehavior="inside" > - - {(onClose) => ( - <> - - Manage Trusted Vaults - - + {(onClose) => ( + <> + } + onClose={onClose} + /> + {/* Info Section */} -
-

- Select which vaults you trust. Trusted vaults can be used to filter markets based on - vault participation. -

+

@@ -188,7 +175,7 @@ export default function TrustedVaultsModal({

{/* Search and Actions */} -
+
-
-

+
+

Known Vaults ({sortedMonarchVaults.length})

{sortedMonarchVaults.length === 0 ? ( @@ -314,15 +301,14 @@ export default function TrustedVaultsModal({ ) )}
- - - - - - )} - + + + + + + )} ); } diff --git a/src/hooks/useRebalance.ts b/src/hooks/useRebalance.ts index bcee74b9..073ba41a 100644 --- a/src/hooks/useRebalance.ts +++ b/src/hooks/useRebalance.ts @@ -91,17 +91,21 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () }, []); // Transaction hook for the final multicall + const handleTransactionSuccess = useCallback(() => { + setRebalanceActions([]); + void refetchIsBundlerAuthorized(); + if (onRebalance) { + onRebalance(); + } + }, [refetchIsBundlerAuthorized, onRebalance]); + const { sendTransactionAsync, isConfirming: isExecuting } = useTransactionWithToast({ toastId: 'rebalance', pendingText: 'Rebalancing positions', successText: 'Positions rebalanced successfully', errorText: 'Failed to rebalance positions', chainId: groupedPosition.chainId, - onSuccess: () => { - setRebalanceActions([]); // Clear actions on success - void refetchIsBundlerAuthorized(); // Refetch bundler auth status - if (onRebalance) void onRebalance(); // Call external callback - }, + onSuccess: handleTransactionSuccess, }); // Helper function to generate common withdraw/supply tx data