From b97e73b7fc2ca232330824705e509e121aa34178 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Fri, 14 Nov 2025 17:31:23 -0300 Subject: [PATCH 1/3] feat: util preview on rebalacne modal --- app/positions/components/ApyPreview.tsx | 42 ++- app/positions/components/FromMarketsTable.tsx | 41 ++- .../components/RebalanceActionInput.tsx | 139 +--------- .../components/RebalanceActionRow.tsx | 253 ++++++++++++++++++ app/positions/components/RebalanceCart.tsx | 100 +------ .../components/UtilizationPreview.tsx | 50 ++++ 6 files changed, 390 insertions(+), 235 deletions(-) create mode 100644 app/positions/components/RebalanceActionRow.tsx create mode 100644 app/positions/components/UtilizationPreview.tsx diff --git a/app/positions/components/ApyPreview.tsx b/app/positions/components/ApyPreview.tsx index ab7eed06..9fa30367 100644 --- a/app/positions/components/ApyPreview.tsx +++ b/app/positions/components/ApyPreview.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import { Tooltip } from '@heroui/react'; import { formatReadable } from '@/utils/balance'; +import { TooltipContent } from '@/components/TooltipContent'; type ApyPreviewProps = { currentApy: number; @@ -8,25 +10,41 @@ 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. + * Shows preview APY if available, otherwise shows current APY. + * Displays tooltip on hover showing before/after values. */ 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' : ''}`; + // Show preview value if available, otherwise show current value + const displayValue = hasPreview ? formattedPreview : formattedCurrent; + + if (!hasPreview) { + return ( + + {displayValue}% + + ); + } return ( - - {formattedCurrent}% - {hasPreview && ( - <> - {' → '} - {formattedPreview}% - - )} - + + } + > + + {displayValue}% + + ); } 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/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/UtilizationPreview.tsx b/app/positions/components/UtilizationPreview.tsx new file mode 100644 index 00000000..4ab69af3 --- /dev/null +++ b/app/positions/components/UtilizationPreview.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Tooltip } from '@heroui/react'; +import { formatReadable } from '@/utils/balance'; +import { TooltipContent } from '@/components/TooltipContent'; + +type UtilizationPreviewProps = { + currentUtilization: number; + previewUtilization?: number | null; +}; + +/** + * Standardized Utilization preview component. + * Shows preview utilization if available, otherwise shows current utilization. + * Displays tooltip on hover showing before/after values. + */ +export function UtilizationPreview({ currentUtilization, previewUtilization }: UtilizationPreviewProps) { + const formattedCurrent = formatReadable(currentUtilization * 100); + const formattedPreview = previewUtilization ? formatReadable(previewUtilization * 100) : null; + const hasPreview = Boolean(previewUtilization && formattedPreview); + + // Show preview value if available, otherwise show current value + const displayValue = hasPreview ? formattedPreview : formattedCurrent; + + if (!hasPreview) { + return ( + + {displayValue}% + + ); + } + + return ( + + } + > + + {displayValue}% + + + ); +} From 5b76703b37d701a0420fdca51e3892b2e73f2ccb Mon Sep 17 00:00:00 2001 From: antoncoding Date: Fri, 14 Nov 2025 17:33:21 -0300 Subject: [PATCH 2/3] chore: process modal style --- .../components/RebalanceProcessModal.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) 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}
); From 8b928820d38f85947abd55b825cf2e729ecb81df Mon Sep 17 00:00:00 2001 From: antoncoding Date: Fri, 14 Nov 2025 17:42:50 -0300 Subject: [PATCH 3/3] refactor: preview components --- app/positions/components/ApyPreview.tsx | 43 ++-------------- app/positions/components/MetricPreview.tsx | 51 +++++++++++++++++++ .../components/UtilizationPreview.tsx | 43 ++-------------- 3 files changed, 59 insertions(+), 78 deletions(-) create mode 100644 app/positions/components/MetricPreview.tsx diff --git a/app/positions/components/ApyPreview.tsx b/app/positions/components/ApyPreview.tsx index 9fa30367..221f003a 100644 --- a/app/positions/components/ApyPreview.tsx +++ b/app/positions/components/ApyPreview.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import { Tooltip } from '@heroui/react'; -import { formatReadable } from '@/utils/balance'; -import { TooltipContent } from '@/components/TooltipContent'; +import { MetricPreview } from './MetricPreview'; type ApyPreviewProps = { currentApy: number; @@ -9,42 +7,9 @@ type ApyPreviewProps = { }; /** - * Standardized APY preview component. - * Shows preview APY if available, otherwise shows current APY. - * Displays tooltip on hover showing before/after values. + * 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); - - // Show preview value if available, otherwise show current value - const displayValue = hasPreview ? formattedPreview : formattedCurrent; - - if (!hasPreview) { - return ( - - {displayValue}% - - ); - } - - return ( - - } - > - - {displayValue}% - - - ); + return ; } 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/UtilizationPreview.tsx b/app/positions/components/UtilizationPreview.tsx index 4ab69af3..84c16444 100644 --- a/app/positions/components/UtilizationPreview.tsx +++ b/app/positions/components/UtilizationPreview.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import { Tooltip } from '@heroui/react'; -import { formatReadable } from '@/utils/balance'; -import { TooltipContent } from '@/components/TooltipContent'; +import { MetricPreview } from './MetricPreview'; type UtilizationPreviewProps = { currentUtilization: number; @@ -9,42 +7,9 @@ type UtilizationPreviewProps = { }; /** - * Standardized Utilization preview component. - * Shows preview utilization if available, otherwise shows current utilization. - * Displays tooltip on hover showing before/after values. + * Utilization preview component. + * Thin wrapper around MetricPreview for utilization-specific usage. */ export function UtilizationPreview({ currentUtilization, previewUtilization }: UtilizationPreviewProps) { - const formattedCurrent = formatReadable(currentUtilization * 100); - const formattedPreview = previewUtilization ? formatReadable(previewUtilization * 100) : null; - const hasPreview = Boolean(previewUtilization && formattedPreview); - - // Show preview value if available, otherwise show current value - const displayValue = hasPreview ? formattedPreview : formattedCurrent; - - if (!hasPreview) { - return ( - - {displayValue}% - - ); - } - - return ( - - } - > - - {displayValue}% - - - ); + return ; }