@retiregolden/planner-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +181 -0
  3. package/package.json +77 -0
  4. package/src/App.tsx +246 -0
  5. package/src/RouteErrorBoundary.tsx +45 -0
  6. package/src/assets/hero.png +0 -0
  7. package/src/assets/react.svg +1 -0
  8. package/src/assets/vite.svg +1 -0
  9. package/src/data/fedInvestClient.ts +113 -0
  10. package/src/data/localStore.ts +42 -0
  11. package/src/data/planOrigin.ts +24 -0
  12. package/src/data/planStore.ts +165 -0
  13. package/src/data/v2Backup.ts +101 -0
  14. package/src/import/ImportPage.tsx +347 -0
  15. package/src/import/ReviewChecklistView.tsx +38 -0
  16. package/src/import/brokerCsv.ts +395 -0
  17. package/src/import/csv.ts +133 -0
  18. package/src/import/genericCsv.ts +224 -0
  19. package/src/import/projectionLab.ts +350 -0
  20. package/src/import/reviewChecklist.ts +33 -0
  21. package/src/import/tenForty.ts +275 -0
  22. package/src/index.css +630 -0
  23. package/src/index.ts +16 -0
  24. package/src/learn/ArticleBody.tsx +78 -0
  25. package/src/learn/ArticlePage.tsx +57 -0
  26. package/src/learn/GlossaryPage.tsx +33 -0
  27. package/src/learn/LearnAboutScreen.tsx +41 -0
  28. package/src/learn/LearnCards.tsx +41 -0
  29. package/src/learn/LearnLink.tsx +91 -0
  30. package/src/learn/LearningCenterPage.tsx +114 -0
  31. package/src/learn/SourcesPage.tsx +98 -0
  32. package/src/learn/components/ArticleFigure.tsx +34 -0
  33. package/src/learn/components/ArticleShell.tsx +86 -0
  34. package/src/learn/components/ComparisonTable.tsx +42 -0
  35. package/src/learn/components/FormulaBlock.tsx +34 -0
  36. package/src/learn/components/PurchasingPowerChart.tsx +41 -0
  37. package/src/learn/components/RelatedArticles.tsx +27 -0
  38. package/src/learn/components/ScenarioCard.tsx +24 -0
  39. package/src/learn/components/SourceList.tsx +23 -0
  40. package/src/learn/components/charts.tsx +21 -0
  41. package/src/learn/content/about-retiregolden.ts +100 -0
  42. package/src/learn/content/aca-premium-tax-credits-and-magi.ts +103 -0
  43. package/src/learn/content/account-types-overview.ts +106 -0
  44. package/src/learn/content/after-tax-estate.ts +111 -0
  45. package/src/learn/content/agi-magi-and-taxable-income.ts +112 -0
  46. package/src/learn/content/appealing-irmaa-ssa-44.ts +95 -0
  47. package/src/learn/content/assumption-general-inflation.ts +82 -0
  48. package/src/learn/content/assumption-healthcare-inflation.ts +85 -0
  49. package/src/learn/content/assumption-heir-tax-rate.ts +79 -0
  50. package/src/learn/content/assumption-investment-returns.ts +90 -0
  51. package/src/learn/content/assumption-longevity-planning-age.ts +78 -0
  52. package/src/learn/content/assumption-recent-magi.ts +83 -0
  53. package/src/learn/content/assumption-social-security-cola.ts +89 -0
  54. package/src/learn/content/assumption-social-security-trust-fund.ts +83 -0
  55. package/src/learn/content/assumption-state-tax-override.ts +79 -0
  56. package/src/learn/content/beneficiaries-and-account-titling.ts +99 -0
  57. package/src/learn/content/break-even-useful-lens.ts +94 -0
  58. package/src/learn/content/building-a-retirement-spending-budget.ts +100 -0
  59. package/src/learn/content/cola-and-inflation-protection.ts +102 -0
  60. package/src/learn/content/divorced-spousal-and-survivor-records.ts +104 -0
  61. package/src/learn/content/dynamic-spending-guardrails.ts +90 -0
  62. package/src/learn/content/earnings-test-before-fra.ts +100 -0
  63. package/src/learn/content/employer-match-and-contribution-order.ts +104 -0
  64. package/src/learn/content/examplePlanArticles.ts +525 -0
  65. package/src/learn/content/fees-expense-ratios-and-compounding-drag.ts +98 -0
  66. package/src/learn/content/fi-number-and-four-percent-rule.ts +64 -0
  67. package/src/learn/content/filling-a-tax-bracket-with-roth-conversions.ts +98 -0
  68. package/src/learn/content/funded-ratio.ts +70 -0
  69. package/src/learn/content/healthcare-after-65.ts +103 -0
  70. package/src/learn/content/healthcare-before-65.ts +104 -0
  71. package/src/learn/content/historical-vs-random-return-models.ts +101 -0
  72. package/src/learn/content/how-assumptions-change-the-answer.ts +105 -0
  73. package/src/learn/content/how-much-can-i-spend.ts +105 -0
  74. package/src/learn/content/how-social-security-is-taxed.ts +95 -0
  75. package/src/learn/content/how-the-optimizer-thinks.ts +102 -0
  76. package/src/learn/content/how-the-optimizer-values-after-tax-estate.ts +97 -0
  77. package/src/learn/content/how-to-model-accumulation.ts +67 -0
  78. package/src/learn/content/how-to-read-a-retirement-projection.ts +115 -0
  79. package/src/learn/content/hsas-and-qualified-medical-expenses.ts +108 -0
  80. package/src/learn/content/hsas-as-retirement-accounts.ts +101 -0
  81. package/src/learn/content/inflation-risk.ts +98 -0
  82. package/src/learn/content/inherited-ira-10-year-rule.ts +105 -0
  83. package/src/learn/content/insurance-in-your-retirement-plan.ts +103 -0
  84. package/src/learn/content/irmaa-two-year-lookback.ts +99 -0
  85. package/src/learn/content/long-term-care-costs-and-insurance.ts +103 -0
  86. package/src/learn/content/long-term-care-insurance-as-risk-transfer.ts +98 -0
  87. package/src/learn/content/longevity-risk.ts +99 -0
  88. package/src/learn/content/marginal-vs-effective-tax-rate.ts +98 -0
  89. package/src/learn/content/medicare-part-b-vs-part-d-irmaa.ts +102 -0
  90. package/src/learn/content/mortality-weighted-social-security.ts +113 -0
  91. package/src/learn/content/moving-to-retiregolden.ts +86 -0
  92. package/src/learn/content/niit-high-income-investment-tax.ts +98 -0
  93. package/src/learn/content/ordinary-income-vs-capital-gains.ts +103 -0
  94. package/src/learn/content/paying-conversion-taxes-taxable-vs-ira.ts +102 -0
  95. package/src/learn/content/pensions-and-annuities.ts +101 -0
  96. package/src/learn/content/permanent-life-insurance-in-a-plan.ts +106 -0
  97. package/src/learn/content/pia-aime-and-bend-points.ts +103 -0
  98. package/src/learn/content/planner-overview.ts +106 -0
  99. package/src/learn/content/planning-for-couples-and-survivor-years.ts +108 -0
  100. package/src/learn/content/privacy-what-stays-in-your-browser.ts +99 -0
  101. package/src/learn/content/qcds-qualified-charitable-distributions.ts +101 -0
  102. package/src/learn/content/reading-the-results-page.ts +96 -0
  103. package/src/learn/content/reading-the-social-security-analysis-page.ts +106 -0
  104. package/src/learn/content/real-estate-home-equity-and-debt.ts +100 -0
  105. package/src/learn/content/reports-csv-exports-and-sharing.ts +101 -0
  106. package/src/learn/content/risk-based-guardrails.ts +100 -0
  107. package/src/learn/content/rmds-required-minimum-distributions.ts +100 -0
  108. package/src/learn/content/roth-conversion-basics.ts +104 -0
  109. package/src/learn/content/rsus-and-espp.ts +101 -0
  110. package/src/learn/content/rule-of-55-and-72t.ts +107 -0
  111. package/src/learn/content/savings-rate-biggest-lever.ts +66 -0
  112. package/src/learn/content/seed-your-plan-from-your-tax-return.ts +93 -0
  113. package/src/learn/content/sensitivity-testing-what-changes-the-answer.ts +104 -0
  114. package/src/learn/content/sequence-of-returns-risk.ts +98 -0
  115. package/src/learn/content/social-security-bridge.ts +67 -0
  116. package/src/learn/content/social-security-claiming-age-basics.ts +113 -0
  117. package/src/learn/content/social-security-taxes-vs-benefits.ts +76 -0
  118. package/src/learn/content/spending-profiles-and-the-retirement-smile.ts +92 -0
  119. package/src/learn/content/spousal-and-survivor-benefits.ts +120 -0
  120. package/src/learn/content/ssdi-and-retirement-planning.ts +72 -0
  121. package/src/learn/content/standard-deduction-senior-deduction-and-itemizing.ts +97 -0
  122. package/src/learn/content/state-income-taxes-in-retirement.ts +97 -0
  123. package/src/learn/content/step-up-in-basis.ts +102 -0
  124. package/src/learn/content/survivor-planning-for-couples.ts +110 -0
  125. package/src/learn/content/survivor-spending-in-couple-plans.ts +98 -0
  126. package/src/learn/content/tax-cliffs-and-bracket-edges.ts +105 -0
  127. package/src/learn/content/tax-loss-and-gain-harvesting.ts +99 -0
  128. package/src/learn/content/taxable-brokerage-basis-and-capital-gains.ts +99 -0
  129. package/src/learn/content/three-big-questions-spending-time-risk.ts +103 -0
  130. package/src/learn/content/tips-ladders.ts +92 -0
  131. package/src/learn/content/todays-dollars-vs-future-dollars.ts +107 -0
  132. package/src/learn/content/traditional-vs-roth-contributions.ts +113 -0
  133. package/src/learn/content/troubleshooting-surprising-results.ts +105 -0
  134. package/src/learn/content/trust-fund-haircut-scenarios.ts +101 -0
  135. package/src/learn/content/understanding-monte-carlo-success-rate.ts +118 -0
  136. package/src/learn/content/understanding-your-plan-assumptions.ts +134 -0
  137. package/src/learn/content/using-assumptions-and-provenance.ts +98 -0
  138. package/src/learn/content/using-scenarios-to-compare-choices.ts +99 -0
  139. package/src/learn/content/what-changes-when-you-move-states.ts +141 -0
  140. package/src/learn/content/what-is-fire.ts +65 -0
  141. package/src/learn/content/what-monte-carlo-proves.ts +98 -0
  142. package/src/learn/content/what-retiregolden-models.ts +103 -0
  143. package/src/learn/content/what-retirement-healthcare-really-costs.ts +117 -0
  144. package/src/learn/content/why-95-percent-is-not-a-guarantee.ts +98 -0
  145. package/src/learn/content/why-roth-conversions-raise-other-costs.ts +106 -0
  146. package/src/learn/content/why-small-tax-cliffs-can-matter.ts +109 -0
  147. package/src/learn/content/widows-penalty-and-survivor-brackets.ts +106 -0
  148. package/src/learn/content/withdrawal-order-basics.ts +105 -0
  149. package/src/learn/glossary.ts +191 -0
  150. package/src/learn/inlineMarkdown.tsx +54 -0
  151. package/src/learn/learn.css +537 -0
  152. package/src/learn/learningRegistry.ts +502 -0
  153. package/src/longevity/LongevityResults.tsx +85 -0
  154. package/src/longevity/LongevityWizard.tsx +305 -0
  155. package/src/longevity/constants.ts +15 -0
  156. package/src/longevity/factors.ts +125 -0
  157. package/src/longevity/model.ts +31 -0
  158. package/src/longevity/persistedGuard.ts +129 -0
  159. package/src/longevity/storage.ts +40 -0
  160. package/src/mc/messages.ts +118 -0
  161. package/src/mc/monteCarlo.worker.ts +44 -0
  162. package/src/mc/pool.ts +267 -0
  163. package/src/mc/runRequest.ts +125 -0
  164. package/src/optimize/messages.ts +84 -0
  165. package/src/optimize/optimize.worker.ts +29 -0
  166. package/src/optimize/runOptimize.ts +92 -0
  167. package/src/optimize/runSpendingSolve.ts +47 -0
  168. package/src/optimize/runner.ts +21 -0
  169. package/src/optimize/spendingMessages.ts +44 -0
  170. package/src/optimize/spendingRunner.ts +21 -0
  171. package/src/optimize/spendingSolve.worker.ts +18 -0
  172. package/src/planner/AssumptionsCardPage.tsx +136 -0
  173. package/src/planner/BucketLensCard.tsx +114 -0
  174. package/src/planner/ComparePlansPage.tsx +219 -0
  175. package/src/planner/DisclaimerPage.tsx +88 -0
  176. package/src/planner/HowTestedPage.tsx +159 -0
  177. package/src/planner/LiveStatus.tsx +15 -0
  178. package/src/planner/LongevityModal.tsx +55 -0
  179. package/src/planner/Modal.tsx +97 -0
  180. package/src/planner/MonteCarloPage.tsx +907 -0
  181. package/src/planner/OptimizePage.tsx +611 -0
  182. package/src/planner/PlanContext.tsx +198 -0
  183. package/src/planner/PlanPickerPage.tsx +124 -0
  184. package/src/planner/PlanWorkspace.tsx +290 -0
  185. package/src/planner/ProvenancePanel.tsx +45 -0
  186. package/src/planner/RelocationComparePage.tsx +485 -0
  187. package/src/planner/ReportPage.tsx +375 -0
  188. package/src/planner/ResultsPage.tsx +817 -0
  189. package/src/planner/ScenariosPage.tsx +285 -0
  190. package/src/planner/SocialSecuritySection.tsx +556 -0
  191. package/src/planner/SpendingSolverPage.tsx +512 -0
  192. package/src/planner/SsAnalysisPage.tsx +1134 -0
  193. package/src/planner/SurvivalPercentileModal.tsx +161 -0
  194. package/src/planner/SurvivorTransitionPage.tsx +286 -0
  195. package/src/planner/assumptionsExport.ts +371 -0
  196. package/src/planner/bucketLens.ts +89 -0
  197. package/src/planner/chartFrame.ts +8 -0
  198. package/src/planner/chartStyle.ts +11 -0
  199. package/src/planner/dialogViews.tsx +184 -0
  200. package/src/planner/dialogs.tsx +133 -0
  201. package/src/planner/examples/ExampleLibrary.tsx +189 -0
  202. package/src/planner/examples/ExamplePreviewBanner.tsx +55 -0
  203. package/src/planner/examples/ExamplesPage.tsx +25 -0
  204. package/src/planner/examples/OpenExampleButton.tsx +61 -0
  205. package/src/planner/examples/buildAggressiveSaver.ts +102 -0
  206. package/src/planner/examples/buildAnnuityEstate.ts +137 -0
  207. package/src/planner/examples/buildBaristaFire.ts +115 -0
  208. package/src/planner/examples/buildBracketFillRoth.ts +65 -0
  209. package/src/planner/examples/buildBridgeEarlyRetirement.ts +94 -0
  210. package/src/planner/examples/buildBrokerageNoHsa.ts +109 -0
  211. package/src/planner/examples/buildCoastFire.ts +88 -0
  212. package/src/planner/examples/buildContext.ts +20 -0
  213. package/src/planner/examples/buildEarlyCareerMatch.ts +93 -0
  214. package/src/planner/examples/buildEarlyRetireeAca.ts +61 -0
  215. package/src/planner/examples/buildExampleCouple.ts +103 -0
  216. package/src/planner/examples/buildFixedTargetSpending.ts +74 -0
  217. package/src/planner/examples/buildGlidepathAllocation.ts +131 -0
  218. package/src/planner/examples/buildGuardrailsFlex.ts +120 -0
  219. package/src/planner/examples/buildHsaPropertyDepth.ts +109 -0
  220. package/src/planner/examples/buildHsaStealthRetirement.ts +97 -0
  221. package/src/planner/examples/buildLeanFatFire.ts +109 -0
  222. package/src/planner/examples/buildLtcShock.ts +62 -0
  223. package/src/planner/examples/buildMovingStateTax.ts +53 -0
  224. package/src/planner/examples/buildNoAnnuityBrokerage.ts +92 -0
  225. package/src/planner/examples/buildRmdIrmaa.ts +55 -0
  226. package/src/planner/examples/buildSalaryGrowthEscalation.ts +96 -0
  227. package/src/planner/examples/buildStaticAllocationControl.ts +96 -0
  228. package/src/planner/examples/buildSurvivorYears.ts +62 -0
  229. package/src/planner/examples/buildUnderSavedSingle.ts +51 -0
  230. package/src/planner/examples/exampleCopy.ts +23 -0
  231. package/src/planner/examples/loadExample.ts +90 -0
  232. package/src/planner/examples/registry.ts +313 -0
  233. package/src/planner/explainPanels.tsx +233 -0
  234. package/src/planner/fields.tsx +381 -0
  235. package/src/planner/format.ts +33 -0
  236. package/src/planner/home/DataAndPrivacyCard.tsx +56 -0
  237. package/src/planner/home/GettingStartedPaths.tsx +46 -0
  238. package/src/planner/home/GettingStartedReopener.tsx +32 -0
  239. package/src/planner/home/StartHereLinks.tsx +22 -0
  240. package/src/planner/home/WelcomeHero.tsx +39 -0
  241. package/src/planner/home/YourPlans.tsx +72 -0
  242. package/src/planner/home/importErrorMessage.ts +22 -0
  243. package/src/planner/home/startHereSlugs.ts +7 -0
  244. package/src/planner/home/useHomeData.ts +190 -0
  245. package/src/planner/home/useHomeMode.ts +47 -0
  246. package/src/planner/householdActions.ts +22 -0
  247. package/src/planner/insights/InsightCardView.tsx +340 -0
  248. package/src/planner/insights/InsightsPage.tsx +204 -0
  249. package/src/planner/insights/categoryLabels.ts +11 -0
  250. package/src/planner/learnLinks.ts +85 -0
  251. package/src/planner/marketModelPicker.ts +172 -0
  252. package/src/planner/optimizePageChart.ts +40 -0
  253. package/src/planner/optimizePageClaim.ts +64 -0
  254. package/src/planner/planCompleteness.ts +27 -0
  255. package/src/planner/planContextCore.ts +26 -0
  256. package/src/planner/planner.css +2304 -0
  257. package/src/planner/provenanceLinks.ts +25 -0
  258. package/src/planner/sections/AccountFields.tsx +872 -0
  259. package/src/planner/sections/AccountsSection.tsx +89 -0
  260. package/src/planner/sections/AllocationPanel.tsx +261 -0
  261. package/src/planner/sections/AssumptionsSection.tsx +256 -0
  262. package/src/planner/sections/HouseholdSection.tsx +243 -0
  263. package/src/planner/sections/IncomeFloorSection.tsx +418 -0
  264. package/src/planner/sections/IncomeSection.tsx +170 -0
  265. package/src/planner/sections/InsuranceSection.tsx +362 -0
  266. package/src/planner/sections/SpendingSection.tsx +904 -0
  267. package/src/planner/sections/StrategySection.tsx +349 -0
  268. package/src/planner/sections/UpdateBalancesPanel.tsx +182 -0
  269. package/src/planner/sections/sectionHelpers.ts +48 -0
  270. package/src/planner/sections/shared.tsx +15 -0
  271. package/src/planner/sections.tsx +15 -0
  272. package/src/planner/ssAnalysis.ts +325 -0
  273. package/src/planner/successBand.ts +20 -0
  274. package/src/planner/survivorAnalysis.ts +277 -0
  275. package/src/planner/usStates.ts +19 -0
  276. package/src/planner/useMcSuccessRate.ts +77 -0
  277. package/src/planner/useProjection.ts +63 -0
  278. package/src/relocation/messages.ts +21 -0
  279. package/src/relocation/relocation.worker.ts +18 -0
  280. package/src/relocation/runRelocation.ts +17 -0
  281. package/src/relocation/runner.ts +22 -0
  282. package/src/report/brandingContext.ts +15 -0
  283. package/src/report/downloadReport.ts +34 -0
  284. package/src/report/reportHtml.ts +547 -0
  285. package/src/routes/LearnRoutes.tsx +46 -0
  286. package/src/routes/PlanRoutes.tsx +55 -0
  287. package/src/routes/RouteFallback.tsx +9 -0
  288. package/src/socialSecurity/breakEven.ts +107 -0
  289. package/src/socialSecurity/expectedPv.ts +164 -0
  290. package/src/socialSecurity/explain.ts +92 -0
  291. package/src/socialSecurity/ficaReturn.ts +81 -0
  292. package/src/socialSecurity/persistedSsGuard.ts +138 -0
  293. package/src/socialSecurity/ssFormUtils.ts +48 -0
  294. package/src/socialSecurity/ssaStatementXml.ts +156 -0
  295. package/src/socialSecurity/storage.ts +69 -0
  296. package/src/socialSecurity/survivorSwitching.ts +153 -0
  297. package/src/testSupport/samplePlan.ts +2 -0
  298. package/src/workers/run.ts +45 -0
@@ -0,0 +1,349 @@
1
+ /** Strategy section: withdrawal order, Roth conversions, rebalancing. */
2
+
3
+ import { useId } from 'react'
4
+
5
+ import { usePlan } from '../planContextCore'
6
+ import { CheckboxField, MoneyField, NumberField, SelectField } from '../fields'
7
+ import { LearnAboutScreen } from '../../learn/LearnAboutScreen'
8
+ import { LearnLink } from '../../learn/LearnLink'
9
+ import { LEARN } from '../learnLinks'
10
+ import { fmtMoney } from '../format'
11
+ import { provenanceSource } from '../provenanceLinks'
12
+ import { Issues } from './shared'
13
+
14
+ /**
15
+ * Full account-order detail for each withdrawal mode, shown as a hint under
16
+ * the Order select. The option labels stay short so the closed control reads
17
+ * cleanly at the default column width (the long labels used to clip).
18
+ */
19
+ const WITHDRAWAL_ORDER_HINTS = {
20
+ sequential: 'Drains cash → taxable → vested equity comp → traditional → Roth, keeping HSA for last.',
21
+ proportional: 'Draws pro-rata across cash, taxable, vested equity comp, traditional, and Roth; HSA last.',
22
+ bracketTargeted:
23
+ 'Fills traditional up to the target bracket first, then cash → taxable → vested equity comp → Roth, then traditional again; HSA last.',
24
+ } as const
25
+
26
+ export function StrategySection() {
27
+ const { plan, update } = usePlan()
28
+ const w = plan.strategies.withdrawalOrder
29
+ const rc = plan.strategies.rothConversion
30
+ const orderDetailId = useId()
31
+ const thisYear = new Date().getFullYear()
32
+ return (
33
+ <section>
34
+ <div className="card">
35
+ <h2>Withdrawal strategy</h2>
36
+ <p className="card-hint">How the engine drains accounts when spending exceeds income. RMDs always come first.</p>
37
+ <div className="form-grid">
38
+ <SelectField
39
+ label="Order"
40
+ help="Sets which accounts fund spending that income doesn't cover. The order changes when taxes fall due: pre-tax withdrawals add ordinary income now, while Roth and cash come out tax-free."
41
+ learn={LEARN.withdrawalOrder}
42
+ describedBy={orderDetailId}
43
+ value={w.mode}
44
+ options={[
45
+ { value: 'sequential', label: 'Sequential' },
46
+ { value: 'proportional', label: 'Proportional' },
47
+ { value: 'bracketTargeted', label: 'Bracket-targeted' },
48
+ ]}
49
+ onCommit={(v) =>
50
+ update((d) => {
51
+ d.strategies.withdrawalOrder = v === 'bracketTargeted' ? { mode: v, bracketPct: 22 } : { mode: v }
52
+ })
53
+ }
54
+ />
55
+ {w.mode === 'bracketTargeted' ? (
56
+ <NumberField
57
+ label="Target bracket"
58
+ help="Higher brackets tax each extra dollar more; this caps how much ordinary income the strategy realizes each year."
59
+ hint="Fill ordinary income to the top of this bracket."
60
+ learn={LEARN.marginalVsEffective}
61
+ value={w.bracketPct}
62
+ suffix="%"
63
+ onCommit={(v) => update((d) => void (d.strategies.withdrawalOrder = { mode: 'bracketTargeted', bracketPct: v ?? 22 }))}
64
+ />
65
+ ) : null}
66
+ <MoneyField
67
+ label="Taxable safety-net floor"
68
+ help="An optional minimum cash + taxable reserve (today's dollars) the plan tries to keep liquid. Spending is funded from other accounts first so this cushion stays intact, and fill-to-target Roth conversions are trimmed so their tax bill never forces you below the floor. It is only dipped into as a last resort. Leave blank for no floor."
69
+ hint="Blank = no floor."
70
+ value={plan.strategies.taxableSafetyNetFloor ?? null}
71
+ allowNull
72
+ onCommit={(v) => update((d) => void (d.strategies.taxableSafetyNetFloor = v ?? undefined))}
73
+ />
74
+ {plan.household.people.length === 2 ? (
75
+ <MoneyField
76
+ label="Survivor reserve target (today's $)"
77
+ help="The minimum investable balance the surviving spouse should have in the first survivor year, in today's dollars (deflated by inflation). Used as a hard constraint by the decision engine's protect-survivor-liquidity objective — candidates whose survivor-year investable falls below this target are disqualified. Leave blank for no reserve constraint."
78
+ hint="Blank = no survivor reserve constraint."
79
+ value={plan.strategies.survivorReserveTarget ?? null}
80
+ allowNull
81
+ onCommit={(v) => update((d) => void (d.strategies.survivorReserveTarget = v ?? undefined))}
82
+ />
83
+ ) : null}
84
+ </div>
85
+ {/* Always-visible account-order detail for the selected mode: the option
86
+ labels are short so the closed select never clips, and this line keeps
87
+ the full order on screen without opening the ⓘ bubble. The select
88
+ points here via aria-describedby so the detail is announced on focus,
89
+ not only when it changes. */}
90
+ <p className="card-hint" id={orderDetailId} aria-live="polite">
91
+ {WITHDRAWAL_ORDER_HINTS[w.mode]}
92
+ </p>
93
+ </div>
94
+
95
+ <div className="card">
96
+ <h2>Roth conversions</h2>
97
+ <p className="card-hint">
98
+ Taxes on conversions ride the normal withdrawal flow (cash/taxable first). Watch the IRMAA and ACA effects in
99
+ Results. <LearnLink {...LEARN.whyConversionsRaiseCosts} />
100
+ </p>
101
+ <div className="form-grid">
102
+ <SelectField
103
+ label="Mode"
104
+ help="Roth conversions move money from a pre-tax account into Roth, adding taxable income now to cut future taxes and RMDs."
105
+ learn={LEARN.rothConversionBasics}
106
+ value={rc.mode}
107
+ options={[
108
+ { value: 'none', label: 'No conversions' },
109
+ { value: 'manual', label: 'Manual amounts per year' },
110
+ { value: 'fillToTarget', label: 'Fill to a target each year' },
111
+ // Display-only: set from the Optimize tab. Picking any other mode
112
+ // takes manual control and discards the optimized schedule.
113
+ ...(rc.mode === 'optimized' ? [{ value: 'optimized', label: 'Optimized (from the Optimize tab)' }] : []),
114
+ ]}
115
+ onCommit={(v) =>
116
+ update((d) => {
117
+ if (v === 'optimized') return // no-op; optimized is produced in the Optimize tab
118
+ d.strategies.rothConversion =
119
+ v === 'none'
120
+ ? { mode: 'none' }
121
+ : v === 'manual'
122
+ ? { mode: 'manual', conversions: [{ year: thisYear + 1, amount: 50_000 }] }
123
+ : { mode: 'fillToTarget', target: 'topOfBracket', targetValue: 22, startYear: thisYear + 1, endYear: thisYear + 10 }
124
+ })
125
+ }
126
+ />
127
+ </div>
128
+ {rc.mode === 'optimized' ? (
129
+ <div className="callout callout--info">
130
+ This {rc.conversions.length}-year schedule (
131
+ {fmtMoney(rc.conversions.reduce((a, c) => a + c.amount, 0))} total) was produced by the{' '}
132
+ <strong>Optimize</strong> tab. Re-run or tune it there, choose <em>Accept as manual</em> to edit the amounts
133
+ here, or pick another mode above to take manual control.
134
+ </div>
135
+ ) : null}
136
+ {rc.mode === 'manual' ? (
137
+ <>
138
+ {rc.conversions.map((c, i) => (
139
+ <div className="item-row" key={i}>
140
+ <div className="item-row-head">
141
+ <span className="item-row-title"><span className="type-chip">Convert</span>{c.year}</span>
142
+ <button
143
+ type="button"
144
+ className="btn-ghost btn-ghost-danger"
145
+ onClick={() =>
146
+ update((d) => {
147
+ const m = d.strategies.rothConversion
148
+ if (m.mode === 'manual') m.conversions.splice(i, 1)
149
+ })
150
+ }
151
+ >
152
+ Remove
153
+ </button>
154
+ </div>
155
+ <div className="form-grid">
156
+ <NumberField
157
+ label="Year"
158
+ value={c.year}
159
+ min={1900}
160
+ max={2200}
161
+ onCommit={(v) =>
162
+ update((d) => {
163
+ const m = d.strategies.rothConversion
164
+ if (m.mode === 'manual') m.conversions[i]!.year = Math.round(v ?? c.year)
165
+ })
166
+ }
167
+ />
168
+ <MoneyField
169
+ label="Amount"
170
+ value={c.amount}
171
+ onCommit={(v) =>
172
+ update((d) => {
173
+ const m = d.strategies.rothConversion
174
+ if (m.mode === 'manual') m.conversions[i]!.amount = v ?? 0
175
+ })
176
+ }
177
+ />
178
+ </div>
179
+ </div>
180
+ ))}
181
+ <div className="add-row">
182
+ <button
183
+ type="button"
184
+ className="btn btn-secondary btn-small"
185
+ onClick={() =>
186
+ update((d) => {
187
+ const m = d.strategies.rothConversion
188
+ if (m.mode === 'manual') {
189
+ const last = m.conversions[m.conversions.length - 1]
190
+ m.conversions.push({ year: (last?.year ?? thisYear) + 1, amount: last?.amount ?? 50_000 })
191
+ }
192
+ })
193
+ }
194
+ >
195
+ + Year
196
+ </button>
197
+ </div>
198
+ </>
199
+ ) : null}
200
+ {rc.mode === 'fillToTarget' ? (
201
+ <div className="form-grid">
202
+ <SelectField
203
+ label="Target"
204
+ help="Each year, convert just enough to reach this ceiling — a bracket top, an IRMAA tier, the ACA cliff, or a fixed MAGI."
205
+ learn={LEARN.fillingTaxBracket}
206
+ source={provenanceSource(
207
+ rc.target === 'irmaaTier' ? 'medicare-irmaa' : rc.target === 'acaCliff' ? 'aca-ptc' : 'federal-brackets',
208
+ )}
209
+ value={rc.target}
210
+ options={[
211
+ { value: 'topOfBracket', label: 'Top of tax bracket' },
212
+ { value: 'irmaaTier', label: 'IRMAA tier edge' },
213
+ { value: 'acaCliff', label: 'ACA 400% FPL cliff' },
214
+ { value: 'fixedMagi', label: 'Fixed MAGI' },
215
+ ]}
216
+ onCommit={(v) =>
217
+ update((d) => {
218
+ const m = d.strategies.rothConversion
219
+ if (m.mode === 'fillToTarget') {
220
+ m.target = v
221
+ m.targetValue = v === 'topOfBracket' ? 22 : v === 'irmaaTier' ? 1 : v === 'fixedMagi' ? 150_000 : null
222
+ }
223
+ })
224
+ }
225
+ />
226
+ {rc.target !== 'acaCliff' ? (
227
+ <NumberField
228
+ label={rc.target === 'topOfBracket' ? 'Bracket (%)' : rc.target === 'irmaaTier' ? 'Tier index' : 'MAGI ($)'}
229
+ value={rc.targetValue}
230
+ allowNull
231
+ onCommit={(v) =>
232
+ update((d) => {
233
+ const m = d.strategies.rothConversion
234
+ if (m.mode === 'fillToTarget') m.targetValue = v
235
+ })
236
+ }
237
+ />
238
+ ) : null}
239
+ <NumberField
240
+ label="Start year"
241
+ value={rc.startYear}
242
+ min={1900}
243
+ max={2200}
244
+ onCommit={(v) =>
245
+ update((d) => {
246
+ const m = d.strategies.rothConversion
247
+ if (m.mode === 'fillToTarget') m.startYear = Math.round(v ?? thisYear)
248
+ })
249
+ }
250
+ />
251
+ <NumberField
252
+ label="End year"
253
+ value={rc.endYear}
254
+ min={1900}
255
+ max={2200}
256
+ onCommit={(v) =>
257
+ update((d) => {
258
+ const m = d.strategies.rothConversion
259
+ if (m.mode === 'fillToTarget') m.endYear = Math.round(v ?? thisYear + 10)
260
+ })
261
+ }
262
+ />
263
+ </div>
264
+ ) : null}
265
+ </div>
266
+
267
+ <div className="card">
268
+ <h2>Charitable giving</h2>
269
+ <div className="form-grid">
270
+ <MoneyField
271
+ label="QCD per year (today's $)"
272
+ help="A qualified charitable distribution sends IRA money straight to charity from age 70½. It counts toward your RMD but never appears in taxable income — usually better than a deductible cash gift once you take the standard deduction."
273
+ learn={LEARN.qcd}
274
+ source={provenanceSource('rmd-qcd')}
275
+ hint="Routed out of RMDs from age 70½; excluded from income."
276
+ value={plan.strategies.qcdAnnual}
277
+ onCommit={(v) => update((d) => void (d.strategies.qcdAnnual = v ?? 0))}
278
+ />
279
+ </div>
280
+ <Issues />
281
+ </div>
282
+
283
+ <div className="card">
284
+ <h2>Itemized deductions</h2>
285
+ <p className="card-hint">
286
+ Federal tax uses the larger of these and the standard deduction. Most retirees take the standard deduction —
287
+ turn this on only if your deductible taxes, mortgage interest, and charitable gifts together exceed it. Enter
288
+ today's dollars.
289
+ </p>
290
+ <div className="form-grid">
291
+ <CheckboxField
292
+ label="Itemize deductions"
293
+ help="When on, the projection compares your itemized total (SALT capped at the current-law limit) against the standard deduction each year and uses whichever is larger. The OBBBA senior deduction still applies on top either way."
294
+ learn={LEARN.itemizedDeductions}
295
+ value={plan.strategies.itemizedDeductions !== undefined}
296
+ onCommit={(v) =>
297
+ update((d) => void (d.strategies.itemizedDeductions = v ? { stateAndLocalTaxes: 0, mortgageInterest: 0, charitable: 0 } : undefined))
298
+ }
299
+ />
300
+ </div>
301
+ {plan.strategies.itemizedDeductions ? (
302
+ <div className="form-grid">
303
+ <MoneyField
304
+ label="State & local taxes (SALT)"
305
+ hint="State income + property tax; capped at the current-law SALT limit."
306
+ value={plan.strategies.itemizedDeductions.stateAndLocalTaxes}
307
+ onCommit={(v) => update((d) => { if (d.strategies.itemizedDeductions) d.strategies.itemizedDeductions.stateAndLocalTaxes = v ?? 0 })}
308
+ />
309
+ <MoneyField
310
+ label="Mortgage interest"
311
+ value={plan.strategies.itemizedDeductions.mortgageInterest}
312
+ onCommit={(v) => update((d) => { if (d.strategies.itemizedDeductions) d.strategies.itemizedDeductions.mortgageInterest = v ?? 0 })}
313
+ />
314
+ <MoneyField
315
+ label="Charitable gifts"
316
+ value={plan.strategies.itemizedDeductions.charitable}
317
+ onCommit={(v) => update((d) => { if (d.strategies.itemizedDeductions) d.strategies.itemizedDeductions.charitable = v ?? 0 })}
318
+ />
319
+ </div>
320
+ ) : null}
321
+ </div>
322
+
323
+ <div className="card">
324
+ <h2>Capital loss carryforward</h2>
325
+ <p className="card-hint">
326
+ Net capital losses you're carrying from prior years' tax returns (Schedule D line 16). The plan nets them
327
+ against future realized gains first, then deducts up to $3,000/yr against other income, until used up —
328
+ lowering tax and AGI (and the IRMAA/ACA costs AGI drives) in early-retirement years spent drawing down a
329
+ brokerage. Most users leave this at $0. <LearnLink {...LEARN.lossHarvesting} />
330
+ </p>
331
+ <div className="form-grid">
332
+ <MoneyField
333
+ label="Capital loss carryforward"
334
+ help="The net capital loss carried into this plan's first year, in today's dollars. Treated as flat nominal (capital losses don't index). Nets against realized gains first, then up to $3,000/yr against other income."
335
+ value={plan.household.capitalLossCarryforward}
336
+ onCommit={(v) => update((d) => void (d.household.capitalLossCarryforward = Math.max(0, v ?? 0)))}
337
+ />
338
+ </div>
339
+ </div>
340
+
341
+ <LearnAboutScreen route="/plan/:planId/strategy" />
342
+ </section>
343
+ )
344
+ }
345
+
346
+ // ---------------------------------------------------------------------------
347
+ // Assumptions
348
+ // ---------------------------------------------------------------------------
349
+
@@ -0,0 +1,182 @@
1
+ /**
2
+ * "Update balances from a broker CSV" — the returning-user path of the broker
3
+ * import (onboarding-import-and-migration step 2, annual-checkup posture).
4
+ * Parses a Schwab/Fidelity/Vanguard positions CSV on-device and lets the user
5
+ * assign each account found in the file to a plan account before applying.
6
+ */
7
+
8
+ import { useRef, useState } from 'react'
9
+
10
+ import {
11
+ applyBrokerBalance,
12
+ BROKER_LABEL,
13
+ isBalanceUpdatable,
14
+ parseBrokerPositionsCsv,
15
+ type BrokerAccountBalance,
16
+ type BrokerId,
17
+ } from '../../import/brokerCsv'
18
+ import type { ImportReviewItem } from '../../import/reviewChecklist'
19
+ import { ReviewChecklist } from '../../import/ReviewChecklistView'
20
+ import { usePlan } from '../planContextCore'
21
+ import { fmtMoney } from '../format'
22
+
23
+ interface ParsedFile {
24
+ broker: BrokerId
25
+ accounts: BrokerAccountBalance[]
26
+ /** Selected plan-account id (or '') per parsed account, by index. */
27
+ targets: string[]
28
+ /** The parser's honesty checklist (partial basis, skipped rows, …). */
29
+ review: ImportReviewItem[]
30
+ }
31
+
32
+ /** Prefer a plan account whose name shares words with the broker label. */
33
+ function guessTarget(label: string, candidates: Array<{ id: string; name: string }>): string {
34
+ const l = label.toLowerCase()
35
+ const hit = candidates.find((c) => l.includes(c.name.toLowerCase()) || c.name.toLowerCase().split(/\s+/).some((w) => w.length > 2 && l.includes(w)))
36
+ return hit?.id ?? ''
37
+ }
38
+
39
+ export function UpdateBalancesPanel() {
40
+ const { plan, update } = usePlan()
41
+ const [parsed, setParsed] = useState<ParsedFile | null>(null)
42
+ const [message, setMessage] = useState<string | null>(null)
43
+ const fileInput = useRef<HTMLInputElement>(null)
44
+
45
+ const updatable = plan.accounts.filter(isBalanceUpdatable).map((a) => ({ id: a.id, name: a.name }))
46
+
47
+ const handleFile = async (file: File) => {
48
+ setMessage(null)
49
+ const r = parseBrokerPositionsCsv(await file.text())
50
+ if (!r.ok) {
51
+ setParsed(null)
52
+ setMessage(r.message)
53
+ return
54
+ }
55
+ setParsed({
56
+ broker: r.broker,
57
+ accounts: r.accounts,
58
+ targets: r.accounts.map((acc) => guessTarget(acc.accountLabel, updatable)),
59
+ review: r.review,
60
+ })
61
+ }
62
+
63
+ // Two file accounts pointed at one plan account would silently last-write-win.
64
+ const duplicateTargets = parsed
65
+ ? [...new Set(parsed.targets.filter((t, i) => t !== '' && parsed.targets.indexOf(t) !== i))]
66
+ : []
67
+ const duplicateNames = duplicateTargets.map((id) => updatable.find((a) => a.id === id)?.name ?? id)
68
+
69
+ const apply = () => {
70
+ if (!parsed || duplicateTargets.length > 0) return
71
+ let applied = 0
72
+ update((d) => {
73
+ parsed.accounts.forEach((source, i) => {
74
+ const targetId = parsed.targets[i]
75
+ if (!targetId) return
76
+ const idx = d.accounts.findIndex((a) => a.id === targetId)
77
+ if (idx === -1) return
78
+ d.accounts[idx] = applyBrokerBalance(d.accounts[idx]!, source)
79
+ applied++
80
+ })
81
+ })
82
+ setParsed(null)
83
+ setMessage(
84
+ applied === 0
85
+ ? 'No accounts were assigned, so nothing changed.'
86
+ : `Updated ${applied} account${applied === 1 ? '' : 's'} from the ${BROKER_LABEL[parsed.broker]} file — balances, plus cost basis where the file carried it. Review taxable accounts whose basis the file lacked.`,
87
+ )
88
+ }
89
+
90
+ return (
91
+ <div className="card">
92
+ <h2>Update balances from a broker CSV</h2>
93
+ <p className="card-hint">
94
+ Download the positions/holdings CSV from Schwab, Fidelity, or Vanguard and refresh your account
95
+ balances (and cost basis where the file has it) without retyping. The file is read on this device
96
+ only. To start a whole new plan from a file, use Import &amp; migrate on the home screen.
97
+ </p>
98
+ {message ? (
99
+ <div className="callout callout--info" role="status">
100
+ {message}
101
+ </div>
102
+ ) : null}
103
+ {parsed ? (
104
+ <>
105
+ <div className="year-table-wrap">
106
+ <table className="year-table">
107
+ <thead>
108
+ <tr>
109
+ <th scope="col">In the {BROKER_LABEL[parsed.broker]} file</th>
110
+ <th scope="col">Value</th>
111
+ <th scope="col">Cost basis</th>
112
+ <th scope="col">Apply to plan account</th>
113
+ </tr>
114
+ </thead>
115
+ <tbody>
116
+ {parsed.accounts.map((acc, i) => (
117
+ <tr key={`${acc.accountLabel}-${i}`}>
118
+ <td>{acc.accountLabel}</td>
119
+ <td>{fmtMoney(acc.totalValue)}</td>
120
+ <td>{acc.costBasis === null ? '—' : fmtMoney(acc.costBasis)}</td>
121
+ <td>
122
+ <select
123
+ aria-label={`Plan account for ${acc.accountLabel}`}
124
+ value={parsed.targets[i] ?? ''}
125
+ onChange={(e) =>
126
+ setParsed((prev) =>
127
+ prev ? { ...prev, targets: prev.targets.map((t, j) => (j === i ? e.target.value : t)) } : prev,
128
+ )
129
+ }
130
+ >
131
+ <option value="">Don&apos;t update</option>
132
+ {updatable.map((a) => (
133
+ <option key={a.id} value={a.id}>
134
+ {a.name}
135
+ </option>
136
+ ))}
137
+ </select>
138
+ </td>
139
+ </tr>
140
+ ))}
141
+ </tbody>
142
+ </table>
143
+ </div>
144
+ {duplicateNames.length > 0 ? (
145
+ <div className="callout callout--warn" role="alert">
146
+ Two rows are assigned to the same plan account ({duplicateNames.join(', ')}) — the second would silently
147
+ overwrite the first. Assign each plan account at most once.
148
+ </div>
149
+ ) : null}
150
+ <ReviewChecklist items={parsed.review} />
151
+ <div className="picker-actions">
152
+ <button type="button" className="btn btn-primary" onClick={apply} disabled={duplicateNames.length > 0}>
153
+ Apply selected balances
154
+ </button>
155
+ <button type="button" className="btn btn-secondary" onClick={() => setParsed(null)}>
156
+ Cancel
157
+ </button>
158
+ </div>
159
+ </>
160
+ ) : (
161
+ <div className="picker-actions" style={{ margin: 0 }}>
162
+ <button type="button" className="btn btn-secondary" onClick={() => fileInput.current?.click()}>
163
+ Choose broker CSV
164
+ </button>
165
+ </div>
166
+ )}
167
+ <input
168
+ ref={fileInput}
169
+ type="file"
170
+ accept=".csv,text/csv"
171
+ className="sr-only"
172
+ tabIndex={-1}
173
+ aria-hidden="true"
174
+ onChange={(e) => {
175
+ const f = e.target.files?.[0]
176
+ if (f) void handleFile(f)
177
+ e.target.value = ''
178
+ }}
179
+ />
180
+ </div>
181
+ )
182
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Non-component helpers shared by the plan entry sections (kept out of the
3
+ * component files so react-refresh sees component-only modules).
4
+ */
5
+
6
+ import type { Account, AllocationWeights } from '@retiregolden/engine/model/plan'
7
+
8
+ export const newId = () => crypto.randomUUID()
9
+
10
+ export const MONTH_OPTIONS = [
11
+ 'January',
12
+ 'February',
13
+ 'March',
14
+ 'April',
15
+ 'May',
16
+ 'June',
17
+ 'July',
18
+ 'August',
19
+ 'September',
20
+ 'October',
21
+ 'November',
22
+ 'December',
23
+ ].map((label, i) => ({ value: String(i + 1), label }))
24
+
25
+ export function isIndividuallyOwnedAccount(type: Account['type']): boolean {
26
+ return type === 'traditional' || type === 'roth' || type === 'hsa'
27
+ }
28
+
29
+ export const ACCOUNT_LABEL: Record<Account['type'], string> = {
30
+ cash: 'Cash',
31
+ taxable: 'Brokerage',
32
+ equityComp: 'Equity comp',
33
+ traditional: 'Traditional',
34
+ roth: 'Roth',
35
+ hsa: 'HSA',
36
+ pension: 'Pension',
37
+ annuity: 'Annuity',
38
+ property: 'Property',
39
+ debt: 'Debt',
40
+ }
41
+
42
+ export type AllocatableAccount = Extract<Account, { type: 'taxable' | 'traditional' | 'roth' | 'hsa' }>
43
+
44
+ export const EVEN_START_WEIGHTS: AllocationWeights = { usStocks: 60, intlStocks: 10, bonds: 25, cash: 5 }
45
+
46
+ export function isAllocatable(account: Account): account is AllocatableAccount {
47
+ return account.type === 'taxable' || account.type === 'traditional' || account.type === 'roth' || account.type === 'hsa'
48
+ }
@@ -0,0 +1,15 @@
1
+ /** Validation-issue list shared by the plan entry sections. */
2
+
3
+ import { usePlan } from '../planContextCore'
4
+
5
+ export function Issues() {
6
+ const { issues } = usePlan()
7
+ if (issues.length === 0) return null
8
+ return (
9
+ <ul className="issue-list">
10
+ {issues.map((i) => (
11
+ <li key={i}>{i}</li>
12
+ ))}
13
+ </ul>
14
+ )
15
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Plan entry sections. Each card edits one slice of the Plan via
3
+ * usePlan().update; validation issues from the Zod schema surface beneath
4
+ * the active section. The section components live in ./sections/; this
5
+ * barrel keeps existing import paths stable.
6
+ */
7
+
8
+ export { HouseholdSection } from './sections/HouseholdSection'
9
+ export { AccountsSection } from './sections/AccountsSection'
10
+ export { IncomeSection } from './sections/IncomeSection'
11
+ export { IncomeFloorSection } from './sections/IncomeFloorSection'
12
+ export { SpendingSection } from './sections/SpendingSection'
13
+ export { StrategySection } from './sections/StrategySection'
14
+ export { AssumptionsSection } from './sections/AssumptionsSection'
15
+ export { InsuranceSection } from './sections/InsuranceSection'