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 - - + + {categoryLabel(bill.categoryName)} - - + + {bill.averageAmount !== null && ( )} - - + + {bill.frequency && ( {FREQUENCY_LABELS[bill.frequency] ?? bill.frequency} )} - - + + - - + + ); } diff --git a/src/components/molecules/date-range-popover.tsx b/src/components/molecules/date-range-popover.tsx index b2562c3..bc033ff 100644 --- a/src/components/molecules/date-range-popover.tsx +++ b/src/components/molecules/date-range-popover.tsx @@ -6,6 +6,7 @@ import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; export interface DatePresetOption { @@ -58,8 +59,12 @@ export function DateRangePopover({ }: DateRangePopoverProps) { const [open, setOpen] = useState(false); - function handlePreset(id: string) { - onSelectPreset(id); + // Base UI hands back an array even for single select, and an empty one when + // the active item is clicked again. Re-picking the current preset should just + // close the popover, not clear the range. + function handlePreset(groupValue: string[]) { + const id = groupValue[0] ?? selectedId; + if (id) onSelectPreset(id); setOpen(false); } @@ -90,19 +95,33 @@ export function DateRangePopover({ -
+ {/* A roving-focus group rather than a stack of buttons, so the presets + answer to arrow keys instead of costing one Tab stop each. This + cannot be a DropdownMenu: the custom-range inputs below share the + popover, and Base UI's menu typeahead has no input guard, so typing + a date would move the menu selection instead. */} + {presets.map((preset) => ( - + + ))} -
+

Custom range

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. */} -
- - - - - - - - - - - - {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)} -
-
+ + + +
); } 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 */}