@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,56 @@
1
+ import type { RefObject } from 'react'
2
+
3
+ import type { PlanSummary } from '../../data/planStore'
4
+
5
+ type DataAndPrivacyCardProps = {
6
+ plans: PlanSummary[] | null
7
+ fileInput: RefObject<HTMLInputElement | null>
8
+ onExportAll: () => void
9
+ onImportFile: (file: File) => void
10
+ onClearAll: () => void
11
+ }
12
+
13
+ export function DataAndPrivacyCard({
14
+ plans,
15
+ fileInput,
16
+ onExportAll,
17
+ onImportFile,
18
+ onClearAll,
19
+ }: DataAndPrivacyCardProps) {
20
+ return (
21
+ <div className="card home-privacy-card">
22
+ <h2>Your data stays on your device, not on our servers</h2>
23
+ <p className="card-hint">
24
+ RetireGolden has no accounts and no server storage. Everything you enter stays on this device, which means we
25
+ can never see it — and we can never recover it either. To keep a plan safe or move it to another device,
26
+ download a plan backup and take it with you.
27
+ </p>
28
+ <div className="picker-actions" style={{ margin: 0 }}>
29
+ <button type="button" className="btn btn-secondary" onClick={() => void onExportAll()} disabled={!plans || plans.length === 0}>
30
+ Download plan backup
31
+ </button>
32
+ <button type="button" className="btn btn-secondary" onClick={() => fileInput.current?.click()}>
33
+ Import previous backup
34
+ </button>
35
+ <button type="button" className="btn btn-secondary btn-danger" onClick={() => void onClearAll()}>
36
+ Clear all data
37
+ </button>
38
+ </div>
39
+ {/* Reached only through the visible "Import previous backup" button;
40
+ keeping it out of the tab order stops an invisible tab stop. */}
41
+ <input
42
+ ref={fileInput}
43
+ type="file"
44
+ accept="application/json"
45
+ className="sr-only"
46
+ tabIndex={-1}
47
+ aria-hidden="true"
48
+ onChange={(e) => {
49
+ const f = e.target.files?.[0]
50
+ if (f) void onImportFile(f)
51
+ e.target.value = ''
52
+ }}
53
+ />
54
+ </div>
55
+ )
56
+ }
@@ -0,0 +1,46 @@
1
+ import { Link } from 'react-router-dom'
2
+
3
+ import { createEmptyPlan } from '@retiregolden/engine/model/plan'
4
+
5
+ type GettingStartedPathsProps = {
6
+ onCreatePlan: (plan: ReturnType<typeof createEmptyPlan>) => void
7
+ }
8
+
9
+ export function GettingStartedPaths({ onCreatePlan }: GettingStartedPathsProps) {
10
+ return (
11
+ <section className="home-paths" aria-labelledby="getting-started-paths-heading">
12
+ <h2 id="getting-started-paths-heading">Getting started</h2>
13
+ <div className="home-paths-grid plan-grid">
14
+ <Link to="/learn" className="home-path-card plan-card">
15
+ <span className="home-path-card-title">Learn the basics</span>
16
+ <span className="home-path-card-desc">
17
+ Read short guides on privacy, the planner sections, and what RetireGolden models.
18
+ </span>
19
+ </Link>
20
+ <Link to="/examples" className="home-path-card plan-card">
21
+ <span className="home-path-card-title">Try an example</span>
22
+ <span className="home-path-card-desc">
23
+ Open a curated household with realistic numbers — explore freely without saving anything yet.
24
+ </span>
25
+ </Link>
26
+ <button
27
+ type="button"
28
+ className="home-path-card plan-card"
29
+ onClick={() => void onCreatePlan(createEmptyPlan())}
30
+ >
31
+ <span className="home-path-card-title">Build your own plan</span>
32
+ <span className="home-path-card-desc">
33
+ Start from a blank slate and enter your household, accounts, and spending.
34
+ </span>
35
+ </button>
36
+ <Link to="/import" className="home-path-card plan-card">
37
+ <span className="home-path-card-title">Import from a file</span>
38
+ <span className="home-path-card-desc">
39
+ Seed a plan from a broker CSV, a ProjectionLab export, a spreadsheet, or last year&apos;s tax
40
+ return — parsed on this device, never uploaded.
41
+ </span>
42
+ </Link>
43
+ </div>
44
+ </section>
45
+ )
46
+ }
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ type GettingStartedReopenerProps = {
4
+ expanded: boolean
5
+ onToggle: () => void
6
+ panelId: string
7
+ children: ReactNode
8
+ }
9
+
10
+ export function GettingStartedReopener({ expanded, onToggle, panelId, children }: GettingStartedReopenerProps) {
11
+ return (
12
+ <div className="home-getting-started-reopener">
13
+ <button
14
+ type="button"
15
+ className="home-reopener-btn"
16
+ aria-expanded={expanded}
17
+ aria-controls={panelId}
18
+ onClick={onToggle}
19
+ >
20
+ <span className="home-reopener-icon" aria-hidden="true">
21
+ {expanded ? '▾' : '▸'}
22
+ </span>
23
+ New here? Getting started
24
+ </button>
25
+ {expanded ? (
26
+ <div id={panelId} className="home-getting-started-panel">
27
+ {children}
28
+ </div>
29
+ ) : null}
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,22 @@
1
+ import { LearnLink } from '../../learn/LearnLink'
2
+ import { getArticle } from '../../learn/learningRegistry'
3
+ import { START_HERE_SLUGS } from './startHereSlugs'
4
+
5
+ export function StartHereLinks() {
6
+ return (
7
+ <section className="home-start-here" aria-labelledby="start-here-heading">
8
+ <h2 id="start-here-heading">Start here</h2>
9
+ <ul className="home-start-here-list">
10
+ {START_HERE_SLUGS.map((slug) => {
11
+ const article = getArticle(slug)
12
+ if (!article) return null
13
+ return (
14
+ <li key={slug}>
15
+ <LearnLink slug={slug} label={article.title} variant="inline" />
16
+ </li>
17
+ )
18
+ })}
19
+ </ul>
20
+ </section>
21
+ )
22
+ }
@@ -0,0 +1,39 @@
1
+ type WelcomeHeroProps = {
2
+ onTryExample: () => void
3
+ onDismiss?: () => void
4
+ showDismiss?: boolean
5
+ headingLevel?: 'h1' | 'h2'
6
+ }
7
+
8
+ export function WelcomeHero({
9
+ onTryExample,
10
+ onDismiss,
11
+ showDismiss = false,
12
+ headingLevel = 'h1',
13
+ }: WelcomeHeroProps) {
14
+ const Heading = headingLevel
15
+ return (
16
+ <section className="home-hero" aria-labelledby="home-hero-heading">
17
+ <Heading id="home-hero-heading">Plan your retirement — privately, in your browser</Heading>
18
+ <p className="home-hero-subhead lede">
19
+ Model your household, stress-test taxes and withdrawals, and explore scenarios — with no account and no data
20
+ leaving this device.
21
+ </p>
22
+ <ul className="trust-chips" aria-label="Why RetireGolden">
23
+ <li>No accounts</li>
24
+ <li>Nothing leaves your device</li>
25
+ <li>Educational, with sources</li>
26
+ </ul>
27
+ <div className="home-hero-cta">
28
+ <button type="button" className="btn btn-primary" onClick={onTryExample}>
29
+ Try an example
30
+ </button>
31
+ {showDismiss && onDismiss ? (
32
+ <button type="button" className="btn btn-ghost btn-small" onClick={onDismiss}>
33
+ Hide getting started
34
+ </button>
35
+ ) : null}
36
+ </div>
37
+ </section>
38
+ )
39
+ }
@@ -0,0 +1,72 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ import type { PlanSummary } from '../../data/planStore'
4
+
5
+ function fmtUpdated(iso: string): string {
6
+ const d = new Date(iso)
7
+ return Number.isNaN(d.getTime()) ? '' : `Updated ${d.toLocaleDateString()} ${d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`
8
+ }
9
+
10
+ type YourPlansProps = {
11
+ plans: PlanSummary[] | null
12
+ headingLevel?: 'h1' | 'h2'
13
+ actions?: ReactNode
14
+ onOpenPlan: (id: string) => void
15
+ onDuplicate: (s: PlanSummary) => void
16
+ onDelete: (s: PlanSummary) => void
17
+ }
18
+
19
+ export function YourPlans({ plans, headingLevel = 'h2', actions, onOpenPlan, onDuplicate, onDelete }: YourPlansProps) {
20
+ const Heading = headingLevel
21
+
22
+ if (plans === null) {
23
+ return <div className="skeleton" style={{ height: '8rem' }} aria-label="Loading plans" />
24
+ }
25
+
26
+ if (plans.length === 0) return null
27
+
28
+ return (
29
+ <section className="home-your-plans" aria-labelledby="your-plans-heading">
30
+ <div className="home-your-plans-head">
31
+ <Heading id="your-plans-heading">Your plans</Heading>
32
+ {actions ? <div className="picker-actions home-returning-actions">{actions}</div> : null}
33
+ </div>
34
+ <div className="plan-grid">
35
+ {plans.map((s) => (
36
+ /* The open action is a real button stretched over the card via CSS
37
+ (::after), so the whole card stays clickable without nesting the
38
+ Duplicate/Delete buttons inside another interactive element. */
39
+ <div key={s.id} className="plan-card">
40
+ <button
41
+ type="button"
42
+ className="plan-card-open"
43
+ aria-label={`Open plan ${s.name}`}
44
+ onClick={() => onOpenPlan(s.id)}
45
+ >
46
+ <span className="plan-card-name">{s.name}</span>
47
+ </button>
48
+ <span className="plan-card-meta">{fmtUpdated(s.updatedAtIso)}</span>
49
+ <span className="plan-card-actions">
50
+ <button
51
+ type="button"
52
+ className="btn-ghost"
53
+ aria-label={`Duplicate plan ${s.name}`}
54
+ onClick={() => void onDuplicate(s)}
55
+ >
56
+ Duplicate
57
+ </button>
58
+ <button
59
+ type="button"
60
+ className="btn-ghost btn-ghost-danger"
61
+ aria-label={`Delete plan ${s.name}`}
62
+ onClick={() => void onDelete(s)}
63
+ >
64
+ Delete
65
+ </button>
66
+ </span>
67
+ </div>
68
+ ))}
69
+ </div>
70
+ </section>
71
+ )
72
+ }
@@ -0,0 +1,22 @@
1
+ import type { parseV2Backup } from '../../data/v2Backup'
2
+
3
+ type ImportFailure = Extract<ReturnType<typeof parseV2Backup>, { ok: false }>['reason']
4
+
5
+ /**
6
+ * Human import-failure copy with a next step (UI/UX round 2, Step 6) — never the
7
+ * raw enum reason with underscores, which leaked schema jargon at the user.
8
+ */
9
+ export function importErrorMessage(reason: ImportFailure | string): string {
10
+ switch (reason) {
11
+ case 'too_large':
12
+ return 'That file is too large to be a RetireGolden backup. Pick the JSON file you downloaded from Data & privacy → Download backup.'
13
+ case 'unsupported_version':
14
+ return 'That backup was made by a different version of RetireGolden, so it can’t be imported here.'
15
+ case 'no_valid_plans':
16
+ return 'That backup didn’t contain any readable plans. Export a fresh one from Data & privacy → Download backup.'
17
+ case 'not_json':
18
+ case 'wrong_kind':
19
+ default:
20
+ return 'That file isn’t a RetireGolden backup. Export one from Data & privacy → Download backup, then import that file.'
21
+ }
22
+ }
@@ -0,0 +1,7 @@
1
+ /** Curated beginner articles surfaced on the planner home "Start here" row. */
2
+ export const START_HERE_SLUGS = [
3
+ 'about-retiregolden',
4
+ 'privacy-what-stays-in-your-browser',
5
+ 'planner-overview',
6
+ 'what-retiregolden-models',
7
+ ] as const
@@ -0,0 +1,190 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+
4
+ import {
5
+ clearAllPlans,
6
+ duplicatePlan,
7
+ listUserPlanSummaries,
8
+ loadPlan,
9
+ savePlan,
10
+ deletePlan,
11
+ type PlanSummary,
12
+ } from '../../data/planStore'
13
+ import { normalizePlansForImport, parseV2Backup, serializeV2Backup } from '../../data/v2Backup'
14
+ import { type Plan } from '@retiregolden/engine/model/plan'
15
+ import { useDialogs } from '../dialogs'
16
+ import { importErrorMessage } from './importErrorMessage'
17
+
18
+ export function useHomeData() {
19
+ const navigate = useNavigate()
20
+ const [plans, setPlans] = useState<PlanSummary[] | null>(null)
21
+ const [notice, setNotice] = useState<string | null>(null)
22
+ // A just-deleted plan, held in memory for a brief undo window. The delete is
23
+ // already committed to storage, so an expired or navigated-away toast never
24
+ // leaves a plan half-deleted; Undo simply re-saves the in-memory copy.
25
+ const [undoPlan, setUndoPlan] = useState<Plan | null>(null)
26
+ const undoTimer = useRef<number | null>(null)
27
+ const fileInput = useRef<HTMLInputElement>(null)
28
+ const { confirm, prompt, dialogs } = useDialogs()
29
+
30
+ const refresh = useCallback(() => {
31
+ void listUserPlanSummaries().then(setPlans)
32
+ }, [])
33
+
34
+ const clearUndoTimer = () => {
35
+ if (undoTimer.current !== null) {
36
+ clearTimeout(undoTimer.current)
37
+ undoTimer.current = null
38
+ }
39
+ }
40
+
41
+ useEffect(() => {
42
+ refresh()
43
+ }, [refresh])
44
+
45
+ useEffect(() => () => clearUndoTimer(), [])
46
+
47
+ const openPlan = (id: string) => navigate(`/plan/${id}`)
48
+
49
+ const createAndOpen = async (plan: Plan) => {
50
+ const r = await savePlan(plan)
51
+ if (r.ok) openPlan(r.plan.id)
52
+ else setNotice(`Could not save the new plan: ${r.issues.join('; ')}`)
53
+ }
54
+
55
+ const handleExportAll = async () => {
56
+ const summaries = await listUserPlanSummaries()
57
+ const loaded: Plan[] = []
58
+ for (const s of summaries) {
59
+ const r = await loadPlan(s.id)
60
+ if (r.ok) loaded.push(r.plan)
61
+ }
62
+ const blob = new Blob([serializeV2Backup(loaded)], { type: 'application/json' })
63
+ const a = document.createElement('a')
64
+ a.href = URL.createObjectURL(blob)
65
+ a.download = `retiregolden-backup-${new Date().toISOString().slice(0, 10)}.json`
66
+ a.click()
67
+ URL.revokeObjectURL(a.href)
68
+ }
69
+
70
+ const handleImportFile = async (file: File) => {
71
+ const r = parseV2Backup(await file.text())
72
+ if (!r.ok) {
73
+ setNotice(importErrorMessage(r.reason))
74
+ return
75
+ }
76
+ const normalized = await normalizePlansForImport(r.plans)
77
+ for (const p of normalized) await savePlan(p)
78
+ setNotice(
79
+ `Imported ${normalized.length} plan${normalized.length === 1 ? '' : 's'}.` +
80
+ (r.warnings.length > 0 ? ` Skipped: ${r.warnings.join('; ')}` : ''),
81
+ )
82
+ refresh()
83
+ }
84
+
85
+ const handleDuplicate = async (s: PlanSummary) => {
86
+ const name = await prompt({
87
+ title: 'Duplicate plan',
88
+ label: 'Name for the duplicated plan',
89
+ defaultValue: `Copy of ${s.name}`,
90
+ confirmLabel: 'Duplicate',
91
+ })
92
+ if (name === null) return
93
+ const r = await duplicatePlan(s.id, { name })
94
+ if (r.ok) {
95
+ setNotice(`Duplicated "${s.name}" as "${r.plan.name}".`)
96
+ refresh()
97
+ } else {
98
+ setNotice(`Could not duplicate "${s.name}": ${r.issues.join('; ')}`)
99
+ }
100
+ }
101
+
102
+ const handleDelete = async (s: PlanSummary) => {
103
+ const ok = await confirm({
104
+ title: 'Delete plan',
105
+ body: `Delete "${s.name}"? You'll have a few seconds to undo.`,
106
+ confirmLabel: 'Delete plan',
107
+ danger: true,
108
+ })
109
+ if (!ok) return
110
+ const loaded = await loadPlan(s.id)
111
+ await deletePlan(s.id)
112
+ refresh()
113
+ if (loaded.ok) {
114
+ clearUndoTimer()
115
+ setUndoPlan(loaded.plan)
116
+ undoTimer.current = window.setTimeout(() => setUndoPlan(null), 5000)
117
+ }
118
+ }
119
+
120
+ const undoDelete = async () => {
121
+ if (!undoPlan) return
122
+ clearUndoTimer()
123
+ const restored = undoPlan
124
+ // Keep the in-memory plan (and the toast) until the restore actually
125
+ // lands — if the save fails, the user must not lose both the plan and
126
+ // the affordance at once.
127
+ try {
128
+ const r = await savePlan(restored)
129
+ if (r.ok) {
130
+ setUndoPlan(null)
131
+ refresh()
132
+ } else {
133
+ setNotice(`Could not restore "${restored.name}": ${r.issues.join('; ')}`)
134
+ }
135
+ } catch {
136
+ setNotice(`Could not restore "${restored.name}" — storage is unavailable in this browser right now.`)
137
+ }
138
+ }
139
+
140
+ const dismissUndo = () => {
141
+ clearUndoTimer()
142
+ setUndoPlan(null)
143
+ }
144
+
145
+ const handleClearAll = async () => {
146
+ const ok = await confirm({
147
+ title: 'Clear all data',
148
+ body:
149
+ 'This erases ALL RetireGolden data from this browser — every plan, plus Social Security and ' +
150
+ 'life-expectancy entries. It cannot be undone. Download a plan backup first if you want to keep anything.',
151
+ confirmLabel: 'Erase everything',
152
+ danger: true,
153
+ typedConfirmation: 'delete',
154
+ extraAction: { label: 'Download backup', onClick: () => void handleExportAll() },
155
+ })
156
+ if (!ok) return
157
+ // A pending delete-undo would let "Undo" resurrect a plan after the
158
+ // erasure — drop it before clearing so "erases ALL data" stays true.
159
+ dismissUndo()
160
+ await clearAllPlans()
161
+ try {
162
+ for (const key of Object.keys(localStorage)) {
163
+ if (key.startsWith('retiregolden.')) localStorage.removeItem(key)
164
+ }
165
+ } catch {
166
+ /* localStorage unavailable — IndexedDB is already cleared */
167
+ }
168
+ setNotice('All RetireGolden data has been erased from this browser.')
169
+ refresh()
170
+ }
171
+
172
+ return {
173
+ plans,
174
+ notice,
175
+ setNotice,
176
+ undoPlan,
177
+ undoDelete,
178
+ dismissUndo,
179
+ fileInput,
180
+ refresh,
181
+ createAndOpen,
182
+ handleExportAll,
183
+ handleImportFile,
184
+ handleDuplicate,
185
+ handleDelete,
186
+ handleClearAll,
187
+ openPlan,
188
+ dialogs,
189
+ }
190
+ }
@@ -0,0 +1,47 @@
1
+ import { useCallback, useState } from 'react'
2
+
3
+ import { readLocal, STORAGE_KEYS, writeLocal } from '../../data/localStore'
4
+ import type { PlanSummary } from '../../data/planStore'
5
+
6
+ export const WELCOME_DISMISSED_KEY = STORAGE_KEYS.homeWelcomeDismissed
7
+
8
+ export type HomeMode = 'first-run' | 'returning'
9
+
10
+ function readWelcomeDismissed(): boolean {
11
+ if (typeof window === 'undefined') return true
12
+ return readLocal(WELCOME_DISMISSED_KEY) !== 'false'
13
+ }
14
+
15
+ function writeWelcomeDismissed(dismissed: boolean): void {
16
+ if (typeof window === 'undefined') return
17
+ writeLocal(WELCOME_DISMISSED_KEY, dismissed ? 'true' : 'false')
18
+ }
19
+
20
+ export function deriveHomeMode(plans: PlanSummary[] | null): HomeMode {
21
+ if (plans !== null && plans.length === 0) return 'first-run'
22
+ return 'returning'
23
+ }
24
+
25
+ export function isWelcomeExpanded(mode: HomeMode, welcomeDismissed: boolean): boolean {
26
+ if (mode === 'first-run') return true
27
+ return !welcomeDismissed
28
+ }
29
+
30
+ export function useHomeMode(plans: PlanSummary[] | null) {
31
+ const [welcomeDismissed, setWelcomeDismissed] = useState(readWelcomeDismissed)
32
+
33
+ const mode = deriveHomeMode(plans)
34
+ const welcomeExpanded = isWelcomeExpanded(mode, welcomeDismissed)
35
+
36
+ const dismissWelcome = useCallback(() => {
37
+ setWelcomeDismissed(true)
38
+ writeWelcomeDismissed(true)
39
+ }, [])
40
+
41
+ const showWelcome = useCallback(() => {
42
+ setWelcomeDismissed(false)
43
+ writeWelcomeDismissed(false)
44
+ }, [])
45
+
46
+ return { mode, welcomeExpanded, dismissWelcome, showWelcome }
47
+ }
@@ -0,0 +1,22 @@
1
+ /** Pure plan mutations for household edits (kept out of the component file so
2
+ * they're testable and don't trip react-refresh's only-export-components rule). */
3
+
4
+ import type { Plan } from '@retiregolden/engine/model/plan'
5
+
6
+ /**
7
+ * Remove a partner and re-home everything that referenced them so the plan stays
8
+ * valid: accounts move to the primary, the removed person's incomes and policies
9
+ * drop, and any permanent-life beneficiary pointing at them falls back to the
10
+ * estate. Pure mutator (works on an Immer draft or a plain Plan).
11
+ */
12
+ export function removePartner(d: Plan, removedId: string) {
13
+ d.household.people = d.household.people.filter((p) => p.id !== removedId)
14
+ d.household.filingStatus = 'single'
15
+ const primaryId = d.household.people[0]!.id
16
+ d.accounts = d.accounts.map((a) => (a.ownerPersonId === removedId ? { ...a, ownerPersonId: primaryId } : a))
17
+ d.incomes = d.incomes.filter((s) => !('personId' in s) || s.personId !== removedId)
18
+ d.insurance = d.insurance
19
+ .filter((p) => (p.kind === 'ltc' ? p.owner : p.insured) !== removedId)
20
+ .map((p) => (p.kind === 'permanentLife' && p.beneficiary === removedId ? { ...p, beneficiary: 'estate' as const } : p))
21
+ d.careEvents = d.careEvents.filter((c) => c.personId !== removedId)
22
+ }