From 8a71dbaff01274ef6beed209a7aca20e9f3bf70d Mon Sep 17 00:00:00 2001 From: Tania Markina Date: Mon, 6 Apr 2026 19:42:27 +0300 Subject: [PATCH 1/5] collapse card --- app/components/common/BaseInstructionCard.tsx | 134 +++++++++--------- .../common/InspectorInstructionCard.tsx | 24 ++-- app/components/inspector/AccountsCard.tsx | 38 ++--- .../inspector/AddressTableLookupsCard.tsx | 61 ++++---- .../inspector/UnknownDetailsCard.tsx | 39 ++--- .../shared/ui/collapsible-card.stories.tsx | 112 +++++++++++++++ app/components/shared/ui/collapsible-card.tsx | 36 +++++ app/components/transaction/AccountsCard.tsx | 105 +++++++------- .../transaction/TokenBalancesCard.tsx | 58 +++----- .../cu-profiling/ui/CUProfilingCard.tsx | 8 +- 10 files changed, 353 insertions(+), 262 deletions(-) create mode 100644 app/components/shared/ui/collapsible-card.stories.tsx create mode 100644 app/components/shared/ui/collapsible-card.tsx diff --git a/app/components/common/BaseInstructionCard.tsx b/app/components/common/BaseInstructionCard.tsx index 0c843a33d..76bf708e5 100644 --- a/app/components/common/BaseInstructionCard.tsx +++ b/app/components/common/BaseInstructionCard.tsx @@ -1,5 +1,6 @@ import { Address } from '@components/common/Address'; import { useScrollAnchor } from '@providers/scroll-anchor'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { cn } from '@shared/utils'; import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js'; import getInstructionCardScrollAnchorId from '@utils/get-instruction-card-scroll-anchor-id'; @@ -46,7 +47,6 @@ export function BaseInstructionCard({ }: InstructionProps) { const [resultClass] = ixResult(result, index); const [showRaw, setShowRaw] = React.useState(defaultRaw || false); - const [expanded, setExpanded] = React.useState(true); const rawClickHandler = () => { if (!defaultRaw && !showRaw && !raw) { // trigger handler to simulate behaviour for the InstructionCard for the transcation which contains logic in it to fetch raw transaction data @@ -59,93 +59,87 @@ export function BaseInstructionCard({ getInstructionCardScrollAnchorId(childIndex != null ? [index + 1, childIndex + 1] : [index + 1]), ); return ( -
-
-

+ #{index + 1} {childIndex !== undefined ? `.${childIndex + 1}` : ''} {title} -

- + + } + headerButtons={
{headerButtons} - {collapsible && ( - - )}
+ } + > +
+ + + {showRaw ? ( + <> + + + + + {'parsed' in ix ? ( + + {raw ? : null} + + ) : ( + + )} + + ) : ( + children + )} + {innerCards && innerCards.length > 0 && ( + <> + + + + + + + + )} + {eventCards && eventCards.length > 0 && ( + <> + + + + + + + + )} + +
Program +
+
Inner Instructions
+ {/* !e-m-0 overrides the 1.5rem margin from inner-cards + so the card aligns with the "Inner Instructions" label above */} +
{innerCards}
+
Events
+
{eventCards}
+
- {expanded && ( -
- - - {showRaw ? ( - <> - - - - - {'parsed' in ix ? ( - - {raw ? : null} - - ) : ( - - )} - - ) : ( - children - )} - {innerCards && innerCards.length > 0 && ( - <> - - - - - - - - )} - {eventCards && eventCards.length > 0 && ( - <> - - - - - - - - )} - -
Program -
-
Inner Instructions
- {/* !e-m-0 overrides the 1.5rem margin from inner-cards - so the card aligns with the "Inner Instructions" label above */} -
{innerCards}
-
Events
-
{eventCards}
-
-
- )} -
+ ); } diff --git a/app/components/common/InspectorInstructionCard.tsx b/app/components/common/InspectorInstructionCard.tsx index 7a7699b9e..57a12f1f3 100644 --- a/app/components/common/InspectorInstructionCard.tsx +++ b/app/components/common/InspectorInstructionCard.tsx @@ -1,5 +1,6 @@ import { ProgramField } from '@entities/instruction-card'; import { useScrollAnchor } from '@providers/scroll-anchor'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { cn } from '@shared/utils'; import { ParsedInstruction, SignatureResult, TransactionInstruction, VersionedMessage } from '@solana/web3.js'; import getInstructionCardScrollAnchorId from '@utils/get-instruction-card-scroll-anchor-id'; @@ -54,24 +55,31 @@ export function InspectorInstructionCard({ ); return ( -
-
-

+ #{index + 1} {childIndex !== undefined ? `.${childIndex + 1}` : ''} {title} -

- + + } + headerButtons={ -
+ } + >
@@ -102,7 +110,7 @@ export function InspectorInstructionCard({
-
+ ); } diff --git a/app/components/inspector/AccountsCard.tsx b/app/components/inspector/AccountsCard.tsx index 24ef888e5..5617fc0ee 100755 --- a/app/components/inspector/AccountsCard.tsx +++ b/app/components/inspector/AccountsCard.tsx @@ -3,7 +3,7 @@ import { ErrorCard } from '@components/common/ErrorCard'; import { TableCardBody } from '@components/common/TableCardBody'; import { type AccountInfo, useAccountsInfo } from '@entities/account'; import { useCluster } from '@providers/cluster'; -import { cn } from '@shared/utils'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { PublicKey, VersionedMessage } from '@solana/web3.js'; import React, { useMemo } from 'react'; @@ -12,7 +12,6 @@ import { toHex } from '@/app/shared/lib/bytes'; import { AddressFromLookupTableWithContext, AddressWithContext } from './AddressWithContext'; export function AccountsCard({ message }: { message: VersionedMessage }) { - const [expanded, setExpanded] = React.useState(true); const { url } = useCluster(); const pubkeys = useMemo(() => message.staticAccountKeys, [message.staticAccountKeys]); @@ -113,32 +112,17 @@ export function AccountsCard({ message }: { message: VersionedMessage }) { } return ( -
-
-

{`Account List (${numAccounts})`}

- -
- {expanded && ( - <> - {accountRows} - {!loading && totalAccountSize > 0 && ( -
-
- - Total Account Size: - - {totalAccountSize.toLocaleString('en-US')} bytes -
-
- )} - + + {accountRows} + {!loading && totalAccountSize > 0 && ( +
+
+ Total Account Size: + {totalAccountSize.toLocaleString('en-US')} bytes +
+
)} -
+ ); } diff --git a/app/components/inspector/AddressTableLookupsCard.tsx b/app/components/inspector/AddressTableLookupsCard.tsx index 37857a25f..318d5a82c 100755 --- a/app/components/inspector/AddressTableLookupsCard.tsx +++ b/app/components/inspector/AddressTableLookupsCard.tsx @@ -1,13 +1,11 @@ import { Address } from '@components/common/Address'; import { useAddressLookupTable } from '@providers/accounts'; import { FetchStatus } from '@providers/cache'; -import { cn } from '@shared/utils'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { PublicKey, VersionedMessage } from '@solana/web3.js'; import React from 'react'; export function AddressTableLookupsCard({ message }: { message: VersionedMessage }) { - const [expanded, setExpanded] = React.useState(true); - const lookupRows = React.useMemo(() => { let key = 0; return message.addressTableLookups.flatMap(lookup => { @@ -32,42 +30,31 @@ export function AddressTableLookupsCard({ message }: { message: VersionedMessage if (message.version === 'legacy') return null; return ( -
-
-

Address Table Lookup(s)

- -
- {expanded && ( -
- - + +
+
+ + + + + + + + + {lookupRows.length > 0 ? ( + {lookupRows} + ) : ( + - - - - + - - {lookupRows.length > 0 ? ( - {lookupRows} - ) : ( - - - - - - )} -
Address Lookup Table AddressTable IndexResolved AddressDetails
Address Lookup Table AddressTable IndexResolved AddressDetails + No entries found +
- No entries found -
-
- )} -
+ + )} + +
+ ); } diff --git a/app/components/inspector/UnknownDetailsCard.tsx b/app/components/inspector/UnknownDetailsCard.tsx index 12cf744d6..a5231ad7d 100644 --- a/app/components/inspector/UnknownDetailsCard.tsx +++ b/app/components/inspector/UnknownDetailsCard.tsx @@ -1,9 +1,8 @@ import { TableCardBody } from '@components/common/TableCardBody'; import { ProgramField } from '@entities/instruction-card'; import { useScrollAnchor } from '@providers/scroll-anchor'; -import { cn } from '@shared/utils'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { TransactionInstruction } from '@solana/web3.js'; -import React from 'react'; import getInstructionCardScrollAnchorId from '@/app/utils/get-instruction-card-scroll-anchor-id'; @@ -18,31 +17,23 @@ export function UnknownDetailsCard({ ix: TransactionInstruction; programName: string; }) { - const [expanded, setExpanded] = React.useState(false); - const scrollAnchorRef = useScrollAnchor(getInstructionCardScrollAnchorId([index + 1])); return ( -
-
-

- #{index + 1} + + #{index + 1} {programName} Instruction -

- - -
- {expanded && ( - - - - - )} -
+ + } + > + + + + + ); } diff --git a/app/components/shared/ui/collapsible-card.stories.tsx b/app/components/shared/ui/collapsible-card.stories.tsx new file mode 100644 index 000000000..a298d3309 --- /dev/null +++ b/app/components/shared/ui/collapsible-card.stories.tsx @@ -0,0 +1,112 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { CollapsibleCard } from './collapsible-card'; + +const meta: Meta = { + argTypes: { + collapsible: { + control: 'boolean', + }, + defaultExpanded: { + control: 'boolean', + }, + }, + component: CollapsibleCard, + decorators: [ + Story => ( +
+ +
+ ), + ], + parameters: { + layout: 'centered', + }, + title: 'Components/Shared/UI/CollapsibleCard', +}; + +export default meta; +type Story = StoryObj; + +const SampleContent = () => ( +
+ + + + + + + + + + + + + + + + + + + + + +
NameValue
Account #1Gzf3…k9Pq
Account #25xRt…mN7v
Account #3BqWu…dL2j
+
+); + +export const Default: Story = { + args: {} as never, + render: () => ( + + + + ), +}; + +export const StartsCollapsed: Story = { + args: {} as never, + render: () => ( + + + + ), +}; + +export const WithHeaderButtons: Story = { + args: {} as never, + render: () => ( + Raw} + > + + + ), +}; + +export const NonCollapsible: Story = { + args: {} as never, + render: () => ( + + + + ), +}; + +export const WithBadgeTitle: Story = { + args: {} as never, + render: () => ( + + #1 + Token Program: Transfer + + } + > + + + ), +}; diff --git a/app/components/shared/ui/collapsible-card.tsx b/app/components/shared/ui/collapsible-card.tsx new file mode 100644 index 000000000..d71bcd1bf --- /dev/null +++ b/app/components/shared/ui/collapsible-card.tsx @@ -0,0 +1,36 @@ +import { cn } from '@shared/utils'; +import React from 'react'; + +type CollapsibleCardProps = { + title: React.ReactNode; + children: React.ReactNode; + defaultExpanded?: boolean; + className?: string; + headerButtons?: React.ReactNode; + collapsible?: boolean; +}; + +export const CollapsibleCard = React.forwardRef( + ({ title, children, defaultExpanded = true, className, headerButtons, collapsible = true }, ref) => { + const [expanded, setExpanded] = React.useState(defaultExpanded); + + return ( +
+
+

{title}

+ {headerButtons} + {collapsible && ( + + )} +
+ {(!collapsible || expanded) && children} +
+ ); + }, +); +CollapsibleCard.displayName = 'CollapsibleCard'; diff --git a/app/components/transaction/AccountsCard.tsx b/app/components/transaction/AccountsCard.tsx index 7eba9a504..10f67df1b 100644 --- a/app/components/transaction/AccountsCard.tsx +++ b/app/components/transaction/AccountsCard.tsx @@ -7,6 +7,7 @@ import { type AccountInfo, useAccountsInfo } from '@entities/account'; import { useCluster } from '@providers/cluster'; import { useTransactionDetails } from '@providers/transactions'; import { Button } from '@shared/ui/button'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { cn } from '@shared/utils'; import { PublicKey } from '@solana/web3.js'; import { SignatureProps } from '@utils/index'; @@ -21,7 +22,6 @@ export function AccountsCard({ signature }: SignatureProps) { const details = useTransactionDetails(signature); const { url } = useCluster(); const [showRaw, setShowRaw] = useState(false); - const [expanded, setExpanded] = useState(true); const transactionWithMeta = details?.data?.transactionWithMeta; const message = transactionWithMeta?.transaction.message; @@ -100,71 +100,64 @@ export function AccountsCard({ signature }: SignatureProps) { }); return ( -
-
-

{`Account Input(s) (${message.accountKeys.length})`}

+ setShowRaw(r => !r)} > Raw - -
- {expanded && - (showRaw ? ( -
- -
- ) : ( -
- - + } + > + {showRaw ? ( +
+ +
+ ) : ( +
+
+ + + + + + + + + + + {accountRows} + {totalAccountSize > 0 && ( + - - - - - - + + + + - - {accountRows} - {totalAccountSize > 0 && ( - - - - - - - - )} -
#AddressChange (SOL)Post Balance (SOL)Size (bytes)Details
#AddressChange (SOL)Post Balance (SOL)Size (bytes)Details +

+ reflects current account state +

+
+

+ Total Account Size: +

+
+ + {totalAccountSize.toLocaleString('en-US')} + +
-

- reflects current account state -

-
-

- Total Account Size: -

-
- - {totalAccountSize.toLocaleString('en-US')} - - -
-
- ))} -
+ + )} + + + )} + ); } diff --git a/app/components/transaction/TokenBalancesCard.tsx b/app/components/transaction/TokenBalancesCard.tsx index 17fb6a16b..14f73c237 100644 --- a/app/components/transaction/TokenBalancesCard.tsx +++ b/app/components/transaction/TokenBalancesCard.tsx @@ -1,7 +1,7 @@ import { Address } from '@components/common/Address'; import { BalanceDelta } from '@components/common/BalanceDelta'; import { useTransactionDetails } from '@providers/transactions'; -import { cn } from '@shared/utils'; +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { ParsedMessageAccount, PublicKey, TokenBalance } from '@solana/web3.js'; import { SignatureProps } from '@utils/index'; import { BigNumber } from 'bignumber.js'; @@ -54,7 +54,6 @@ export type TokenBalancesCardInnerProps = { export function TokenBalancesCardInner({ rows }: TokenBalancesCardInnerProps) { const { cluster, url } = useCluster(); const [tokenSymbols, setTokenSymbols] = useState>(new Map()); - const [expanded, setExpanded] = useState(true); useAsyncEffect(async isMounted => { const mints = rows.map(r => new PublicKey(r.mint)); @@ -66,40 +65,29 @@ export function TokenBalancesCardInner({ rows }: TokenBalancesCardInnerProps) { }, []); return ( -
-
-

Token Balances

- + +
+ + + + + + + + + + + {rows.map(row => ( + + ))} + +
AddressTokenChangePost Balance
- {expanded && ( -
- - - - - - - - - - - {rows.map(row => ( - - ))} - -
AddressTokenChangePost Balance
-
- )} -
+ ); } diff --git a/app/features/cu-profiling/ui/CUProfilingCard.tsx b/app/features/cu-profiling/ui/CUProfilingCard.tsx index 75cf0c041..e36e02476 100644 --- a/app/features/cu-profiling/ui/CUProfilingCard.tsx +++ b/app/features/cu-profiling/ui/CUProfilingCard.tsx @@ -1,3 +1,4 @@ +import { CollapsibleCard } from '@shared/ui/collapsible-card'; import { InstructionCUData } from '@utils/cu-profiling'; import { BarElement, CategoryScale, Chart, ChartData, ChartOptions, LinearScale, Tooltip } from 'chart.js'; import React from 'react'; @@ -241,10 +242,7 @@ export function CUProfilingCard({ instructions, unitsConsumed }: CUProfilingCard if (instructions.length === 0) return null; return ( -
-
-

CU profiling

-
+
{Boolean(unitsConsumed) &&
Total: {unitsConsumed?.toLocaleString()} CU
} @@ -278,6 +276,6 @@ export function CUProfilingCard({ instructions, unitsConsumed }: CUProfilingCard })}
-
+ ); } From 23b10a48cfa561b18ad055052fa46dd5fa5eca11 Mon Sep 17 00:00:00 2001 From: Tania Markina Date: Tue, 7 Apr 2026 10:42:26 +0300 Subject: [PATCH 2/5] chore: build info --- bench/BUILD.md | 68 +++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/bench/BUILD.md b/bench/BUILD.md index d83aae058..f13abeb8f 100644 --- a/bench/BUILD.md +++ b/bench/BUILD.md @@ -2,38 +2,38 @@ | Type | Route | Size | First Load JS | |------|-------|------|---------------| -| Static | `/` | 20 kB | 1.12 MB | -| Static | `/_not-found` | 330 B | 170 kB | -| Dynamic | `/address/[address]` | 20 kB | 1.03 MB | -| Dynamic | `/address/[address]/anchor-account` | 10 kB | 1.09 MB | -| Dynamic | `/address/[address]/anchor-program` | 340 B | 950 kB | -| Dynamic | `/address/[address]/attestation` | 10 kB | 1.06 MB | -| Dynamic | `/address/[address]/attributes` | 10 kB | 1.01 MB | -| Dynamic | `/address/[address]/blockhashes` | 10 kB | 1.01 MB | -| Dynamic | `/address/[address]/compression` | 10 kB | 1.05 MB | -| Dynamic | `/address/[address]/concurrent-merkle-tree` | 10 kB | 1.04 MB | -| Dynamic | `/address/[address]/domains` | 10 kB | 1.03 MB | -| Dynamic | `/address/[address]/entries` | 10 kB | 1.03 MB | -| Dynamic | `/address/[address]/feature-gate` | 340 B | 950 kB | -| Dynamic | `/address/[address]/idl` | 130 kB | 1.27 MB | -| Dynamic | `/address/[address]/instructions` | 10 kB | 1.13 MB | -| Dynamic | `/address/[address]/metadata` | 10 kB | 1.03 MB | -| Dynamic | `/address/[address]/nftoken-collection-nfts` | 10 kB | 1.10 MB | -| Dynamic | `/address/[address]/program-multisig` | 10 kB | 1.08 MB | -| Dynamic | `/address/[address]/rewards` | 10 kB | 1.02 MB | -| Dynamic | `/address/[address]/security` | 10 kB | 1.08 MB | -| Dynamic | `/address/[address]/slot-hashes` | 10 kB | 1.01 MB | -| Dynamic | `/address/[address]/stake-history` | 10 kB | 1.02 MB | -| Dynamic | `/address/[address]/token-extensions` | 10 kB | 1.10 MB | -| Dynamic | `/address/[address]/tokens` | 30 kB | 1.21 MB | -| Dynamic | `/address/[address]/transfers` | 10 kB | 1.15 MB | -| Dynamic | `/address/[address]/verified-build` | 10 kB | 1.08 MB | -| Dynamic | `/address/[address]/vote-history` | 10 kB | 1.01 MB | +| Static | `/` | 15 kB | 1.03 MB | +| Static | `/_not-found` | 326 B | 164 kB | +| Dynamic | `/address/[address]` | 14 kB | 975 kB | +| Dynamic | `/address/[address]/anchor-account` | 8 kB | 1.01 MB | +| Dynamic | `/address/[address]/anchor-program` | 337 B | 893 kB | +| Dynamic | `/address/[address]/attestation` | 7 kB | 984 kB | +| Dynamic | `/address/[address]/attributes` | 3 kB | 938 kB | +| Dynamic | `/address/[address]/blockhashes` | 3 kB | 937 kB | +| Dynamic | `/address/[address]/compression` | 6 kB | 973 kB | +| Dynamic | `/address/[address]/concurrent-merkle-tree` | 5 kB | 972 kB | +| Dynamic | `/address/[address]/domains` | 3 kB | 940 kB | +| Dynamic | `/address/[address]/entries` | 4 kB | 958 kB | +| Dynamic | `/address/[address]/feature-gate` | 335 B | 893 kB | +| Dynamic | `/address/[address]/idl` | 124 kB | 1.2 MB | +| Dynamic | `/address/[address]/instructions` | 2 kB | 1.05 MB | +| Dynamic | `/address/[address]/metadata` | 8 kB | 953 kB | +| Dynamic | `/address/[address]/nftoken-collection-nfts` | 10 kB | 1.02 MB | +| Dynamic | `/address/[address]/program-multisig` | 5 kB | 1.01 MB | +| Dynamic | `/address/[address]/rewards` | 5 kB | 942 kB | +| Dynamic | `/address/[address]/security` | 10 kB | 1.02 MB | +| Dynamic | `/address/[address]/slot-hashes` | 5 kB | 941 kB | +| Dynamic | `/address/[address]/stake-history` | 5 kB | 942 kB | +| Dynamic | `/address/[address]/token-extensions` | 14 kB | 1.02 MB | +| Dynamic | `/address/[address]/tokens` | 27 kB | 1.13 MB | +| Dynamic | `/address/[address]/transfers` | 3 kB | 1.07 MB | +| Dynamic | `/address/[address]/verified-build` | 8 kB | 1.01 MB | +| Dynamic | `/address/[address]/vote-history` | 5 kB | 941 kB | | Dynamic | `/api/anchor` | 0 B | 0 B | | Dynamic | `/api/ans-domains/[address]` | 0 B | 0 B | | Dynamic | `/api/domain-info/[domain]` | 0 B | 0 B | | Dynamic | `/api/geo-location` | 0 B | 0 B | -| Dynamic | `/api/metadata/proxy` | 0 B | 0 B | +| Static | `/api/metadata/proxy` | 0 B | 0 B | | Dynamic | `/api/ping/[network]` | 0 B | 0 B | | Dynamic | `/api/program-metadata-idl` | 0 B | 0 B | | Dynamic | `/api/receipt/price/[mintAddress]` | 0 B | 0 B | @@ -54,9 +54,9 @@ | Dynamic | `/og/feature-gate/[address]` | 0 B | 0 B | | Dynamic | `/og/receipt/[signature]` | 0 B | 0 B | | Static | `/opengraph-image.png` | 0 B | 0 B | -| Static | `/supply` | 10 kB | 1.04 MB | -| Static | `/tos` | 330 B | 170 kB | -| Dynamic | `/tx/[signature]` | 60 kB | 1.53 MB | -| Dynamic | `/tx/[signature]/inspect` | 620 B | 1.29 MB | -| Static | `/tx/inspector` | 630 B | 1.29 MB | -| Static | `/verified-programs` | 10 kB | 180 kB | \ No newline at end of file +| Static | `/supply` | 7 kB | 948 kB | +| Static | `/tos` | 325 B | 164 kB | +| Dynamic | `/tx/[signature]` | 58 kB | 1.47 MB | +| Dynamic | `/tx/[signature]/inspect` | 628 B | 1.23 MB | +| Static | `/tx/inspector` | 632 B | 1.23 MB | +| Static | `/verified-programs` | 7 kB | 173 kB | From 372329f9cb54c68a50ab2750ab624d98a571558b Mon Sep 17 00:00:00 2001 From: Tania Markina Date: Mon, 13 Apr 2026 12:18:09 +0300 Subject: [PATCH 3/5] resolve comments --- app/components/shared/ui/collapsible-card.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/shared/ui/collapsible-card.tsx b/app/components/shared/ui/collapsible-card.tsx index d71bcd1bf..b8f9e9998 100644 --- a/app/components/shared/ui/collapsible-card.tsx +++ b/app/components/shared/ui/collapsible-card.tsx @@ -15,12 +15,13 @@ export const CollapsibleCard = React.forwardRef +

{title}

{headerButtons} {collapsible && (