feat(dashboard): make balances, net worth and transfers legible at a glance - #161
Merged
Conversation
#159 stopped investment activity counting toward spending, but the list still rendered those rows at full weight — the arithmetic changed and the page did not. A day holding a $1,000 brokerage buy read as $1,000 gone while Reports, Budgets and the dashboard all disagreed. Transfer rows now take muted text rather than `opacity-60`: opacity is already spent on `pending`, and stacking the two leaves a pending transfer barely readable. Muting the amount also drops its `text-positive` green, which money moved between your own accounts never earned. Date headers net spending only, and name the two counts apart once a transfer is present ("2 spending · 2 transfers") — the count and the net beside it otherwise described different sets of rows with nothing saying so.
$77,506.89 could be a paid-off house or a leveraged brokerage account. The hero stated one number and nothing about the position producing it. getDashboardSummary already summed both sides to produce netWorth and then threw them away; it now returns them. The pair sits on the figure row rather than under it, so the hero keeps its height on a page that already has a lot below it, and it uses BalanceDisplay — the component the Accounts page totals its groups with — so a debt reads as a debt here without a second colour convention.
The widget flattened every account into one undifferentiated table, so an $18,240 savings balance and a -$8,400 loan read as the same kind of number with nothing marking which side of the sheet each was on. Reuses groupAccountsByType — the helper already behind the Accounts page — rather than adding a second grouping, so the widget and the page it links to name, order and total their sections identically. The raw <table> stays deliberately: shadcn's <Table> wraps itself in an `overflow-x-auto` container, which is the horizontal scrollbar #159 removed from this widget. Group headers are ordinary rows of the same table so the pinned balance column still can't be pushed past the card's edge.
KenTaniguchi-R
added a commit
that referenced
this pull request
Sep 7, 2026
…for Bills and reconciliation (#165) * refactor(filters): give the two hand-rolled option lists real keyboard nav Both built their options from raw `<button>`s inside a `PopoverContent`, so each option cost a Tab stop and the arrow keys did nothing. The Type filter is a plain single-select list, so it becomes a `DropdownMenu` with a `DropdownMenuRadioGroup` — roving focus, typeahead, `role="menuitem"` and Esc, all from the component. A radio group needs a value for every row and `null` is not one, so the "All types" row carries an `ALL_TYPES` sentinel that maps back to null at the boundary. The date presets could not follow it there. They share their popover with the custom-range From/To inputs, and Base UI's menu typeahead (`floating-ui-react/hooks/useTypeahead`) has no guard for typeable targets — inside a menu, typing a date would move the menu selection instead of filling the field. They become a vertical `ToggleGroup` instead, which is also a roving-focus composite: one Tab stop, arrow keys, Home/End, and the inputs keep working. No typeahead there, which is the honest trade. Nothing moves visually. Both lists keep `h-8`, `px-2` and the same hover fill. * refactor(tables): build Bills and reconciliation from the Table component Both hand-rolled `<table>` and re-added, by hand, the `overflow-x-auto` container `ui/table.tsx` already ships — the same scroll wrapper written twice, in two slightly different ways. Rows sit a little tighter: `Table` pads cells `p-2` where these files set `px-3` and `px-4`. Reconciliation keeps its `px-4` because its four columns have room for it; Bills takes the component's spacing. Both keep their `min-w-[560px]`, so the mobile fix from the responsive pass still holds — the scrolling just happens in Table's container now. `widgets/account-balances.tsx` is deliberately left alone. Its `colgroup`, `table-fixed` and per-group `tbody` came from #161 and are doing work Table does not help with; converting it would put the #159 clipping fix at risk for no gain.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three gaps found by comparing Ledgr's dashboard and transactions list against Copilot Money, Quicken, Rocket Money and Origin. Each was mocked in Ledgr's own design tokens and reviewed before implementation.
1. Account balances group by type, with subtotals
The widget flattened every account into one undifferentiated table, so an $18,240 savings balance and a −$8,400 loan read as the same kind of number with nothing marking which side of the sheet each was on.
Reuses
groupAccountsByType— the helper already behind the Accounts page — rather than adding a second grouping, so the widget and the page it links to name, order and total their sections identically. Subtotals useBalanceDisplay, asaccount-list.tsxdoes.The raw
<table>stays deliberately. shadcn's<Table>wraps itself in anoverflow-x-autocontainer, which is the horizontal scrollbar #159 removed from this widget; group headers are ordinary rows of the same table so the pinned balance column still can't be pushed past the card's edge.2. Net worth shows the assets and debts behind it
$77,506.89 could be a paid-off house or a leveraged brokerage account.
getDashboardSummaryalready summed both sides to producenetWorthand then threw them away — it now returns them.The pair sits on the figure row rather than under it, so the hero keeps its height on a page that already has a lot below it. It uses
BalanceDisplay, so a debt reads as a debt without introducing a second colour convention.3. Transfers recede in the transaction list
#159 stopped investment activity counting toward spending, but the list still rendered those rows at full weight — the arithmetic changed and the page did not. A day holding a $1,000 brokerage buy read as $1,000 gone while Reports, Budgets and the dashboard all disagreed.
Transfer rows take muted text rather than
opacity-60: opacity is already spent onpending, and stacking the two leaves a pending transfer barely readable. Muting the amount also drops itstext-positivegreen, which money moved between your own accounts never earned.Date headers now net spending only and name the two counts apart once a transfer is present ("2 spending · 2 transfers") — the count and the net beside it otherwise described different sets of rows with nothing saying so.
Tests
7 new tests in
src/lib/transaction-day-summary.test.tscover transfer exclusion from the net, a transfers-only day, a nullisTransfer, and each label form. Full unit suite passes (718 tests); typecheck and lint clean.🤖 Generated with Claude Code