diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 83d91caf..160fb7ae 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -398,7 +398,8 @@ src/modals/
/rebalance/
/preview/
/onboarding/
- positions-summary-table.tsx
+ supplied-morpho-blue-grouped-table.tsx
+ collateral-icons-display.tsx
...
/autovault/
autovault-view.tsx
diff --git a/docs/Styling.md b/docs/Styling.md
index 8928e0d2..f1c93ee6 100644
--- a/docs/Styling.md
+++ b/docs/Styling.md
@@ -550,6 +550,76 @@ import { TablePagination } from '@/components/common/TablePagination';
**Styling:** All styling applied via `app/global.css` - don't add inline styles or override padding.
+### TableContainerWithHeader Component
+
+**TableContainerWithHeader** (`@/components/common/table-container-with-header`)
+- Standard wrapper for tables with header section
+- Provides title on left, optional actions on right
+- Consistent styling with border separator, overflow handling, and responsive layout
+
+**Structure:**
+- Outer wrapper with `bg-surface`, `rounded-md`, `font-zen`, `shadow-sm`
+- Header section with border bottom separator
+- Title uses `font-monospace text-xs uppercase text-secondary`
+- Actions area with flexbox gap spacing
+- Overflow-x-auto wrapper for table content
+
+**Props:**
+- `title`: Table heading (e.g., "Asset Activity", "Supplied Positions")
+- `actions?`: Optional React node for controls (filters, refresh, settings dropdowns)
+- `children`: Table component
+- `className?`: Additional classes for outer wrapper
+
+**Usage Examples:**
+
+```tsx
+import { TableContainerWithHeader } from '@/components/common/table-container-with-header';
+import { Table, TableHeader, TableBody } from '@/components/ui/table';
+import { Button } from '@/components/ui/button';
+import { DropdownMenu } from '@/components/ui/dropdown-menu';
+
+// Simple table with title only
+
+
+ {/* table content */}
+
+
+
+// Table with actions (filters, refresh, settings)
+
+
+
+
+
+
+ {/* filter options */}
+
+
+
+
+ >
+ }
+>
+
+ {/* table content */}
+
+
+```
+
+**Examples in codebase:**
+- `src/features/admin/components/asset-metrics-table.tsx`
+- `src/features/positions/components/supplied-morpho-blue-grouped-table.tsx`
+
### TablePagination Component
**TablePagination** (`@/components/common/TablePagination`)
@@ -696,6 +766,53 @@ import { TransactionIdentity } from '@/components/common/TransactionIdentity';
```
+### CollateralIconsDisplay
+
+Use `CollateralIconsDisplay` from `@/features/positions/components/collateral-icons-display` for displaying collateral tokens with smart overflow handling.
+
+**Features:**
+- Shows up to `maxDisplay` collateral icons (default: 8)
+- Overlapping icon style with proper z-index stacking
+- Automatic sorting by amount (descending)
+- "+X more" badge for additional collaterals with tooltip
+- Opacity support for collaterals with zero balance
+
+**Props:**
+- `collaterals`: Array of collateral objects with `address`, `symbol`, `amount`
+- `chainId`: Network chain ID for token icons
+- `maxDisplay?`: Maximum icons to display before showing "+X more" badge (default: 8)
+- `iconSize?`: Size of token icons in pixels (default: 20)
+
+**Usage:**
+
+```tsx
+import { CollateralIconsDisplay } from '@/features/positions/components/collateral-icons-display';
+
+// Basic usage
+
+
+// Custom display limit and icon size
+
+```
+
+**Pattern:**
+This component follows the same overlapping icon pattern as `TrustedByCell` in `trusted-vault-badges.tsx`:
+- First icon has `ml-0`, subsequent icons have `-ml-2` for overlapping effect
+- Z-index decreases from left to right for proper stacking
+- "+X more" badge shows remaining items in tooltip
+- Empty state shows "No known collaterals" message
+
+**Examples in codebase:**
+- `src/features/positions/components/supplied-morpho-blue-grouped-table.tsx`
+
## Input Components
The codebase uses two different input approaches depending on the use case:
diff --git a/src/components/common/table-container-with-header.tsx b/src/components/common/table-container-with-header.tsx
new file mode 100644
index 00000000..d4e4a028
--- /dev/null
+++ b/src/components/common/table-container-with-header.tsx
@@ -0,0 +1,42 @@
+type TableContainerWithHeaderProps = {
+ title: string;
+ actions?: React.ReactNode;
+ children: React.ReactNode;
+ className?: string;
+};
+
+/**
+ * Standard table container with header section.
+ *
+ * Provides consistent styling for tables with:
+ * - Title on the left (uppercase, monospace font)
+ * - Optional actions on the right (filters, refresh, settings, etc.)
+ * - Separator border between header and content
+ * - Responsive overflow handling
+ *
+ * @example
+ *
+ *
+ * ...
+ * >
+ * }
+ * >
+ *
+ *
+ */
+export function TableContainerWithHeader({ title, actions, children, className = '' }: TableContainerWithHeaderProps) {
+ return (
+
+
+
{title}
+ {actions &&
{actions}
}
+
+
{children}
+
+ );
+}
diff --git a/src/components/layout/header/Navbar.tsx b/src/components/layout/header/Navbar.tsx
index 7a3b182d..6a09db65 100644
--- a/src/components/layout/header/Navbar.tsx
+++ b/src/components/layout/header/Navbar.tsx
@@ -11,6 +11,7 @@ import { FaRegMoon } from 'react-icons/fa';
import { FiSettings } from 'react-icons/fi';
import { LuSunMedium } from 'react-icons/lu';
import { RiBookLine, RiDiscordFill, RiGithubFill } from 'react-icons/ri';
+import { TbReport } from 'react-icons/tb';
import { useConnection } from 'wagmi';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { EXTERNAL_LINKS } from '@/utils/external';
@@ -157,6 +158,14 @@ export function Navbar() {
>
GitHub
+ {mounted && address && (
+ }
+ onClick={() => router.push(`/positions/report/${address}`)}
+ >
+ Report
+
+ )}
: )}
onClick={toggleTheme}
diff --git a/src/components/ui/icon-switch.tsx b/src/components/ui/icon-switch.tsx
index f563f8cd..1ae86074 100644
--- a/src/components/ui/icon-switch.tsx
+++ b/src/components/ui/icon-switch.tsx
@@ -149,11 +149,13 @@ export function IconSwitch({
const isControlled = controlledSelected !== undefined;
const isSelected = isControlled ? controlledSelected : internalSelected;
- // Determine which icon to use (null means no icon)
+ // Determine which icon to use (null/undefined means no icon)
const IconComponent = thumbIconOn && thumbIconOff ? (isSelected ? thumbIconOn : thumbIconOff) : ThumbIcon;
// Use compact config for plain switches, icon config otherwise
- const config = IconComponent ? SIZE_CONFIG_WITH_ICON[size] : SIZE_CONFIG_PLAIN[size];
+ // Treat undefined the same as null - no icon
+ const hasIcon = IconComponent !== null && IconComponent !== undefined;
+ const config = hasIcon ? SIZE_CONFIG_WITH_ICON[size] : SIZE_CONFIG_PLAIN[size];
const translate = config.width - config.thumbWidth - config.padding * 2;
const handleToggle = useCallback(() => {
@@ -202,7 +204,7 @@ export function IconSwitch({
onKeyDown={handleKeyDown}
className={cn(
'relative inline-flex shrink-0 items-center justify-start overflow-hidden rounded-[8px] transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background',
- IconComponent && 'ring-1 ring-[var(--color-background-secondary)]',
+ hasIcon && 'ring-1 ring-[var(--color-background-secondary)]',
isSelected ? TRACK_COLOR[color] : 'bg-main',
disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
classNames?.base,
@@ -215,7 +217,7 @@ export function IconSwitch({
- {IconComponent && (
+ {hasIcon && IconComponent && (
-
+ {thumbIconOn && thumbIconOff ? (
+
+ ) : (
+
+ )}
)}
diff --git a/src/data-sources/morpho-api/prices.ts b/src/data-sources/morpho-api/prices.ts
new file mode 100644
index 00000000..893bdd36
--- /dev/null
+++ b/src/data-sources/morpho-api/prices.ts
@@ -0,0 +1,88 @@
+import { assetPricesQuery } from '@/graphql/morpho-api-queries';
+import { morphoGraphqlFetcher } from './fetchers';
+
+// Type for token price input
+export type TokenPriceInput = {
+ address: string;
+ chainId: number;
+};
+
+// Type for asset price response from Morpho API
+type AssetPriceItem = {
+ address: string;
+ symbol: string;
+ decimals: number;
+ chain: {
+ id: number;
+ };
+ priceUsd: number | null;
+};
+
+type AssetPricesResponse = {
+ data: {
+ assets: {
+ items: AssetPriceItem[];
+ };
+ };
+};
+
+// Create a unique key for token prices
+export const getTokenPriceKey = (address: string, chainId: number): string => {
+ return `${address.toLowerCase()}-${chainId}`;
+};
+
+/**
+ * Fetches token prices from Morpho API for a list of tokens
+ * @param tokens - Array of token addresses and chain IDs
+ * @returns Map of token prices keyed by address-chainId
+ */
+export const fetchTokenPrices = async (tokens: TokenPriceInput[]): Promise