Skip to content

Commit c497a73

Browse files
committed
fix(treasury): real grid for the inflows list — aligned columns, headers
The list was a bag of floats wearing a table costume: named rows had no avatar so their text started at the card edge while hex rows started after a 32px identicon, and each badge floated wherever its row's name ended. No column read as a column. - One grid template, five fixed columns (From / Source / Amount / Age / link), shared by a header row exactly like the design reference. - Every row carries an avatar: entity mark when known, the address's identicon otherwise — including named entities with no known logo, so the left edge is a single line. - Amounts, ages and links justify to their column end; badges sit in a fixed 116px column instead of trailing the name. - Below `sm` the same children wrap into the verified mobile layout. Verified in browser: all avatars at one x, all badges at one x, header visible, mobile 390px with zero collisions and no horizontal overflow.
1 parent 77c9626 commit c497a73

3 files changed

Lines changed: 92 additions & 73 deletions

File tree

messages/en/treasury.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,11 @@
8787
"nameWarehouse": "Splits Warehouse",
8888
"nameVault": "Rider vault",
8989
"nameSplit": "Rider split",
90-
"loadFailed": "Couldn't load more — try again"
90+
"loadFailed": "Couldn't load more — try again",
91+
"colFrom": "From",
92+
"colSource": "Source",
93+
"colAmount": "Amount",
94+
"colAge": "Age"
9195
},
9296
"allocation": {
9397
"title": "Allocation",

messages/pt-br/treasury.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,11 @@
8787
"nameWarehouse": "Splits Warehouse",
8888
"nameVault": "Vault do rider",
8989
"nameSplit": "Split do rider",
90-
"loadFailed": "Não deu pra carregar mais — tenta de novo"
90+
"loadFailed": "Não deu pra carregar mais — tenta de novo",
91+
"colFrom": "De",
92+
"colSource": "Origem",
93+
"colAmount": "Valor",
94+
"colAge": "Idade"
9195
},
9296
"allocation": {
9397
"title": "Alocação",

src/components/treasury/TreasuryInflowsList.tsx

Lines changed: 82 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -259,48 +259,39 @@ export function TreasuryInflowsList({
259259
</ul>
260260
) : null}
261261

262+
{/* Column headers, like the reference design — the grid below shares
263+
this exact template, which is what actually aligns the columns. */}
264+
<div className="hidden gap-x-3 border-b border-border pb-2 text-[11px] font-medium text-muted-foreground sm:grid sm:grid-cols-[minmax(0,1.3fr)_116px_minmax(0,1fr)_44px_20px]">
265+
<span>{t("colFrom")}</span>
266+
<span>{t("colSource")}</span>
267+
<span className="text-right">{t("colAmount")}</span>
268+
<span className="text-right">{t("colAge")}</span>
269+
<span aria-hidden />
270+
</div>
271+
262272
<ul className="divide-y divide-border">
263-
{rows.map((flow) => (
264-
// `flex-wrap` is the mobile layout: at 390px a hex sender + badge +
265-
// amount + age don't fit one line, and without the wrap the left
266-
// group's overflow painted OVER the amount. Wrapped, the amount
267-
// group drops to its own right-aligned line; on desktop nothing
268-
// wraps and the row is identical to before.
269-
<li key={flow.hash} className="flex flex-wrap items-center gap-x-3 gap-y-1 py-2.5">
270-
<div className="flex min-w-0 flex-1 basis-40 items-center gap-2">
271-
{(() => {
272-
const entity = knownEntity(flow.from, flow.source, t);
273-
if (!entity)
274-
return (
275-
<AddressDisplay
276-
address={flow.from}
277-
variant="compact"
278-
showCopy={false}
279-
showExplorer={false}
280-
truncateLength={4}
281-
/>
282-
);
283-
// `min-h-8` matches the avatar AddressDisplay renders on hex
284-
// rows: without it a named row is text-height, a hex row is
285-
// avatar-height, and the card's total height becomes a function
286-
// of which senders happen to be on the page — which is the
287-
// instability the fillers below exist to prevent.
288-
return (
289-
<span className="flex min-h-8 min-w-0 items-center gap-2">
290-
{entity.icon ? (
291-
<span className="relative size-8 shrink-0 overflow-hidden rounded-full bg-muted">
292-
{/* Rider cut-outs reuse the sponsorship card's face
293-
crop; square logos just cover; the noggles sit
294-
padded on a dark ground like the design shows. */}
295-
{entity.icon.size ? (
296-
/* eslint-disable-next-line @next/next/no-img-element -- 32px local asset; next/image adds nothing at this size */
273+
{rows.map((flow) => {
274+
const entity = knownEntity(flow.from, flow.source, t);
275+
return (
276+
// One grid, five fixed columns from `sm` up — a row is not a bag of
277+
// floats, and every cell starts where the header says it does.
278+
// Below `sm` the same children wrap: sender+badge line, amount
279+
// right, age+link trailing.
280+
<li
281+
key={flow.hash}
282+
className="flex flex-wrap items-center gap-x-3 gap-y-1 py-2 sm:grid sm:grid-cols-[minmax(0,1.3fr)_116px_minmax(0,1fr)_44px_20px]"
283+
>
284+
<div className="flex min-w-0 flex-1 basis-40 items-center gap-2 sm:flex-none">
285+
{entity ? (
286+
<>
287+
<span className="relative size-8 shrink-0 overflow-hidden rounded-full bg-muted">
288+
{entity.icon ? (
289+
entity.icon.size ? (
290+
/* eslint-disable-next-line @next/next/no-img-element -- 32px local asset; the sponsorship card's verified face crop */
297291
<img
298292
src={entity.icon.src}
299293
alt=""
300294
className="size-full object-cover"
301-
// Same crop the sponsorship card renders — the one
302-
// face-crop treatment that has been verified on
303-
// screen, not a reimplementation of it.
304295
style={{
305296
objectPosition: entity.icon.pos,
306297
scale: String(parseFloat(entity.icon.size) / 100),
@@ -317,45 +308,65 @@ export function TreasuryInflowsList({
317308
: "size-full object-cover"
318309
}
319310
/>
320-
)}
321-
</span>
322-
) : null}
311+
)
312+
) : (
313+
/* A named entity with no known mark still gets ITS
314+
identicon (seeded by address, same generator the
315+
hex rows use) — never a bare text row: one ragged
316+
left edge and the whole column stops reading as a
317+
column. */
318+
/* eslint-disable-next-line @next/next/no-img-element -- tiny remote identicon, same source AddressDisplay uses */
319+
<img
320+
src={`https://api.dicebear.com/7.x/identicon/svg?seed=${flow.from.toLowerCase()}&backgroundColor=b6e3f4,c0aede,d1d4f9`}
321+
alt=""
322+
className="size-full"
323+
/>
324+
)}
325+
</span>
323326
<span className="truncate text-sm">{entity.name}</span>
324-
</span>
325-
);
326-
})()}
327-
{/* Every row carries its origin, not just auctions. The binary
328-
"internal = auction" badge could not say where the other three
329-
quarters of the income came from. */}
330-
<span
331-
className={`inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium ${SOURCE_TONE[flow.source]}`}
332-
title={flow.source === "auction" ? t("auctionHint") : undefined}
333-
>
334-
{flow.source === "auction" ? <Gavel className="size-3" /> : null}
335-
{t(flow.source)}
327+
</>
328+
) : (
329+
<AddressDisplay
330+
address={flow.from}
331+
variant="compact"
332+
showCopy={false}
333+
showExplorer={false}
334+
truncateLength={4}
335+
/>
336+
)}
337+
</div>
338+
339+
<span className="shrink-0">
340+
<span
341+
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium ${SOURCE_TONE[flow.source]}`}
342+
title={flow.source === "auction" ? t("auctionHint") : undefined}
343+
>
344+
{flow.source === "auction" ? <Gavel className="size-3" /> : null}
345+
{t(flow.source)}
346+
</span>
336347
</span>
337-
</div>
338348

339-
<span className="ml-auto shrink-0 font-mono text-sm font-semibold whitespace-nowrap tabular-nums">
340-
+{formatAmount(flow.amount, flow.asset, locale)}{" "}
341-
<span className={ASSET_TONE[flow.asset]}>{flow.asset}</span>
342-
</span>
349+
<span className="ml-auto shrink-0 font-mono text-sm font-semibold whitespace-nowrap tabular-nums sm:ml-0 sm:justify-self-end">
350+
+{formatAmount(flow.amount, flow.asset, locale)}{" "}
351+
<span className={ASSET_TONE[flow.asset]}>{flow.asset}</span>
352+
</span>
343353

344-
<span className="w-9 shrink-0 text-right font-mono text-[11px] text-muted-foreground">
345-
{ageLabel(flow.at, now)}
346-
</span>
354+
<span className="shrink-0 text-right font-mono text-[11px] text-muted-foreground sm:justify-self-end">
355+
{ageLabel(flow.at, now)}
356+
</span>
347357

348-
<a
349-
href={`https://basescan.org/tx/${flow.hash}`}
350-
target="_blank"
351-
rel="noopener noreferrer"
352-
aria-label={t("viewTx")}
353-
className="shrink-0 text-muted-foreground hover:text-foreground"
354-
>
355-
<ExternalLink className="size-3.5" />
356-
</a>
357-
</li>
358-
))}
358+
<a
359+
href={`https://basescan.org/tx/${flow.hash}`}
360+
target="_blank"
361+
rel="noopener noreferrer"
362+
aria-label={t("viewTx")}
363+
className="shrink-0 text-muted-foreground hover:text-foreground sm:justify-self-end"
364+
>
365+
<ExternalLink className="size-3.5" />
366+
</a>
367+
</li>
368+
);
369+
})}
359370

360371
{/* Height reservation. `border-t-transparent` cancels the divider so the
361372
padding shows up as space, not as empty ruled lines. The `h-8` block

0 commit comments

Comments
 (0)