Edit and add are the same modal: notes from a Portfolio polish pass
A polish-only pass on the Portfolio panel: a wheel retheme, a nested-scroll trap fixed, four stacked Add forms collapsed into one modal that also handles edit, and a default-tab fix on Nova Fund. The strategic depth got deferred on purpose.
A.7.4 was earmarked as a Portfolio overhaul with strategic features: tax-lot tracking, vs-SPY benchmark line, dividend schedule with DRIP, CSV import for the major brokerages, sector and factor exposure breakdown. Each of those is a multi-day PR. Greenlit a polish-and-restructure pass first — fix the structural issues, then ship the depth as separate work. PR #65 is what landed: six fixes, no new strategic features. Three of them banked patterns worth keeping.
The wheel got a retheme
The RadialAllocation doughnut on each portfolio bucket was using random hash-of-id hues with 0.18–0.32 alpha. The intent had been to give every bucket a visually distinct slice without manual color choice; the result was that nothing read as deliberate. The alpha trick washed everything out against the ink-deep panel background.
Replaced with a curated 6-color rotation against the LTM palette: signal mint → pulse violet → warn amber → signal-dim → pulse-dim → bone-dim. Largest bucket always gets primary mint; the rest fall into rotation order. Dropped the alpha — full saturation against an ink-deep stroke separator reads cleaner. Negative-net buckets get loss color through a tone prop that was already on the component but unused (strokeFor(tone) { void tone } was the actual contents of the function — banked that as a small reminder to wire props you set up).
Hover lifts the active slice by +4 and shrinks the others by −2, fades non-active to 32% opacity, and adds a Gaussian glow filter; the percentage renders in the center. "+ N more" instead of silent legend truncation past 6, so the user knows there's stuff hidden. Three of the five stacked decorative effects also got removed; only nova-panel survived. Less is more on a chart that has to render 30+ holdings without becoming noise.
Edit and add are the same modal
This is the cleanest pattern the PR shipped. The original Portfolio panel had four stacked Add forms below the asset list — Stock, Stocks total, Cash, Real estate — and adding a position when you already had thirty holdings meant scrolling past all of them. Vehicle was supported in the data model but had no form. Edit didn't exist at all; you could only add or delete.
AddAssetModal got renamed to AssetModal and grew an optional existing prop. Title swaps Add → Edit. Submit button swaps Add → Save. Per-kind field state initializes from the existing values when present, blank when not. Kind tabs lock on edit (changing a Cash asset to a Stock would orphan the price-fetch state, so a tooltip explains and the tabs are disabled). The single handleUpsertAsset handler takes an optional existingId so submit preserves the row id on edit and creates a new one on add.
The same shape worked for RenameBucketModal — Rename button in the bucket header, modal opens with the current name pre-filled. Six-card kind picker on AssetModal includes Vehicle and Other now.
Banked: when an upsert API exists in the data layer, the create + edit UIs can be the same surface with one optional prop discriminating mode. The cost of a separate EditAssetModal would have been duplicate field state, duplicate validation, duplicate per-kind branching — all of which would drift apart over time.
Nested scroll containers don't resolve
The Portfolio panel had two overflow-y-auto containers nested inside flex-1 / min-h-0 parents that never resolved to a hard height. Both wanted to scroll; neither could, because neither knew how tall it was supposed to be. The user got either no scroll bar or a scroll bar that scrolled the wrong slab.
Fix: only one scroll container in any vertical stack. The single scroll container is now on <main>. The sidebar has its own scroll container too — separate column — with the Add-bucket form pinned to the bottom via border-t shrink-0.
Banked: nested flex-1 / min-h-0 + nested overflow-y-auto is a scroll trap. When two scroll containers nest inside flex-1 parents that haven't resolved a hard height, the inner one has nothing to scroll against. Push scroll to the outermost or the innermost, never both.
Header tightened, sparklines dropped
A few smaller cuts. The header had a debug line ("Stored in your vault · updates Portfolio/Portfolio.md"), a standalone news-source line, and three action buttons (Refresh prices, Reload, Add bucket). The debug line is gone; the news-source is now a small inline label next to the actions; the Reload button got dropped (redundant with Refresh prices). Net worth is the headline (26px mono); Total assets is secondary. Bucket-delete prompts for confirmation now.
The bucket-row sparklines got dropped entirely. They were mini trend lines next to each bucket's dollar amount. The math worked, but most buckets only had a handful of history points — the lines mostly looked like a slope artifact rather than a real trend. Removed the Sparkline import, the historyGet effect that fetched the data, and the history state. Dropping a feature that doesn't have enough data to be useful is a real fix; cluttered noise costs more than the empty space it replaces.
And one fix on the Nova Fund tab
Separate PR (#66), same theme. Nova Fund had an "auto-switch on initial load" effect that landed you on whatever tab was "most actionable" — pending trades > draft memo > holdings > pending. The intent was helpful; the effect was that you'd navigate to Nova Fund and end up somewhere unexpected, mid-page through a flow you didn't initiate.
Initial tab is Holdings now. Period. Pending and Memo carry their own work via badge counts on the tab bar; that's enough signal. The auto-switch effect and its tabAutoSet state are gone.
Banked: auto-switching tabs on initial load is too clever. Prefer a stable default plus signal-via-badge over auto-routing the user.
What got deferred
The strategic depth that A.7.4 originally scoped is still owed: tax-lot tracking + realized P&L, vs-SPY benchmark line on a perf chart, dividend schedule + DRIP toggle, CSV import (Fidelity / Schwab / Robinhood transaction CSVs), sector + factor exposure breakdown. Each is its own PR. Tax lots is probably the highest leverage if the goal is real P&L tracking. None of them block this polish work; they queue separately.
The polish pass made the Portfolio panel feel deliberate. The depth that turns it into a real investing UX is next.
Free Your Mind · Free weekly newsletter
Liked this? Get the next one in your inbox.
One full ticker through the framework + the lesson behind it, every week. Unsubscribe in one click.
Want the full picture?
Smart-money flow, real conversations, the whole framework.