From 638968b5918e360479b4c388584f42241b11ab3e Mon Sep 17 00:00:00 2001 From: RyuseiTaniguchi Date: Mon, 7 Sep 2026 09:45:21 +0900 Subject: [PATCH 1/2] refactor(filters): give the two hand-rolled option lists real keyboard nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both built their options from raw ` + + ))} - +

Custom range

diff --git a/src/components/organisms/transaction-filters.tsx b/src/components/organisms/transaction-filters.tsx index 9bf11ce..2110454 100644 --- a/src/components/organisms/transaction-filters.tsx +++ b/src/components/organisms/transaction-filters.tsx @@ -26,6 +26,13 @@ import { import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Command, CommandEmpty, @@ -84,6 +91,10 @@ function triggerLabel(label: string, value: string | null, active: boolean): Rea ); } +/** A radio group needs a value for every row; `null` is not one, so the + * "no type filter" row carries this sentinel and maps back to null. */ +const ALL_TYPES = "__all__"; + export function TransactionFilters({ accounts, categories, resultCount }: TransactionFiltersProps) { const { updateFilter, updateFilters, clearFilters, hasFilters, searchParams } = useSearchParamFilters(); @@ -474,39 +485,30 @@ export function TransactionFilters({ accounts, categories, resultCount }: Transa - {/* Type */} - - + } > {triggerLabel("Type", typeValue, !!typeValue)} - - -
- + + + selectType(value === ALL_TYPES ? null : String(value))} + > + All types {Object.entries(TYPE_LABELS).map(([value, label]) => ( - + ))} -
-
-
+ + + {/* Amount */} From 83d74b39dffda8a9ee9c9a38be1f75d7b6741fac Mon Sep 17 00:00:00 2001 From: RyuseiTaniguchi Date: Mon, 7 Sep 2026 09:45:33 +0900 Subject: [PATCH 2/2] refactor(tables): build Bills and reconciliation from the Table component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both hand-rolled `` 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. --- src/components/molecules/bill-row.tsx | 27 +++---- src/components/organisms/bill-list.tsx | 68 ++++++++-------- .../organisms/portfolio-reconciliation.tsx | 77 ++++++++++--------- 3 files changed, 92 insertions(+), 80 deletions(-) diff --git a/src/components/molecules/bill-row.tsx b/src/components/molecules/bill-row.tsx index b48b1a6..a5504c9 100644 --- a/src/components/molecules/bill-row.tsx +++ b/src/components/molecules/bill-row.tsx @@ -1,6 +1,7 @@ import { AmountDisplay } from "@/components/atoms/amount-display"; import { BillStatusIndicator } from "@/components/atoms/bill-status-indicator"; import { Badge } from "@/components/ui/badge"; +import { TableCell, TableRow } from "@/components/ui/table"; import { categoryLabel } from "@/lib/labels"; import type { BillRow as BillRowType } from "@/queries/recurring"; @@ -24,11 +25,11 @@ export function BillRow({ bill, onSelect }: BillRowProps) { // unreachable by keyboard, and wrapping the row in a - - - - - - + + ); } diff --git a/src/components/organisms/bill-list.tsx b/src/components/organisms/bill-list.tsx index d94d79a..0ffd449 100644 --- a/src/components/organisms/bill-list.tsx +++ b/src/components/organisms/bill-list.tsx @@ -2,6 +2,13 @@ import { useState } from "react"; import { BillRow } from "@/components/molecules/bill-row"; +import { + Table, + TableBody, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { BillDetailSheet } from "@/components/organisms/bill-detail-sheet"; import type { BillRow as BillRowType } from "@/queries/recurring"; import type { CategoryGroup } from "@/queries/categories"; @@ -16,38 +23,35 @@ export function BillList({ bills, categoryGroups }: BillListProps) { return (
- {/* The columns need roughly 560px to stay legible. Previously they were - fixed widths with no breakpoint and no scroll container, so the whole - page scrolled sideways on a phone (body scrollWidth 592 at 390px). - Scrolling inside this container is the pattern Investments uses. */} -
-
+ - + + {categoryLabel(bill.categoryName)} - + + {bill.averageAmount !== null && ( )} - + + {bill.frequency && ( {FREQUENCY_LABELS[bill.frequency] ?? bill.frequency} )} - + + -
- - - - - - - - - - - {bills.map((bill) => ( - setSelected(bill)} /> - ))} - -
- Name - - Category - - Amount - - Frequency - - Status -
-
+ {/* The columns need roughly 560px to stay legible; below that they scroll + inside Table's own container rather than pushing the page sideways + (body scrollWidth 592 at 390px, before this had a scroll parent). */} + + + + + Name + + + Category + + + Amount + + + Frequency + + + Status + + + + + {bills.map((bill) => ( + setSelected(bill)} /> + ))} + +
{/* Keyed so opening another bill remounts the sheet and its form re-seeds, rather than syncing props into state from an effect. */} diff --git a/src/components/organisms/portfolio-reconciliation.tsx b/src/components/organisms/portfolio-reconciliation.tsx index b25705f..ffc0dc7 100644 --- a/src/components/organisms/portfolio-reconciliation.tsx +++ b/src/components/organisms/portfolio-reconciliation.tsx @@ -1,4 +1,13 @@ import { centsToDisplay } from "@/lib/money"; +import { + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import type { AccountReconciliationRow } from "@/queries/investments"; interface PortfolioReconciliationProps { @@ -33,22 +42,21 @@ export function PortfolioReconciliation({ rows }: PortfolioReconciliationProps) What each account reports, against the holdings Ledgr can itemize.

- {/* Wide content scrolls inside its own container so the page never + {/* Wide content scrolls inside Table's own container so the page never scrolls sideways on a phone. */} -
- - - - - - - - - - +
AccountBalanceHoldingsCash / unallocated
+ + + Account + Balance + Holdings + Cash / unallocated + + + {rows.map((row) => ( - - - - - - + + ))} - - - - - - - - - -
+ + {row.accountName} {/* A balance with no holdings at all is a connector that itemized nothing, not a cash position -- worth saying so @@ -58,37 +66,36 @@ export function PortfolioReconciliation({ rows }: PortfolioReconciliationProps) not itemized )} - {centsToDisplay(row.balance)} + + {centsToDisplay(row.balance)} + {centsToDisplay(row.holdingsValue)} - + + {row.cashValue > 0 ? ( centsToDisplay(row.cashValue) ) : ( )} -
Total + + + + Total + {centsToDisplay(totals.balance)} - + + {centsToDisplay(totals.holdingsValue)} - + + {centsToDisplay(totals.cashValue)} -
-
+ + + + ); }