Skip to content

Use the shadcn components we already have (charts, search fields, budget toggle) - #162

Merged
KenTaniguchi-R merged 3 commits into
mainfrom
feat/ui-library-adoption
Sep 7, 2026
Merged

Use the shadcn components we already have (charts, search fields, budget toggle)#162
KenTaniguchi-R merged 3 commits into
mainfrom
feat/ui-library-adoption

Conversation

@KenTaniguchi-R

Copy link
Copy Markdown
Owner

An audit of where the codebase hand-rolls UI that shadcn/ui already provides. The library turns out to be used well overall — 24 of 27 installed components have consumers, there is exactly one raw <input> and no raw <select> — so this PR takes only the three clearest gaps.

1. chart was never installed, and it was costing us a dark-mode bug

spending-chart.tsx passed Recharts' bare <Tooltip> in both donut and bar mode. That renders a hardcoded white box with dark text, which is unreadable against a dark background. net-worth-area-chart.tsx had already hand-rolled a CustomTooltip to route around the same problem, and CLAUDE.md has described the stack as "Recharts v3 (via shadcn Chart)" the whole time — the component itself was just missing.

All four charts now render inside ChartContainer with ChartTooltip / ChartLegendContent, and series labels come from ChartConfig rather than per-series name props, so legend and tooltip can't drift apart.

Two deliberate local edits to the generated ui/chart.tsx, both commented in the file:

  • The shadcn CLI emits import { cn } from "cn" and tries to add an npm package by that name. Repointed at @/lib/utils.
  • Added a valueFormatter prop. Every value this app charts is an integer cent count, and upstream renders values with toLocaleString()123,456 where $1,234.56 belongs. Recharts' own formatter can't be used for this: it replaces the entire tooltip row, taking the colour indicator and series name with it.

Category-keyed charts keep inline colours on purpose. Category names are user data (Groceries & Dining), and those can't be emitted as --color-<key> custom properties.

The net worth chart keeps a five-line wrapper for the one thing ChartTooltipContent doesn't do: dropping the null half of the split coverage series, which would otherwise print the same date twice.

2. input-group was installed with zero consumers

bill-search.tsx and transaction-filters.tsx both positioned their magnifier by hand — a relative wrapper, an absolute icon, matching pl-8 on the input. Now InputGroup / InputGroupAddon / InputGroupInput. Small a11y gain: the addon focuses the input when clicked, which the old pointer-events-none icon could not.

3. A segmented control built from raw buttons

budget-page-header.tsx built its Category/Flex switch from two <button>s in a bordered flex row, re-deriving selected-state styling. The same control is built from ToggleGroup in four other places (appearance-toggle, date-range-selector, account-list, holdings-table).

It takes the aria-pressed:bg-primary override that appearance-toggle documents — the shared bg-muted sits too close to the page ground to read as selected on a control whose only job is showing which option is active.

Testing

pnpm typecheck and pnpm lint pass.

Charts have no automated coverage in this repo (vitest is environment: "node" and matches *.test.ts only), so I verified them with a throwaway jsdom harness — all four mount without throwing, legend labels resolve from ChartConfig, and the tooltip renders $1,234.56 rather than 123,456. The harness is not included; adding a jsdom project to vitest so component tests are possible at all is a separate call.

Deliberately not in this PR

Two further findings from the audit, both of which shift pixels and want a mock first:

  • Five popovers build option lists from raw <button>s inside PopoverContent (transaction-filters ×4, report-filter-bar ×2, date-range-popover). DropdownMenu would give roving arrow-key focus, typeahead and role="menuitem" — today those lists are Tab-only. This is the largest remaining a11y win.
  • Three files hand-roll <table> (bill-list, portfolio-reconciliation, widgets/account-balances) while ui/table.tsx has six consumers. Needs care around cell padding and the table-fixed clipping fix from fix(dashboard): stop Account Balances and Investments from clipping their own numbers #159.

Not recommended: the ~20 hand-rolled rounded-lg border shells are visually different from this repo's Card (rounded-xl bg-card ring-1 ring-foreground/10, no border). Converting them is a redesign, not a refactor.

🤖 Generated with Claude Code

Two charts passed Recharts' bare `<Tooltip>`, which paints a hardcoded
white box with dark text — unreadable in dark mode. The net worth chart
had already hand-rolled a `CustomTooltip` to get around exactly that, and
CLAUDE.md has claimed "Recharts v3 (via shadcn Chart)" all along, so the
missing piece was the component itself.

All four charts now render inside `ChartContainer` with `ChartTooltip` /
`ChartLegendContent`, and series labels come from `ChartConfig` rather
than per-series `name` props, which keeps legend and tooltip in step.

Two local adaptations to the generated `ui/chart.tsx`:

- The shadcn CLI emits `import { cn } from "cn"` and tries to install a
  package by that name. Repointed at `@/lib/utils`.
- Added a `valueFormatter` prop. Every value this app charts is an integer
  cent count, and upstream renders values with `toLocaleString()` — 123456
  where $1,234.56 belongs. Recharts' own `formatter` replaces the whole
  tooltip row, dropping the colour indicator and the series name with it.

The net worth chart keeps a five-line wrapper for the one thing the shared
content does not do: dropping the null half of the split coverage series,
which would otherwise print the same date twice.
Both search inputs positioned their magnifier by hand — a `relative`
wrapper, an `absolute` icon and matching `pl-8` on the input — while
`ui/input-group.tsx` sat in the repo with no callers at all.

InputGroup also focuses the input when the icon is clicked, which the old
`pointer-events-none` icon could not do.
The Category/Flex control was two raw buttons in a bordered flex row,
re-deriving selected styling that ToggleGroup already carries — and the
same segmented control is built from ToggleGroup in four other places.

Base UI hands back an empty array when the active item is clicked again,
so the handler ignores anything that is not a budget type, keeping one
option always selected. The selected item takes the `aria-pressed:bg-primary`
override the appearance toggle documents: the shared `bg-muted` sits too
close to the page ground to read as selected on a control whose only job
is showing which option is active.
@KenTaniguchi-R
KenTaniguchi-R merged commit 8577f80 into main Sep 7, 2026
5 checks passed
@KenTaniguchi-R
KenTaniguchi-R deleted the feat/ui-library-adoption branch September 7, 2026 00:22
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.

1 participant