@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,89 @@
1
+ /** Accounts section: add/edit accounts. */
2
+
3
+ import type { Account } from '@retiregolden/engine/model/plan'
4
+ import { AccountFields } from './AccountFields'
5
+ import { ACCOUNT_LABEL, isIndividuallyOwnedAccount } from './sectionHelpers'
6
+ import { usePlan } from '../planContextCore'
7
+ import { Issues } from './shared'
8
+ import { newId } from './sectionHelpers'
9
+ import { UpdateBalancesPanel } from './UpdateBalancesPanel'
10
+
11
+ function makeAccount(type: Account['type'], primaryPersonId: string): Account {
12
+ const base = { id: newId(), ownerPersonId: isIndividuallyOwnedAccount(type) ? primaryPersonId : null, annualReturnPct: null }
13
+ switch (type) {
14
+ case 'cash':
15
+ return { ...base, type, name: 'Savings', balance: 0, annualContribution: 0 }
16
+ case 'taxable':
17
+ return {
18
+ ...base,
19
+ type,
20
+ name: 'Brokerage',
21
+ balance: 0,
22
+ costBasis: 0,
23
+ interestYieldPct: 0,
24
+ dividendYieldPct: 0,
25
+ qualifiedRatio: 0.85,
26
+ reinvestDividends: true,
27
+ annualContribution: 0,
28
+ }
29
+ case 'equityComp':
30
+ return { ...base, type, name: 'RSU / ESPP', balance: 0, costBasis: 0, annualContribution: 0, vestingMode: 'cliff', vestDate: `${new Date().getFullYear()}-12-31` }
31
+ case 'traditional':
32
+ return { ...base, type, name: '401(k)', kind: 'employer', balance: 0, annualContribution: 0 }
33
+ case 'roth':
34
+ return { ...base, type, name: 'Roth IRA', kind: 'ira', balance: 0, annualContribution: 0 }
35
+ case 'hsa':
36
+ return { ...base, type, name: 'HSA', balance: 0, annualContribution: 0 }
37
+ case 'pension':
38
+ return { ...base, type, name: 'Pension', source: 'private', startAge: 65, monthlyAmount: 0, colaPct: 0, survivorPct: 50 }
39
+ case 'annuity':
40
+ return { ...base, type, name: 'Annuity', startAge: 65, monthlyAmount: 0, colaPct: 0, taxablePct: 100 }
41
+ case 'property':
42
+ return { ...base, type, name: 'Home', value: 0, plannedSaleYear: null, expectedNetProceeds: null }
43
+ case 'debt':
44
+ return { ...base, type, name: 'Mortgage', balance: 0, interestPct: 5, monthlyPayment: 0 }
45
+ }
46
+ }
47
+
48
+
49
+ export function AccountsSection() {
50
+ const { plan, update } = usePlan()
51
+ const primaryPersonId = plan.household.people[0]!.id
52
+ return (
53
+ <section>
54
+ <div className="card">
55
+ <h2>Accounts</h2>
56
+ <p className="card-hint">Balances as of today. Investable accounts grow at their expected return (or the default assumption) and are drained per your withdrawal strategy.</p>
57
+ {plan.accounts.length === 0 ? <div className="empty-state"><p>No accounts yet — add your first below.</p></div> : null}
58
+ {plan.accounts.map((a, i) => (
59
+ <div className="item-row" key={a.id} data-testid="account-row" data-account-type={a.type} data-account-name={a.name}>
60
+ <div className="item-row-head">
61
+ <span className="item-row-title">
62
+ <span className="type-chip">{ACCOUNT_LABEL[a.type]}</span>
63
+ {a.name}
64
+ </span>
65
+ <button type="button" className="btn-ghost btn-ghost-danger" onClick={() => update((d) => void d.accounts.splice(i, 1))}>
66
+ Remove
67
+ </button>
68
+ </div>
69
+ <AccountFields account={a} index={i} />
70
+ </div>
71
+ ))}
72
+ <div className="add-row">
73
+ {(Object.keys(ACCOUNT_LABEL) as Account['type'][]).map((t) => (
74
+ <button key={t} type="button" className="btn btn-secondary btn-small" onClick={() => update((d) => void d.accounts.push(makeAccount(t, primaryPersonId)))}>
75
+ + {ACCOUNT_LABEL[t]}
76
+ </button>
77
+ ))}
78
+ </div>
79
+ <Issues />
80
+ </div>
81
+ <UpdateBalancesPanel />
82
+ </section>
83
+ )
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Income
88
+ // ---------------------------------------------------------------------------
89
+
@@ -0,0 +1,261 @@
1
+ /** Asset-allocation panel and return estimator for allocatable accounts. */
2
+
3
+ import { useState } from 'react'
4
+
5
+ import type { AllocationWeights, AssetAllocationPolicy, Plan } from '@retiregolden/engine/model/plan'
6
+ import { ASSET_CLASS_IDS } from '@retiregolden/engine/model/plan'
7
+ import type { AllocatableAccount } from './sectionHelpers'
8
+ import {
9
+ blendedReturnPct,
10
+ blendedTaxableYield,
11
+ DEFAULT_ASSET_CLASS_PARAMS,
12
+ resolveAssetClassParams,
13
+ targetWeightsAt,
14
+ } from '@retiregolden/engine/allocation/assetClasses'
15
+ import { NumberField, PercentField, ReadonlyField, SelectField } from '../fields'
16
+ import { Modal } from '../Modal'
17
+ import { currentStartYear } from '../useProjection'
18
+
19
+ /**
20
+ * Allocation → expected-return estimator. Long-run nominal assumptions
21
+ * (illustrative, before fees): stocks ≈ 7%, bonds ≈ 4%, cash ≈ 2.5%.
22
+ */
23
+ const ASSET_RETURN = { stocks: 7, bonds: 4, cash: 2.5 } as const
24
+
25
+ function riskLabel(stocksPct: number): string {
26
+ if (stocksPct < 25) return 'Conservative — low volatility, lower growth'
27
+ if (stocksPct < 50) return 'Balanced — moderate ups and downs'
28
+ if (stocksPct < 75) return 'Growth — meaningful swings in bad years'
29
+ return 'Aggressive — expect large drawdowns on the way'
30
+ }
31
+
32
+ export function ReturnEstimatorModal({ initialPct, onApply, onClose }: { initialPct: number | null; onApply: (pct: number) => void; onClose: () => void }) {
33
+ const guessStocks = initialPct === null ? 60 : Math.min(100, Math.max(0, Math.round(((initialPct - ASSET_RETURN.cash) / (ASSET_RETURN.stocks - ASSET_RETURN.cash)) * 100 / 5) * 5))
34
+ const [stocks, setStocks] = useState(guessStocks)
35
+ const [bonds, setBonds] = useState(Math.min(100 - guessStocks, 30))
36
+ const cash = Math.max(0, 100 - stocks - bonds)
37
+ const blended = (stocks * ASSET_RETURN.stocks + bonds * ASSET_RETURN.bonds + cash * ASSET_RETURN.cash) / 100
38
+ return (
39
+ <Modal title="Estimate expected return" onClose={onClose}>
40
+ <p className="card-hint">
41
+ Describe roughly how this account is invested; we blend long-run nominal return assumptions (stocks {ASSET_RETURN.stocks}%,
42
+ bonds {ASSET_RETURN.bonds}%, cash {ASSET_RETURN.cash}% — illustrative, before fees, not a forecast). More stocks
43
+ means higher expected growth and bigger swings; Monte Carlo is where that risk shows up.
44
+ </p>
45
+ <div className="alloc-row">
46
+ <span>Stocks</span>
47
+ <input
48
+ type="range"
49
+ min={0}
50
+ max={100}
51
+ step={5}
52
+ value={stocks}
53
+ aria-label="Stocks percent"
54
+ onChange={(e) => {
55
+ const v = Number(e.target.value)
56
+ setStocks(v)
57
+ setBonds((b) => Math.min(b, 100 - v))
58
+ }}
59
+ />
60
+ <span>{stocks}%</span>
61
+ </div>
62
+ <div className="alloc-row">
63
+ <span>Bonds</span>
64
+ <input
65
+ type="range"
66
+ min={0}
67
+ max={100 - stocks}
68
+ step={5}
69
+ value={bonds}
70
+ aria-label="Bonds percent"
71
+ onChange={(e) => setBonds(Number(e.target.value))}
72
+ />
73
+ <span>{bonds}%</span>
74
+ </div>
75
+ <div className="alloc-row">
76
+ <span>Cash</span>
77
+ <span className="muted small">remainder</span>
78
+ <span>{cash}%</span>
79
+ </div>
80
+ <div className="alloc-result">
81
+ <strong>{blended.toFixed(1)}%</strong>
82
+ <span className="muted small">{riskLabel(stocks)}</span>
83
+ </div>
84
+ <div className="add-row" style={{ marginTop: '1rem' }}>
85
+ <button type="button" className="btn btn-primary btn-small" onClick={() => { onApply(Math.round(blended * 10) / 10); onClose() }}>
86
+ Use {blended.toFixed(1)}%
87
+ </button>
88
+ <button type="button" className="btn btn-secondary btn-small" onClick={onClose}>
89
+ Cancel
90
+ </button>
91
+ </div>
92
+ </Modal>
93
+ )
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Asset allocation (opt-in per account)
98
+ // ---------------------------------------------------------------------------
99
+
100
+ function WeightsGrid({ title, weights, onCommit }: { title?: string; weights: AllocationWeights; onCommit: (w: AllocationWeights) => void }) {
101
+ const sum = ASSET_CLASS_IDS.reduce((s, id) => s + weights[id], 0)
102
+ return (
103
+ <div className="nested-form-section field-span-full">
104
+ {title ? <h4>{title}</h4> : null}
105
+ <div className="form-grid nested-control-grid">
106
+ {ASSET_CLASS_IDS.map((id) => (
107
+ <PercentField
108
+ key={id}
109
+ label={DEFAULT_ASSET_CLASS_PARAMS[id].label}
110
+ value={weights[id]}
111
+ min={0}
112
+ max={100}
113
+ step={5}
114
+ onCommit={(v) => onCommit({ ...weights, [id]: Math.min(100, Math.max(0, v ?? 0)) })}
115
+ />
116
+ ))}
117
+ </div>
118
+ <p className={Math.abs(sum - 100) <= 0.5 ? 'muted small' : 'issue-list small'} role="status">
119
+ Total {sum.toFixed(0)}%{Math.abs(sum - 100) > 0.5 ? ' — weights must sum to 100%' : ''}
120
+ </p>
121
+ </div>
122
+ )
123
+ }
124
+
125
+ /**
126
+ * Allocation policy editor: mode (static / glidepaths), rebalancing, and the
127
+ * weight grid(s) for the selected mode. All money math (blends, per-year
128
+ * targets) comes from engine/allocation.
129
+ */
130
+ export function AllocationPanel({ account, plan, onCommit }: { account: AllocatableAccount; plan: Plan; onCommit: (a: AssetAllocationPolicy) => void }) {
131
+ const policy = account.allocation!
132
+ const year = currentStartYear()
133
+ const params = resolveAssetClassParams(plan.assumptions.assetClassParams)
134
+ const weightsNow = targetWeightsAt(policy, year)
135
+ const blendedNow = blendedReturnPct(weightsNow, params)
136
+ const yieldNow = blendedTaxableYield(weightsNow, params)
137
+
138
+ const currentWeights = (): AllocationWeights => {
139
+ const w = targetWeightsAt(policy, year)
140
+ return {
141
+ usStocks: Math.round(w[0]! * 100),
142
+ intlStocks: Math.round(w[1]! * 100),
143
+ bonds: Math.round(w[2]! * 100),
144
+ cash: Math.round(w[3]! * 100),
145
+ }
146
+ }
147
+ const switchMode = (mode: AssetAllocationPolicy['mode']) => {
148
+ if (mode === policy.mode) return
149
+ const weights = currentWeights()
150
+ const shared = { rebalancing: policy.rebalancing }
151
+ if (mode === 'static') onCommit({ mode, ...shared, weights })
152
+ else if (mode === 'linear') onCommit({ mode, ...shared, from: weights, to: weights, startYear: year, endYear: year + 20 })
153
+ else if (mode === 'staged') onCommit({ mode, ...shared, stages: [{ fromYear: year, weights }] })
154
+ else onCommit({ mode, ...shared, targets: [{ year, weights }] })
155
+ }
156
+
157
+ return (
158
+ <div className="nested-form-section field-span-full" data-testid="allocation-panel">
159
+ <div className="form-grid nested-control-grid">
160
+ <SelectField
161
+ label="Allocation over time"
162
+ help="Static holds one target mix. A linear glidepath slides from a starting mix to an ending mix between two years. Staged jumps to a new mix in given years; custom targets interpolate between the mixes you set."
163
+ value={policy.mode}
164
+ options={[
165
+ { value: 'static', label: 'Static target mix' },
166
+ { value: 'linear', label: 'Linear glidepath (from → to)' },
167
+ { value: 'staged', label: 'Staged (step changes)' },
168
+ { value: 'custom', label: 'Custom year targets' },
169
+ ]}
170
+ onCommit={switchMode}
171
+ />
172
+ <SelectField
173
+ label="Rebalancing"
174
+ help="Annual rebalancing trades back to the target mix each January. In a brokerage account those sales realize capital gains through your cost basis; retirement-account rebalancing is tax-free. Drift lets winners run — the mix (and risk) then wanders from the target."
175
+ value={policy.rebalancing}
176
+ options={[
177
+ { value: 'annual', label: 'Rebalance annually to target' },
178
+ { value: 'none', label: 'No rebalancing (drift)' },
179
+ ]}
180
+ onCommit={(v) => onCommit({ ...policy, rebalancing: v })}
181
+ />
182
+ </div>
183
+ {policy.mode === 'static' ? (
184
+ <WeightsGrid weights={policy.weights} onCommit={(weights) => onCommit({ ...policy, weights })} />
185
+ ) : null}
186
+ {policy.mode === 'linear' ? (
187
+ <>
188
+ <div className="form-grid nested-control-grid">
189
+ <NumberField label="From year" value={policy.startYear} min={1900} max={2200} onCommit={(v) => onCommit({ ...policy, startYear: Math.round(v ?? year) })} />
190
+ <NumberField label="To year" value={policy.endYear} min={1900} max={2200} onCommit={(v) => onCommit({ ...policy, endYear: Math.round(v ?? year + 20) })} />
191
+ </div>
192
+ <WeightsGrid title="Starting mix" weights={policy.from} onCommit={(from) => onCommit({ ...policy, from })} />
193
+ <WeightsGrid title="Ending mix" weights={policy.to} onCommit={(to) => onCommit({ ...policy, to })} />
194
+ </>
195
+ ) : null}
196
+ {policy.mode === 'staged' || policy.mode === 'custom' ? (
197
+ <>
198
+ {(policy.mode === 'staged' ? policy.stages : policy.targets).map((row, i) => {
199
+ const rows = policy.mode === 'staged' ? policy.stages : policy.targets
200
+ const setRows = (next: typeof rows) =>
201
+ policy.mode === 'staged'
202
+ ? onCommit({ ...policy, stages: next as { fromYear: number; weights: AllocationWeights }[] })
203
+ : onCommit({ ...policy, targets: next as { year: number; weights: AllocationWeights }[] })
204
+ const rowYear = 'fromYear' in row ? row.fromYear : row.year
205
+ return (
206
+ <div key={i} className="nested-phase-row">
207
+ <div className="form-grid nested-phase-grid">
208
+ <NumberField
209
+ label={policy.mode === 'staged' ? 'From year' : 'Target year'}
210
+ value={rowYear}
211
+ min={1900}
212
+ max={2200}
213
+ onCommit={(v) => {
214
+ const y = Math.round(v ?? rowYear)
215
+ setRows(rows.map((r, idx) => (idx === i ? ('fromYear' in r ? { ...r, fromYear: y } : { ...r, year: y }) : r)) as typeof rows)
216
+ }}
217
+ />
218
+ </div>
219
+ <WeightsGrid
220
+ weights={row.weights}
221
+ onCommit={(weights) => setRows(rows.map((r, idx) => (idx === i ? { ...r, weights } : r)) as typeof rows)}
222
+ />
223
+ {rows.length > 1 ? (
224
+ <button type="button" className="btn btn-secondary btn-small" onClick={() => setRows(rows.filter((_, idx) => idx !== i) as typeof rows)}>
225
+ Remove
226
+ </button>
227
+ ) : null}
228
+ </div>
229
+ )
230
+ })}
231
+ <button
232
+ type="button"
233
+ className="btn btn-secondary btn-small"
234
+ onClick={() => {
235
+ const weights = currentWeights()
236
+ if (policy.mode === 'staged') {
237
+ const lastYear = policy.stages[policy.stages.length - 1]?.fromYear ?? year
238
+ onCommit({ ...policy, stages: [...policy.stages, { fromYear: lastYear + 5, weights }] })
239
+ } else {
240
+ const lastYear = policy.targets[policy.targets.length - 1]?.year ?? year
241
+ onCommit({ ...policy, targets: [...policy.targets, { year: lastYear + 5, weights }] })
242
+ }
243
+ }}
244
+ >
245
+ + Add {policy.mode === 'staged' ? 'stage' : 'target'}
246
+ </button>
247
+ </>
248
+ ) : null}
249
+ <ReadonlyField
250
+ label="This year's blend"
251
+ help="Expected nominal return of this account's target mix this year, blended from the asset-class assumptions. For a brokerage account, the mix also sets the taxable interest/dividend yield used for annual tax drag."
252
+ value={
253
+ account.type === 'taxable'
254
+ ? `${blendedNow.toFixed(1)}% return · ${yieldNow.interestYieldPct.toFixed(1)}% interest + ${yieldNow.dividendYieldPct.toFixed(1)}% dividends`
255
+ : `${blendedNow.toFixed(1)}% return`
256
+ }
257
+ />
258
+ </div>
259
+ )
260
+ }
261
+
@@ -0,0 +1,256 @@
1
+ /** Assumptions section: inflation, returns, taxes, asset classes. */
2
+
3
+ import { Link } from 'react-router-dom'
4
+
5
+ import type { AssetClassId } from '@retiregolden/engine/model/plan'
6
+ import { ASSET_CLASS_IDS } from '@retiregolden/engine/model/plan'
7
+ import { DEFAULT_ASSET_CLASS_PARAMS, resolveAssetClassParams } from '@retiregolden/engine/allocation/assetClasses'
8
+ import { LATEST_PACK_YEAR, TRUSTEES_DEFAULT_SS_HAIRCUT } from '@retiregolden/engine/params'
9
+ import { usePlan } from '../planContextCore'
10
+ import { provenanceSource } from '../provenanceLinks'
11
+ import { CheckboxField, MoneyField, NumberField, PercentField, SelectField } from '../fields'
12
+ import { LearnAboutScreen } from '../../learn/LearnAboutScreen'
13
+ import { LEARN } from '../learnLinks'
14
+ import { Issues } from './shared'
15
+
16
+ /**
17
+ * Assumptions-level asset-class table: return / volatility / yield per class,
18
+ * shown with the sourced defaults and editable as overrides. Only accounts
19
+ * that opt into "Model asset classes" use these numbers.
20
+ */
21
+ function AssetClassAssumptions() {
22
+ const { plan, update } = usePlan()
23
+ const resolved = resolveAssetClassParams(plan.assumptions.assetClassParams)
24
+ const hasOverrides = plan.assumptions.assetClassParams !== undefined
25
+ type OverrideKey = 'returnPct' | 'volatilityPct' | 'interestYieldPct' | 'dividendYieldPct' | 'qualifiedRatioPct'
26
+ const setParam = (id: AssetClassId, key: OverrideKey, v: number) =>
27
+ update((d) => {
28
+ const overrides = d.assumptions.assetClassParams ?? {}
29
+ overrides[id] = { ...overrides[id], [key]: v }
30
+ d.assumptions.assetClassParams = overrides
31
+ })
32
+ return (
33
+ <>
34
+ <h3>Asset classes</h3>
35
+ <p className="card-hint">
36
+ Used by accounts with "Model asset classes" turned on: growth blends these returns, Monte Carlo shocks each
37
+ class with long-horizon historical volatility and correlations, and brokerage yields drive annual tax drag.
38
+ Defaults are documented planning assumptions — edit them to match your own outlook.
39
+ {hasOverrides ? (
40
+ <>
41
+ {' '}
42
+ <button
43
+ type="button"
44
+ className="btn-ghost"
45
+ onClick={() => update((d) => void (d.assumptions.assetClassParams = undefined))}
46
+ >
47
+ Reset to defaults
48
+ </button>
49
+ </>
50
+ ) : null}
51
+ </p>
52
+ {ASSET_CLASS_IDS.map((id) => {
53
+ const p = resolved[id]
54
+ const stockLike = id === 'usStocks' || id === 'intlStocks'
55
+ return (
56
+ <div key={id} className="nested-form-section" data-testid={`asset-class-${id}`}>
57
+ <h4>{p.label}</h4>
58
+ <div className="form-grid nested-control-grid">
59
+ <PercentField label="Expected return" value={p.returnPct} onCommit={(v) => setParam(id, 'returnPct', v ?? DEFAULT_ASSET_CLASS_PARAMS[id].returnPct)} />
60
+ <PercentField label="Volatility" help="Annual standard deviation of returns; drives the size of this class's Monte Carlo shocks." value={p.volatilityPct} min={0} onCommit={(v) => setParam(id, 'volatilityPct', v ?? DEFAULT_ASSET_CLASS_PARAMS[id].volatilityPct)} />
61
+ {stockLike ? (
62
+ <>
63
+ <PercentField label="Dividend yield" help="Distributed as dividends each year; taxable in a brokerage account." value={p.dividendYieldPct} min={0} onCommit={(v) => setParam(id, 'dividendYieldPct', v ?? DEFAULT_ASSET_CLASS_PARAMS[id].dividendYieldPct)} />
64
+ <PercentField label="Qualified share" help="Share of this class's dividends taxed at long-term capital-gain rates." value={p.qualifiedRatioPct} min={0} max={100} onCommit={(v) => setParam(id, 'qualifiedRatioPct', v ?? DEFAULT_ASSET_CLASS_PARAMS[id].qualifiedRatioPct)} />
65
+ </>
66
+ ) : (
67
+ <PercentField label="Interest yield" help="Distributed as taxable interest each year in a brokerage account." value={p.interestYieldPct} min={0} onCommit={(v) => setParam(id, 'interestYieldPct', v ?? DEFAULT_ASSET_CLASS_PARAMS[id].interestYieldPct)} />
68
+ )}
69
+ </div>
70
+ </div>
71
+ )
72
+ })}
73
+ </>
74
+ )
75
+ }
76
+
77
+ export function AssumptionsSection() {
78
+ const { plan, update } = usePlan()
79
+ const a = plan.assumptions
80
+ return (
81
+ <section>
82
+ <div className="card">
83
+ <h2>Assumptions</h2>
84
+ <p className="card-hint">Sensible defaults; every projection is only as good as these. Monte Carlo varies returns and inflation around them.</p>
85
+ <div className="form-grid">
86
+ <PercentField
87
+ label="Inflation"
88
+ help="How fast prices rise each year. Results are shown in today's dollars, so this sets how much future dollars are discounted back."
89
+ learn={LEARN.generalInflation}
90
+ value={a.inflationPct}
91
+ onCommit={(v) => update((d) => void (d.assumptions.inflationPct = v ?? 2.5))}
92
+ />
93
+ <PercentField
94
+ label="Healthcare extra inflation"
95
+ hint="On top of general inflation."
96
+ help="Additional inflation applied to healthcare and Medicare costs."
97
+ learn={LEARN.healthcareInflation}
98
+ value={a.healthcareExtraInflationPct}
99
+ onCommit={(v) => update((d) => void (d.assumptions.healthcareExtraInflationPct = v ?? 3))}
100
+ />
101
+ <PercentField
102
+ label="Default return"
103
+ hint="For accounts without their own."
104
+ help="Assumed long-term nominal return rate for investment accounts."
105
+ learn={LEARN.investmentReturns}
106
+ value={a.defaultReturnPct}
107
+ onCommit={(v) => update((d) => void (d.assumptions.defaultReturnPct = v ?? 5.5))}
108
+ />
109
+ <PercentField
110
+ label="State effective tax (override)"
111
+ help="Leave at 0 to use the modeled per-state brackets for your state(s) of residence. Set a flat rate above 0 to override the model everywhere — useful to correct a state, or to approximate a state that isn't modeled yet."
112
+ hint="0 = use modeled state brackets."
113
+ learn={LEARN.stateTaxOverride}
114
+ source={provenanceSource('state-income-tax')}
115
+ value={a.stateEffectiveTaxPct}
116
+ min={0}
117
+ max={20}
118
+ onCommit={(v) => update((d) => void (d.assumptions.stateEffectiveTaxPct = v ?? 0))}
119
+ />
120
+ <PercentField
121
+ label="Local income tax"
122
+ help="Optional flat city/county income tax. When modeled state brackets are active, this applies to state taxable income; with the state override, it adds to the same flat override base."
123
+ hint="For MD counties, NYC, OH municipalities."
124
+ value={a.localIncomeTaxPct}
125
+ min={0}
126
+ max={10}
127
+ onCommit={(v) => update((d) => void (d.assumptions.localIncomeTaxPct = v ?? 0))}
128
+ />
129
+ <MoneyField
130
+ label="Recent annual MAGI"
131
+ hint="Seeds IRMAA's two-year lookback."
132
+ help="Used to seed the two-year lookback for Medicare premium calculations during your first two years."
133
+ learn={LEARN.recentMagi}
134
+ source={provenanceSource('medicare-irmaa')}
135
+ value={a.recentAnnualMagi}
136
+ onCommit={(v) => update((d) => void (d.assumptions.recentAnnualMagi = v ?? 0))}
137
+ />
138
+ <PercentField
139
+ label="Safe withdrawal rate (SWR)"
140
+ help="The percentage of your portfolio you assume is safe to withdraw in the first year of retirement, adjusted for inflation thereafter. For example, 4% is the common rule-of-thumb lens."
141
+ learn={LEARN.fiNumber}
142
+ value={a.safeWithdrawalRatePct ?? 4}
143
+ min={0.1}
144
+ onCommit={(v) => update((d) => void (d.assumptions.safeWithdrawalRatePct = v ?? 4))}
145
+ />
146
+ <SelectField
147
+ label="Social Security COLA"
148
+ help="How the annual cost-of-living adjustment is modeled: matched to general inflation, or set at a fixed rate."
149
+ learn={LEARN.ssCola}
150
+ value={a.ssCola.mode}
151
+ options={[
152
+ { value: 'matchInflation', label: 'Match inflation' },
153
+ { value: 'fixed', label: 'Fixed rate' },
154
+ ]}
155
+ onCommit={(v) =>
156
+ update((d) => {
157
+ d.assumptions.ssCola = v === 'fixed' ? { mode: 'fixed', annualPct: 2 } : { mode: 'matchInflation' }
158
+ })
159
+ }
160
+ />
161
+ {a.ssCola.mode === 'fixed' ? (
162
+ <PercentField
163
+ label="COLA rate"
164
+ help="The annual cost-of-living adjustment rate."
165
+ learn={LEARN.ssCola}
166
+ value={a.ssCola.annualPct}
167
+ onCommit={(v) => update((d) => void (d.assumptions.ssCola = { mode: 'fixed', annualPct: v ?? 2 }))}
168
+ />
169
+ ) : null}
170
+ <PercentField
171
+ label="Heir tax rate"
172
+ help="Assumed tax rate heirs pay on inherited traditional pre-tax balances, used for the after-tax estate metric."
173
+ learn={LEARN.heirTaxRate}
174
+ value={a.heirTaxRatePct}
175
+ min={0}
176
+ max={50}
177
+ onCommit={(v) => update((d) => void (d.assumptions.heirTaxRatePct = v ?? 25))}
178
+ />
179
+ <CheckboxField
180
+ label="Override heir tax by account class"
181
+ help="Set different heir tax rates for traditional and HSA accounts instead of applying the flat rate to all pre-tax balances. For example, a higher-income heir might face a 32% marginal rate on inherited traditional IRA distributions but only 22% on a modest HSA."
182
+ value={a.heirTaxByClass !== undefined}
183
+ onCommit={(v) =>
184
+ update((d) => void (d.assumptions.heirTaxByClass = v ? { traditional: d.assumptions.heirTaxRatePct, hsa: d.assumptions.heirTaxRatePct } : undefined))
185
+ }
186
+ />
187
+ {a.heirTaxByClass !== undefined ? (
188
+ <>
189
+ <PercentField
190
+ label="Traditional heir tax"
191
+ help="Tax rate heirs pay on inherited traditional IRA/401(k) balances. Overrides the flat heir tax rate for this account class."
192
+ value={a.heirTaxByClass.traditional ?? a.heirTaxRatePct}
193
+ min={0}
194
+ max={50}
195
+ onCommit={(v) =>
196
+ update((d) => {
197
+ if (!d.assumptions.heirTaxByClass) return
198
+ d.assumptions.heirTaxByClass.traditional = v ?? undefined
199
+ })
200
+ }
201
+ />
202
+ <PercentField
203
+ label="HSA heir tax"
204
+ help="Tax rate heirs pay on inherited HSA balances (non-spouse beneficiary). Overrides the flat heir tax rate for this account class."
205
+ value={a.heirTaxByClass.hsa ?? a.heirTaxRatePct}
206
+ min={0}
207
+ max={50}
208
+ onCommit={(v) =>
209
+ update((d) => {
210
+ if (!d.assumptions.heirTaxByClass) return
211
+ d.assumptions.heirTaxByClass.hsa = v ?? undefined
212
+ })
213
+ }
214
+ />
215
+ </>
216
+ ) : null}
217
+ </div>
218
+
219
+ <AssetClassAssumptions />
220
+
221
+ <h3>Social Security trust fund</h3>
222
+ <div className="form-grid">
223
+ <CheckboxField
224
+ label="Model a benefit cut"
225
+ hint="Trustees project depletion around 2034 absent action."
226
+ help="Model a benefit cut if the Social Security trust fund exhausts reserves."
227
+ learn={LEARN.ssTrustFund}
228
+ source={provenanceSource('social-security')}
229
+ value={a.ssHaircut !== null}
230
+ onCommit={(v) => update((d) => void (d.assumptions.ssHaircut = v ? { ...TRUSTEES_DEFAULT_SS_HAIRCUT } : null))}
231
+ />
232
+ {a.ssHaircut ? (
233
+ <>
234
+ <NumberField label="From year" value={a.ssHaircut.fromYear} min={1900} max={2200} onCommit={(v) => update((d) => void (d.assumptions.ssHaircut!.fromYear = Math.round(v ?? TRUSTEES_DEFAULT_SS_HAIRCUT.fromYear)))} />
235
+ <PercentField label="Cut" value={a.ssHaircut.cutPct} min={0} max={100} onCommit={(v) => update((d) => void (d.assumptions.ssHaircut!.cutPct = v ?? TRUSTEES_DEFAULT_SS_HAIRCUT.cutPct))} />
236
+ </>
237
+ ) : null}
238
+ </div>
239
+
240
+ <p className="card-hint">
241
+ Tax brackets, contribution limits, RMD factors, Medicare/IRMAA, and per-state rules come from published{' '}
242
+ {LATEST_PACK_YEAR} figures. <Link to="/disclaimer">See where the numbers come from →</Link>{' '}
243
+ <Link to={`/plan/${plan.id}/assumptions-card`}>See every live assumption on one card →</Link>
244
+ </p>
245
+ <Issues />
246
+
247
+ <LearnAboutScreen route="/plan/:planId/assumptions" limit={10} />
248
+ </div>
249
+ </section>
250
+ )
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Insurance (roadmap V6)
255
+ // ---------------------------------------------------------------------------
256
+