Skip to content

One row rhythm across the dashboard, and a transaction row that stops lying - #227

Merged
henriqueaklein merged 6 commits into
developfrom
redesign/row-rhythm-260808
Aug 10, 2026
Merged

One row rhythm across the dashboard, and a transaction row that stops lying#227
henriqueaklein merged 6 commits into
developfrom
redesign/row-rhythm-260808

Conversation

@SirYakoob

Copy link
Copy Markdown

Three panels on the dashboard drew the same kind of row three different ways, and the transaction row told the reader things that were not true. Walked on a physical iPhone throughout.

What a user hits

The Transactions panel lied about how much history it had. It renders the five most recent rows and leaves the rest behind View all, and then printed "No more transactions" underneath. A wallet with forty transactions showed five and claimed that was all of them. The terminus now belongs to the page, which does not truncate.

A BTC row fell out of line with its neighbours. The second line was gated on a price lookup succeeding, and _fiatLine returns null whenever the price map has no entry. The row shrank by only 0.857px, which is why it read as a non-defect - but Row centres its children, so the amount itself dropped 11.571px out of the column every other row shares. The slot is now always occupied, reading No price when there is no price.

The amount's colour said the same thing twice, in one direction only. A credit was green, a debit was plain white. statusSuccess against textPrimary measures 1.86:1 - under a third of WCAG 1.4.11's 3:1 bar - while + against - is a shape at 19.29:1, and the badge already states direction, in colour, in both directions. The tint was a fourth statement of a fact stated three times. Colour on the amount now means status: pending, failed, not charged.

The type scale had been shrunk on every phone surface by accident. develop's density pass was gated on !useDesktopLayout(context), which reads the window, so on a phone it was true on the dashboard panel as well as the page it was written for. Title rendered 14 against titleMd's 16, subtitle 11, amount 13, the identity glyph 28 against 40. The type-scale half is gone; the density half - padding, the dropped time column, the 44pt filter-bar touch targets - stays.

The row lead was the same ink as the title 2px above it (1.00:1). It arrived there in 0cd2889, which moved those four strings out of a surfaceMenu chip. Now textPrimary70.

Transactions, Assets and Markets now draw one row. Measured before deciding, ink to ink, at 390pt:

gap above rule gap below symmetric
Transactions 4.00 4.00 yes
Markets 13.25 16.75 no
Assets 15.63 20.00 no

Only Transactions was a decision. The other two inherited Material's ListTile snap - Markets declared no contentPadding at all - which is why the two undeclared ones were the asymmetric ones. All three are now wall 8, leading glyph 38, glyph to text 8, right wall 8, rule 12/1/12.

Notes for the reviewer

space6 = 12 was not invented for this. It is the midpoint of the measured [4, 20] range, on the 4-pt grid, and was already shipping symmetrically at three other row lists.

The ListTile had to be replaced rather than tuned. Its contentPadding is charged inside the tile snap and cannot subtract from it - Assets declared vertical: 4 and still rendered 15.63/20.00. Reaching 12/12 through the tile would have meant driving five knobs to land on a number no reader could predict from source, which is the condition being removed.

The value line's colour keys off the Not charged sentinel rather than a status test. orderRowContent deliberately folds an unrecognised Banxa status onto cancelled while keeping the real fiat number, so a status test would have painted an honest number red. Pinned by a test.

The two ROW INSET cases in gw_section_title_rhythm_test.dart asserted C > kMaxAbsorbableInset because the snap existed. They characterised a Material default this branch deletes, so they now assert the absorbed condition. R1 = 12 and R2 = 26 do not move - Markets improves from a rendered 26.75 to exactly 26.

gw_row_rhythm_test.dart measures painted ink rather than reading source, because every number it replaces was invisible in source.

Verification

flutter test 1207 passed, 3 skipped, 0 failed
flutter analyze No issues found
dart format lib test 398 files, 0 changed
check_brace_style pass
check_raw_colors pass
check_onboarding_seed_safety pass
check_no_new_key_logging --scan-tree pass
check_agent_rules_sync fails, and fails on origin/develop too

check_agent_rules_sync is pre-existing: AGENTS.md and its mirror have drifted on the "Not lazy about" paragraph. This branch touches neither file, and the gate was verified failing on a clean origin/develop worktree. Not fixed here because it is unrelated to this change.

Deliberately not here

The transaction detail drawer still gates its own value line. It inherits the amount tone correctly but does not get No price. Found while applying scheme A; left out because the drawer is a different surface with a different width budget and deserves its own look.

The dashboard panel's bottom padding is still asymmetric. The distance from the box top to its title is owned by GWSectionTitle; the distance from the last row to the bottom edge is owned by nothing, so it is whatever the last widget happens to declare - a button's fixed height, a row's own padding, an unmeasured ListTile snap. Planned as quick 260807-v6m and deliberately not executed: its measurements were taken before this branch changed the rows underneath it, so its call-site values are stale and have to be re-measured first.

Three tightenings that fall out of the rhythm change, none of them chosen, all walked on device: the Assets total band to first row goes 20 to 12, the /assets kicker to first row goes 28 to 20, and a zero-balance Assets row gives the token name about 27px less width than ListTile used to. The long-balance case gets more room than before; the split is now deterministic rather than varying with the trailing content's width.

settings_screen.dart and order_card.dart still draw dividers that do not pin their colour, so they inherit dividerTheme, which resolves to surfaceMenu - a fill colour, not a border colour. That is a colour bug, not a rhythm bug, and is out of scope here.

Scheme C from sketch 186 was rejected on a fact, not on taste. fiatValue() multiplies by currentPrice, so a value-first hierarchy would apply today's price to a past transaction and make the row's least reliable number its headline, on a screen that exists as a historical record.

Jakub, on device: the Transactions section does not respect the structure
Assets and Markets have - different gaps around the separator, a different
wall, the logo in a different place. Find a sweet spot between them and roll
it out across the app.

Measured first, ink to ink, at 390pt. Only one of the three was a decision:

  Transactions   4.00 / 4.00    symmetric, declared
  Markets       13.25 / 16.75   asymmetric, 100% Material's bare ListTile
  Assets        15.63 / 20.00   asymmetric, vertical:4 plus 27.62 of snap

So "adopt the Assets structure" would have meant adopting a default nobody
chose, and it is why two of the three were asymmetric while the declared one
was exact. Jakub picked Assets' geometry and 12 for the rule.

`space6` = 12 is the midpoint of the [4, 20] range, on the 4-pt grid, and was
already shipping symmetrically at three other row lists, so the value is one
this app had settled on rather than a new one.

The ListTile had to go rather than be tuned: its contentPadding is charged
INSIDE the tile snap and cannot subtract from it - Assets declared vertical:4
and still rendered 15.63/20.00. Hitting 12/12 through the tile would have
meant driving five knobs to produce a number no reader could predict from
source, which is the disease being cured.

`gw_row_rhythm.dart` holds the four values; `gw_row_rhythm_test.dart` measures
PAINTED INK rather than reading source, because the numbers this replaces were
invisible in source.

Also aligned dashboard_markets.dart's rule from a bare `Container` to the
`Divider` form every other list uses - the same pixel drawn by two widgets,
recorded as unresolved in sketch 019.

The two ROW INSET cases in gw_section_title_rhythm_test.dart asserted
`C > kMaxAbsorbableInset` BECAUSE the snap existed. They characterised a
Material default this commit deletes, so they now assert the absorbed
condition instead. R1=12 and R2=26 do not move; Markets improves from a
rendered 26.75 to exactly 26.
…d at 70%

Three of Jakub's calls on the transaction row, in one file because they
interleave in it.

TYPE SCALE. develop's 260806-hfe gated a phone density pass on
`!useDesktopLayout(context)`, which reads the WINDOW, so on a phone it was
true on every surface - the dashboard panel included, which was never its
target. Title rendered 14 against titleMd's 16, subtitle 11 against bodySm,
amount 13 against 16, the identity glyph 28 against 40. Jakub's call was to
remove the type-scale half everywhere rather than thread density from the
call site; the density half - padding, the dropped time column - stays.

Note the recorded value was wrong where it was written down: bodySm is 14 and
always has been. The 13px floor belongs to labelMd. Deleting the override and
letting the token govern is right either way, and it renders one step above
that floor rather than on it.

AMOUNT. `_toneColor` gave `incoming` statusSuccess and `outgoing` textPrimary,
so direction was carried by colour in one direction only, and stated twice on
a credit and once on a debit. statusSuccess against textPrimary measures
1.86:1 - under a third of 1.4.11's 3:1 - while `+` against `-` is a shape at
19.29:1, and the badge already states direction, in colour, in both. The tint
was a fourth statement of a fact stated three times. It is gone, and colour
now means STATUS, which a sign cannot carry.

VALUE LINE. It was gated on `valueLine != null`, and `_fiatLine` returns null
whenever the price map has no entry - so a BTC row silently lost its second
line. The row shrank only 0.857px, which is why it read as a non-defect, but
`Row` centres its children, so the amount DROPPED 11.571px out of the column
its neighbours share. The slot is now always occupied, reading `No price`.

The value line's colour keys off the `Not charged` sentinel rather than a
literal port of `isDead`, which is a local inside `txRowContent` and not a
field. A literal port would have painted a Banxa order's honest fiat number
red, because `orderRowContent` deliberately folds an unrecognised status onto
`cancelled` while keeping the real value. Pinned by a test.

LEAD. `Sent` / `Processing job` / `Card purchase` sat in textPrimary, the same
ink and weight as the title 2px above it - 1.00:1. They arrived there in
0cd2889, which moved them out of a surfaceMenu chip. Now textPrimary70, the
fallback the file already named by hand for this case.
… panel

`endOfTransactionsLabel` was appended unconditionally in the list branch. The
dashboard panel passes `limit: kDashboardTransactionsCap`, so it renders the
five most recent rows and leaves the rest behind `View all` - and then told
the reader there were no more. A wallet with forty transactions showed five
and claimed that was all of them.

The comment beside it asserted "neither presentation truncates, so no more is
literally true". True of the page, never true of the panel. Now gated on
`limit == null`, which is the page.

The same label also carried the last of develop's phone type-scale shrink,
printing at 11 under rows that print at 14. It takes bodySm at every width
now. The padding around it stays gated on `compact`: that is density, not
type scale, and is treated the same way as the rest of the row.

Nothing pinned any of this before. Three tests do now, in the file that
already had a host mounting the panel with `page: false`.
Three whole schemes for the transaction row's amount column and its direction
signal, drawn at 1:1 on a 390x844 phone with the shipping Inter subset inlined
so every measurement is the width Flutter lays out.

A - neutral amount, fixed two-line slot. B - tinted in, neutral out, as a
declared rule. C - value-first hierarchy.

Jakub picked A, plus the independent LEAD control at L2 (white 70%).

C was rejected on a fact rather than on taste: `fiatValue()` multiplies by
`currentPrice`, so it applies today's price to a past transaction. C would
have made the row's least reliable number its headline, on a screen that
exists as a historical record.

The brief's premise was wrong and Jakub's memory was right. He recalled a
colour decision about the four lead strings; `git show 0cd2889` has them
moving out of a surfaceMenu chip at labelMd 11 in textSecondary and into the
subtitle lead at bodySm 14 w600 in textPrimary. The change did not separate
them from the ticker, it collapsed them into it.
260807-ubg  restore the transaction row type scale
260807-v6m  dashboard panel bottom inset - PLANNED, NOT EXECUTED
260807-wbu  the app-wide row rhythm rollout
260808-k2l  sketch 186 scheme A plus lead L2

260807-v6m is deliberately unexecuted. Its measurements were taken before the
rhythm rollout changed the rows underneath it, so its call-site values are
stale and it has to be re-measured before it runs. It also carries
SEPARATOR-RHYTHM-MEASURED.md, the survey the rollout was decided from.
@braianxde
braianxde marked this pull request as ready for review August 8, 2026 13:53
Repo rule, and 16 comment lines added by this branch broke it. Only lines
this branch added were touched; pre-existing prose elsewhere in these files
is left alone.
@henriqueaklein
henriqueaklein merged commit 820cb0a into develop Aug 10, 2026
8 checks passed
@henriqueaklein
henriqueaklein deleted the redesign/row-rhythm-260808 branch August 10, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants