diff --git a/app/positions/components/ApyPreview.tsx b/app/positions/components/ApyPreview.tsx index ab7eed06..221f003a 100644 --- a/app/positions/components/ApyPreview.tsx +++ b/app/positions/components/ApyPreview.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { formatReadable } from '@/utils/balance'; +import { MetricPreview } from './MetricPreview'; type ApyPreviewProps = { currentApy: number; @@ -7,26 +7,9 @@ type ApyPreviewProps = { }; /** - * Standardized APY preview component. - * Shows current APY in a fixed position, and appends preview to the right when available. - * This ensures perfect vertical alignment across all uses. + * APY preview component. + * Thin wrapper around MetricPreview for APY-specific usage. */ export function ApyPreview({ currentApy, previewApy }: ApyPreviewProps) { - const formattedCurrent = formatReadable(currentApy * 100); - const formattedPreview = previewApy ? formatReadable(previewApy * 100) : null; - const hasPreview = Boolean(previewApy && formattedPreview); - - const currentClasses = `text-foreground${hasPreview ? ' line-through opacity-50' : ''}`; - - return ( - - {formattedCurrent}% - {hasPreview && ( - <> - {' → '} - {formattedPreview}% - - )} - - ); + return ; } diff --git a/app/positions/components/FromMarketsTable.tsx b/app/positions/components/FromMarketsTable.tsx index ade637de..3a73ce47 100644 --- a/app/positions/components/FromMarketsTable.tsx +++ b/app/positions/components/FromMarketsTable.tsx @@ -5,7 +5,6 @@ import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '@/compo import { formatReadable } from '@/utils/balance'; import { previewMarketState } from '@/utils/morpho'; import { MarketPosition } from '@/utils/types'; -import { ApyPreview } from './ApyPreview'; type PositionWithPendingDelta = MarketPosition & { pendingDelta: number }; @@ -52,13 +51,15 @@ export function FromMarketsTable({ - + + - + + @@ -101,11 +102,35 @@ export function FromMarketsTable({ /> - +
MarketAPYAPYUtil Supplied Amount
- + + {apyPreview ? ( + + + {formatReadable(position.market.state.supplyApy * 100)}% + + {' → '} + {formatReadable(apyPreview.supplyApy * 100)}% + + ) : ( + + {formatReadable(position.market.state.supplyApy * 100)}% + + )} + + {apyPreview ? ( + + + {formatReadable(position.market.state.utilization * 100)}% + + {' → '} + {formatReadable(apyPreview.utilization * 100)}% + + ) : ( + + {formatReadable(position.market.state.utilization * 100)}% + + )}
diff --git a/app/positions/components/MetricPreview.tsx b/app/positions/components/MetricPreview.tsx new file mode 100644 index 00000000..ec47fa91 --- /dev/null +++ b/app/positions/components/MetricPreview.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Tooltip } from '@heroui/react'; +import { TooltipContent } from '@/components/TooltipContent'; +import { formatReadable } from '@/utils/balance'; + +type MetricPreviewProps = { + currentValue: number; + previewValue?: number | null; + label: string; +}; + +/** + * Generic metric preview component. + * Shows preview value if available, otherwise shows current value. + * Displays tooltip on hover showing before/after values. + */ +export function MetricPreview({ currentValue, previewValue, label }: MetricPreviewProps) { + const formattedCurrent = formatReadable(currentValue * 100); + const formattedPreview = previewValue ? formatReadable(previewValue * 100) : null; + const hasPreview = Boolean(previewValue && formattedPreview); + + // Show preview value if available, otherwise show current value + const displayValue = hasPreview ? formattedPreview : formattedCurrent; + + if (!hasPreview) { + return ( + + {displayValue}% + + ); + } + + return ( + + } + > + + {displayValue}% + + + ); +} diff --git a/app/positions/components/RebalanceActionInput.tsx b/app/positions/components/RebalanceActionInput.tsx index 8e1a0805..8b578fb1 100644 --- a/app/positions/components/RebalanceActionInput.tsx +++ b/app/positions/components/RebalanceActionInput.tsx @@ -1,12 +1,6 @@ -import React, { useMemo } from 'react'; -import { ArrowRightIcon, Cross2Icon } from '@radix-ui/react-icons'; -import { parseUnits } from 'viem'; -import { Button } from '@/components/common'; -import { MarketIdentity, MarketIdentityMode } from '@/components/MarketIdentity'; -import { TokenIcon } from '@/components/TokenIcon'; -import { previewMarketState } from '@/utils/morpho'; +import React from 'react'; import { GroupedPosition, Market } from '@/utils/types'; -import { ApyPreview } from './ApyPreview'; +import { RebalanceActionRow } from './RebalanceActionRow'; type RebalanceActionInputProps = { amount: string; @@ -43,129 +37,24 @@ export function RebalanceActionInput({ (m) => m.uniqueKey === selectedToMarketUniqueKey, ); - // Calculate preview APY for the selected "to" market - const previewState = useMemo(() => { - if (!selectedToMarket || !amount || Number(amount) <= 0) { - return null; - } - try { - const amountBigInt = parseUnits(amount, groupedPosition.loanAssetDecimals); - return previewMarketState(selectedToMarket, amountBigInt, undefined); - } catch { - return null; - } - }, [selectedToMarket, amount, groupedPosition.loanAssetDecimals]); - return (
Add Rebalance Action
-
- {/* Column 1: From → To Market Section */} -
-
- From -
- {selectedFromMarket ? ( - - ) : ( - Select above... - )} -
-
- - - -
- To -
- - {selectedToMarket && onClearToMarket && ( - - )} -
-
-
- - {/* Column 2: APY Preview */} -
- Market APY - {selectedToMarket ? ( - - ) : ( - -- - )} -
- - {/* Column 3: Amount Input + Button */} -
-
- setAmount(e.target.value)} - placeholder="0.0" - className="h-full w-32 rounded-sm bg-transparent px-2 pr-8 text-right text-sm focus:border-primary focus:outline-none" - /> -
- -
-
- - -
-
+
); } diff --git a/app/positions/components/RebalanceActionRow.tsx b/app/positions/components/RebalanceActionRow.tsx new file mode 100644 index 00000000..d6de3fcb --- /dev/null +++ b/app/positions/components/RebalanceActionRow.tsx @@ -0,0 +1,253 @@ +import React, { useMemo } from 'react'; +import { ArrowRightIcon, Cross2Icon, TrashIcon } from '@radix-ui/react-icons'; +import { formatUnits, parseUnits } from 'viem'; +import { Button } from '@/components/common'; +import { MarketIdentity, MarketIdentityMode } from '@/components/MarketIdentity'; +import { TokenIcon } from '@/components/TokenIcon'; +import { previewMarketState } from '@/utils/morpho'; +import { GroupedPosition, Market } from '@/utils/types'; +import { ApyPreview } from './ApyPreview'; +import { UtilizationPreview } from './UtilizationPreview'; + +type RebalanceActionRowMode = 'input' | 'display'; + +type RebalanceActionRowProps = { + mode: RebalanceActionRowMode; + + // Market selection + fromMarket?: Market; + toMarket?: Market; + + // Amount (string for input mode, bigint for display mode) + amount: string | bigint; + + // Common + groupedPosition: GroupedPosition; + + // Input mode specific + onAmountChange?: (amount: string) => void; + onToMarketClick?: () => void; + onClearToMarket?: () => void; + onAddAction?: () => void; + isAddDisabled?: boolean; + + // Display mode specific + onRemoveAction?: () => void; +}; + +/** + * Shared component for displaying rebalance action rows. + * Used in both RebalanceActionInput (input mode) and RebalanceCart (display mode). + * This ensures perfect alignment and consistency between the two contexts. + */ +export function RebalanceActionRow({ + mode, + fromMarket, + toMarket, + amount, + groupedPosition, + onAmountChange, + onToMarketClick, + onClearToMarket, + onAddAction, + isAddDisabled = false, + onRemoveAction, +}: RebalanceActionRowProps) { + // Calculate preview state for the "to" market + const previewState = useMemo(() => { + if (!toMarket || !amount) { + return null; + } + try { + const amountBigInt = + typeof amount === 'string' + ? parseUnits(amount, groupedPosition.loanAssetDecimals) + : amount; + + if (amountBigInt <= 0n) { + return null; + } + + return previewMarketState(toMarket, amountBigInt, undefined); + } catch { + return null; + } + }, [toMarket, amount, groupedPosition.loanAssetDecimals]); + + // Format amount for display + const displayAmount = + typeof amount === 'string' + ? amount + : formatUnits(amount, groupedPosition.loanAssetDecimals); + + return ( +
+ {/* Column 1: From → To Market Section - 50% */} +
+ {/* From Market */} +
+ From +
+ {fromMarket ? ( + + ) : ( + Select above... + )} +
+
+ + + + {/* To Market */} +
+ To +
+ {mode === 'input' ? ( + <> + + {toMarket && onClearToMarket && ( + + )} + + ) : ( +
+ {toMarket ? ( + + ) : ( + Unknown + )} +
+ )} +
+
+
+ + {/* Column 2: APY & Utilization Preview - 25% */} +
+ {/* Market APY */} +
+ APY + {toMarket ? ( + + ) : ( + -- + )} +
+ + {/* Utilization Rate */} +
+ Util + {toMarket ? ( + + ) : ( + -- + )} +
+
+ + {/* Column 3: Amount Input/Display + Button - 25% */} +
+
+ {mode === 'input' ? ( + <> + onAmountChange?.(e.target.value)} + placeholder="0.0" + className="h-full w-32 rounded-sm bg-transparent px-2 pr-8 text-right text-sm focus:border-primary focus:outline-none" + /> +
+ +
+ + ) : ( + <> +
+ {displayAmount} +
+
+ +
+ + )} +
+ + {mode === 'input' ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/app/positions/components/RebalanceCart.tsx b/app/positions/components/RebalanceCart.tsx index b72c1c47..e9af233c 100644 --- a/app/positions/components/RebalanceCart.tsx +++ b/app/positions/components/RebalanceCart.tsx @@ -1,12 +1,7 @@ import React from 'react'; -import { ArrowRightIcon, TrashIcon } from '@radix-ui/react-icons'; -import { formatUnits } from 'viem'; -import { MarketIdentity, MarketIdentityMode } from '@/components/MarketIdentity'; -import { TokenIcon } from '@/components/TokenIcon'; -import { previewMarketState } from '@/utils/morpho'; import { Market } from '@/utils/types'; import { GroupedPosition, RebalanceAction } from '@/utils/types'; -import { ApyPreview } from './ApyPreview'; +import { RebalanceActionRow } from './RebalanceActionRow'; type RebalanceCartProps = { rebalanceActions: RebalanceAction[]; @@ -39,94 +34,19 @@ export function RebalanceCart({ )?.market; const toMarket = eligibleMarkets.find((m) => m.uniqueKey === action.toMarket.uniqueKey); - let apyPreview: ReturnType | null = null; - if (toMarket) { - try { - apyPreview = previewMarketState(toMarket, action.amount, undefined); - } catch { - apyPreview = null; - } - } - return (
- {/* Column 1: From → To Market Section */} -
-
- From -
- {fromMarket ? ( - - ) : ( - Unknown - )} -
-
- - - -
- To -
- {toMarket ? ( - - ) : ( - Unknown - )} -
-
-
- - {/* Column 2: APY Preview */} -
- Market APY - {toMarket ? ( - - ) : ( - -- - )} -
- - {/* Column 3: Amount + Remove Button */} -
-
- - {formatUnits(action.amount, groupedPosition.loanAssetDecimals)} - -
- -
-
- - -
+ removeRebalanceAction(index)} + />
); })} diff --git a/app/positions/components/RebalanceProcessModal.tsx b/app/positions/components/RebalanceProcessModal.tsx index e2e88908..3a58166d 100644 --- a/app/positions/components/RebalanceProcessModal.tsx +++ b/app/positions/components/RebalanceProcessModal.tsx @@ -103,26 +103,24 @@ export function RebalanceProcessModal({ return (
-
+
{status === 'done' ? ( - + ) : status === 'current' ? ( -
+ ) : ( - + )}
-
-
{step.label}
- {status === 'current' && step.detail && ( -
{step.detail}
- )} +
+
{step.label}
+
{step.detail}
); diff --git a/app/positions/components/UtilizationPreview.tsx b/app/positions/components/UtilizationPreview.tsx new file mode 100644 index 00000000..84c16444 --- /dev/null +++ b/app/positions/components/UtilizationPreview.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { MetricPreview } from './MetricPreview'; + +type UtilizationPreviewProps = { + currentUtilization: number; + previewUtilization?: number | null; +}; + +/** + * Utilization preview component. + * Thin wrapper around MetricPreview for utilization-specific usage. + */ +export function UtilizationPreview({ currentUtilization, previewUtilization }: UtilizationPreviewProps) { + return ; +}