@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,133 @@
1
+ /**
2
+ * In-app replacements for window.confirm / window.prompt / window.alert,
3
+ * built on the shared focus-managed Modal. `useDialogs()` returns
4
+ * promise-based openers so call sites keep their imperative shape:
5
+ *
6
+ * const { confirm, dialogs } = useDialogs()
7
+ * ...
8
+ * if (!(await confirm({ title: 'Delete plan', body: '...' }))) return
9
+ *
10
+ * Render `dialogs` once in the owning component. Escape, backdrop click,
11
+ * and Cancel all resolve to the "did nothing" value (false / null).
12
+ */
13
+
14
+ import { useCallback, useRef, useState, type ReactNode } from 'react'
15
+
16
+ import {
17
+ AlertDialog,
18
+ ChoiceDialog,
19
+ ConfirmDialog,
20
+ PromptDialog,
21
+ type AlertOptions,
22
+ type ChoiceOptions,
23
+ type ConfirmOptions,
24
+ type PromptOptions,
25
+ } from './dialogViews'
26
+
27
+ export type { AlertOptions, ChoiceOption, ChoiceOptions, ConfirmOptions, PromptOptions } from './dialogViews'
28
+
29
+ type ActiveDialog =
30
+ | { kind: 'confirm'; opts: ConfirmOptions; resolve: (ok: boolean) => void }
31
+ | { kind: 'prompt'; opts: PromptOptions; resolve: (value: string | null) => void }
32
+ | { kind: 'alert'; opts: AlertOptions; resolve: () => void }
33
+ | { kind: 'choice'; opts: ChoiceOptions<string>; resolve: (value: string | null) => void }
34
+
35
+ export function useDialogs() {
36
+ const [active, setActive] = useState<ActiveDialog | null>(null)
37
+ // Synchronous mirror of `active`: a second dialog requested while one is
38
+ // open must not clobber it (that would strand the first Promise forever),
39
+ // so re-entrant calls resolve immediately to their "did nothing" value.
40
+ const activeRef = useRef<ActiveDialog | null>(null)
41
+
42
+ const open = useCallback((dialog: ActiveDialog): boolean => {
43
+ if (activeRef.current) return false
44
+ activeRef.current = dialog
45
+ setActive(dialog)
46
+ return true
47
+ }, [])
48
+
49
+ const confirm = useCallback(
50
+ (opts: ConfirmOptions) =>
51
+ new Promise<boolean>((resolve) => {
52
+ if (!open({ kind: 'confirm', opts, resolve })) resolve(false)
53
+ }),
54
+ [open],
55
+ )
56
+ const prompt = useCallback(
57
+ (opts: PromptOptions) =>
58
+ new Promise<string | null>((resolve) => {
59
+ if (!open({ kind: 'prompt', opts, resolve })) resolve(null)
60
+ }),
61
+ [open],
62
+ )
63
+ const alert = useCallback(
64
+ (opts: AlertOptions) =>
65
+ new Promise<void>((resolve) => {
66
+ if (!open({ kind: 'alert', opts, resolve })) resolve()
67
+ }),
68
+ [open],
69
+ )
70
+ const choice = useCallback(
71
+ <T extends string>(opts: ChoiceOptions<T>) =>
72
+ new Promise<T | null>((resolve) => {
73
+ const opened = open({
74
+ kind: 'choice',
75
+ opts: opts as ChoiceOptions<string>,
76
+ resolve: resolve as (value: string | null) => void,
77
+ })
78
+ if (!opened) resolve(null)
79
+ }),
80
+ [open],
81
+ )
82
+
83
+ let dialogs: ReactNode = null
84
+ if (active) {
85
+ const close = () => {
86
+ activeRef.current = null
87
+ setActive(null)
88
+ }
89
+ if (active.kind === 'confirm') {
90
+ dialogs = (
91
+ <ConfirmDialog
92
+ opts={active.opts}
93
+ onResult={(ok) => {
94
+ close()
95
+ active.resolve(ok)
96
+ }}
97
+ />
98
+ )
99
+ } else if (active.kind === 'prompt') {
100
+ dialogs = (
101
+ <PromptDialog
102
+ opts={active.opts}
103
+ onResult={(value) => {
104
+ close()
105
+ active.resolve(value)
106
+ }}
107
+ />
108
+ )
109
+ } else if (active.kind === 'alert') {
110
+ dialogs = (
111
+ <AlertDialog
112
+ opts={active.opts}
113
+ onResult={() => {
114
+ close()
115
+ active.resolve()
116
+ }}
117
+ />
118
+ )
119
+ } else {
120
+ dialogs = (
121
+ <ChoiceDialog
122
+ opts={active.opts}
123
+ onResult={(value) => {
124
+ close()
125
+ active.resolve(value)
126
+ }}
127
+ />
128
+ )
129
+ }
130
+ }
131
+
132
+ return { confirm, prompt, alert, choice, dialogs }
133
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Example library — browsable curated demos (rendered on `/examples`).
3
+ */
4
+
5
+ import { useState } from 'react'
6
+ import { useNavigate } from 'react-router-dom'
7
+
8
+ import { LearnLink } from '../../learn/LearnLink'
9
+ import { useDialogs } from '../dialogs'
10
+ import { EXAMPLE_PLANS, type ExamplePlan } from './registry'
11
+ import { openExampleExisting, openExampleFresh, prepareExampleOpen, saveExampleToMyPlans } from './loadExample'
12
+ import { EXAMPLE_LOAD_FRESH_DESC, EXAMPLE_OPEN_EXISTING_DESC } from './exampleCopy'
13
+ import { loadPlan } from '../../data/planStore'
14
+ import { readLocal, STORAGE_KEYS, writeLocal } from '../../data/localStore'
15
+
16
+ /**
17
+ * Three curated starters shown first so a confused first-timer faces a handful
18
+ * of choices, not the whole 24-card wall (UI/UX round 2, Step 4): a mainstream
19
+ * couple, an approachable single just getting started, and an under-saved /
20
+ * late-start cautionary case. The full library is one click away.
21
+ */
22
+ const FEATURED_EXAMPLE_IDS = ['example-couple', 'early-career-match', 'under-saved-single'] as const
23
+
24
+ function householdFacts(example: ExamplePlan): string {
25
+ const plan = example.build()
26
+ const people = plan.household.people.length
27
+ const filing = plan.household.filingStatus === 'marriedFilingJointly' ? 'Couple' : 'Single'
28
+ return `${filing} · ${plan.household.state} · ${people} ${people === 1 ? 'person' : 'people'}`
29
+ }
30
+
31
+ function ExampleCard({ example, onNotice }: { example: ExamplePlan; onNotice: (msg: string) => void }) {
32
+ const navigate = useNavigate()
33
+ const [busy, setBusy] = useState(false)
34
+ const { choice, dialogs } = useDialogs()
35
+
36
+ const openDemo = async (chosen?: 'open-existing' | 'load-fresh') => {
37
+ setBusy(true)
38
+ try {
39
+ if (chosen === 'open-existing') {
40
+ const r = await openExampleExisting(example.id)
41
+ if (r.ok) navigate(`/plan/${r.planId}/results`)
42
+ else onNotice(r.reason)
43
+ return
44
+ }
45
+ if (chosen === 'load-fresh') {
46
+ const r = await openExampleFresh(example.id)
47
+ if (r.ok) navigate(`/plan/${r.planId}/results`)
48
+ else onNotice(r.reason)
49
+ return
50
+ }
51
+
52
+ const prepared = await prepareExampleOpen(example.id)
53
+ if (!prepared.ok) {
54
+ onNotice(prepared.reason)
55
+ return
56
+ }
57
+ if (prepared.needsChoice) {
58
+ const picked = await choice({
59
+ title: 'Open example',
60
+ body: `"${example.title}" was opened before. Which version would you like?`,
61
+ choices: [
62
+ { value: 'open-existing', label: 'Open my version', description: EXAMPLE_OPEN_EXISTING_DESC },
63
+ { value: 'load-fresh', label: 'Load a fresh copy', description: EXAMPLE_LOAD_FRESH_DESC },
64
+ ],
65
+ })
66
+ if (picked !== null) await openDemo(picked)
67
+ return
68
+ }
69
+ navigate(`/plan/${prepared.planId}/results`)
70
+ } finally {
71
+ setBusy(false)
72
+ }
73
+ }
74
+
75
+ const handleSave = async () => {
76
+ setBusy(true)
77
+ try {
78
+ const prepared = await prepareExampleOpen(example.id)
79
+ if (!prepared.ok) {
80
+ onNotice(prepared.reason)
81
+ return
82
+ }
83
+ let planId = prepared.planId
84
+ if (prepared.needsChoice) {
85
+ const fresh = await openExampleFresh(example.id)
86
+ if (!fresh.ok) {
87
+ onNotice(fresh.reason)
88
+ return
89
+ }
90
+ planId = fresh.planId
91
+ }
92
+ const loaded = await loadPlan(planId)
93
+ if (!loaded.ok) {
94
+ onNotice('Could not load the example to save.')
95
+ return
96
+ }
97
+ const converted = await saveExampleToMyPlans(loaded.plan)
98
+ if (converted.ok) {
99
+ onNotice(`"${example.title}" saved to Your plans.`)
100
+ navigate(`/plan/${converted.plan.id}/results`)
101
+ } else {
102
+ onNotice(converted.issues.join('; '))
103
+ }
104
+ } finally {
105
+ setBusy(false)
106
+ }
107
+ }
108
+
109
+ const learnHook = { slug: example.learnSlug, label: 'Learn about this example' }
110
+
111
+ return (
112
+ <div className="plan-card example-card">
113
+ <span className="plan-card-name">{example.title}</span>
114
+ <span className="plan-card-meta">{householdFacts(example)}</span>
115
+ <p className="example-card-teaches">{example.teaches}</p>
116
+ <span className="plan-card-actions">
117
+ <button type="button" className="btn btn-primary btn-small" disabled={busy} onClick={() => void openDemo()}>
118
+ Open
119
+ </button>
120
+ <button type="button" className="btn btn-secondary btn-small" disabled={busy} onClick={() => void handleSave()}>
121
+ Save to my plans
122
+ </button>
123
+ <LearnLink {...learnHook} variant="button" className="btn btn-ghost btn-small" />
124
+ </span>
125
+ {dialogs}
126
+ </div>
127
+ )
128
+ }
129
+
130
+ const FEATURED = FEATURED_EXAMPLE_IDS.map((id) => EXAMPLE_PLANS.find((e) => e.id === id)).filter(
131
+ (e): e is ExamplePlan => e !== undefined,
132
+ )
133
+ const REST = EXAMPLE_PLANS.filter((e) => !FEATURED_EXAMPLE_IDS.includes(e.id as (typeof FEATURED_EXAMPLE_IDS)[number]))
134
+
135
+ export function ExampleLibrary({
136
+ onNotice,
137
+ headingLevel = 'h2',
138
+ }: {
139
+ onNotice: (msg: string) => void
140
+ headingLevel?: 'h1' | 'h2'
141
+ }) {
142
+ const Heading = headingLevel
143
+ // First-time visitors see the three starters; anyone who expanded the full
144
+ // grid before keeps it open (stored per-device, cleared by "Clear all data").
145
+ const [expanded, setExpanded] = useState(() => readLocal(STORAGE_KEYS.examplesExpanded) === 'true')
146
+
147
+ const toggle = () => {
148
+ setExpanded((prev) => {
149
+ const next = !prev
150
+ writeLocal(STORAGE_KEYS.examplesExpanded, String(next))
151
+ return next
152
+ })
153
+ }
154
+
155
+ return (
156
+ <section className="example-library" aria-labelledby="example-library-heading">
157
+ <Heading id="example-library-heading">Example library</Heading>
158
+ <p className="lede">
159
+ Explore curated households in the full planner. Examples stay out of Your plans until you save one — edit freely
160
+ and refresh without cluttering your own list.
161
+ </p>
162
+ <div className="plan-grid">
163
+ {FEATURED.map((example) => (
164
+ <ExampleCard key={example.id} example={example} onNotice={onNotice} />
165
+ ))}
166
+ </div>
167
+
168
+ <div className="examples-browse-all">
169
+ <button
170
+ type="button"
171
+ className="btn btn-secondary"
172
+ aria-expanded={expanded}
173
+ aria-controls="examples-full-grid"
174
+ onClick={toggle}
175
+ >
176
+ {expanded ? 'Show fewer examples' : `Browse all ${EXAMPLE_PLANS.length} examples`}
177
+ </button>
178
+ </div>
179
+
180
+ {expanded ? (
181
+ <div className="plan-grid" id="examples-full-grid">
182
+ {REST.map((example) => (
183
+ <ExampleCard key={example.id} example={example} onNotice={onNotice} />
184
+ ))}
185
+ </div>
186
+ ) : null}
187
+ </section>
188
+ )
189
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Persistent banner when viewing a library example in the workspace.
3
+ */
4
+
5
+ import { useState } from 'react'
6
+ import { useNavigate } from 'react-router-dom'
7
+
8
+ import { LearnLink } from '../../learn/LearnLink'
9
+ import { useDialogs } from '../dialogs'
10
+ import { usePlan } from '../planContextCore'
11
+ import { getExampleById } from './registry'
12
+ import { saveExampleToMyPlans } from './loadExample'
13
+ import { EXAMPLE_BANNER_PERSISTENCE } from './exampleCopy'
14
+
15
+ export function ExamplePreviewBanner() {
16
+ const { plan, discardPendingSave } = usePlan()
17
+ const navigate = useNavigate()
18
+ const [busy, setBusy] = useState(false)
19
+ const { alert, dialogs } = useDialogs()
20
+
21
+ if (plan.origin !== 'example') return null
22
+
23
+ const example = plan.exampleSourceId ? getExampleById(plan.exampleSourceId) : undefined
24
+ const learnHook = example
25
+ ? { slug: example.learnSlug, label: 'Learn about this example' }
26
+ : null
27
+
28
+ const handleSave = async () => {
29
+ setBusy(true)
30
+ try {
31
+ discardPendingSave()
32
+ const r = await saveExampleToMyPlans(plan)
33
+ if (r.ok) navigate(`/plan/${r.plan.id}/results`)
34
+ else await alert({ title: 'Save example', body: `Could not save: ${r.issues.join('; ')}` })
35
+ } finally {
36
+ setBusy(false)
37
+ }
38
+ }
39
+
40
+ return (
41
+ <div className="callout callout--info example-preview-banner" role="status">
42
+ <p>
43
+ <strong>You&apos;re viewing an example.</strong> {EXAMPLE_BANNER_PERSISTENCE}
44
+ {example?.lookFor ? ` ${example.lookFor}` : ''}
45
+ </p>
46
+ <div className="picker-actions" style={{ margin: 0 }}>
47
+ <button type="button" className="btn btn-primary btn-small" disabled={busy} onClick={() => void handleSave()}>
48
+ Save to my plans
49
+ </button>
50
+ {learnHook ? <LearnLink {...learnHook} variant="button" className="btn btn-secondary btn-small" /> : null}
51
+ </div>
52
+ {dialogs}
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Dedicated example library route — browsable curated demos separate from the
3
+ * planner home so the home stays focused on the user's own plans.
4
+ */
5
+
6
+ import { useState } from 'react'
7
+ import { Link } from 'react-router-dom'
8
+
9
+ import { ExampleLibrary } from './ExampleLibrary'
10
+
11
+ export function ExamplesPage() {
12
+ const [notice, setNotice] = useState<string | null>(null)
13
+
14
+ return (
15
+ <section className="page examples-page">
16
+ <nav className="examples-back" aria-label="Back to planner home">
17
+ <Link to="/">← Your plans</Link>
18
+ </nav>
19
+
20
+ {notice ? <div className="callout callout--info">{notice}</div> : null}
21
+
22
+ <ExampleLibrary onNotice={setNotice} headingLevel="h1" />
23
+ </section>
24
+ )
25
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Open a library example from a Learning Center article.
3
+ */
4
+
5
+ import { useState } from 'react'
6
+ import { useNavigate } from 'react-router-dom'
7
+
8
+ import { useDialogs } from '../dialogs'
9
+ import { getExampleById } from './registry'
10
+ import { openExampleExisting, openExampleFresh, prepareExampleOpen } from './loadExample'
11
+ import { EXAMPLE_LOAD_FRESH_DESC, EXAMPLE_OPEN_EXISTING_DESC } from './exampleCopy'
12
+
13
+ export function OpenExampleButton({ exampleId }: { exampleId: string }) {
14
+ const navigate = useNavigate()
15
+ const [busy, setBusy] = useState(false)
16
+ const { choice, dialogs } = useDialogs()
17
+ const example = getExampleById(exampleId)
18
+ if (!example) return null
19
+
20
+ const open = async (chosen?: 'open-existing' | 'load-fresh') => {
21
+ setBusy(true)
22
+ try {
23
+ if (chosen === 'open-existing') {
24
+ const r = await openExampleExisting(exampleId)
25
+ if (r.ok) navigate(`/plan/${r.planId}/results`)
26
+ return
27
+ }
28
+ if (chosen === 'load-fresh') {
29
+ const r = await openExampleFresh(exampleId)
30
+ if (r.ok) navigate(`/plan/${r.planId}/results`)
31
+ return
32
+ }
33
+ const prepared = await prepareExampleOpen(exampleId)
34
+ if (!prepared.ok) return
35
+ if (prepared.needsChoice) {
36
+ const picked = await choice({
37
+ title: 'Open example',
38
+ body: `"${example.title}" was opened before. Which version would you like?`,
39
+ choices: [
40
+ { value: 'open-existing', label: 'Open my version', description: EXAMPLE_OPEN_EXISTING_DESC },
41
+ { value: 'load-fresh', label: 'Load a fresh copy', description: EXAMPLE_LOAD_FRESH_DESC },
42
+ ],
43
+ })
44
+ if (picked !== null) await open(picked)
45
+ return
46
+ }
47
+ navigate(`/plan/${prepared.planId}/results`)
48
+ } finally {
49
+ setBusy(false)
50
+ }
51
+ }
52
+
53
+ return (
54
+ <>
55
+ <button type="button" className="btn btn-primary" disabled={busy} onClick={() => void open()}>
56
+ Open this example in the planner
57
+ </button>
58
+ {dialogs}
59
+ </>
60
+ )
61
+ }
@@ -0,0 +1,102 @@
1
+ import { createEmptyPlan, parsePlan, type Plan } from '@retiregolden/engine/model/plan'
2
+ import { exampleEntityId, exampleFixedNow, exampleIdFactory } from './buildContext'
3
+
4
+ const EXAMPLE_ID = 'aggressive-saver'
5
+
6
+ export function buildAggressiveSaver(): Plan {
7
+ const p1 = exampleEntityId(EXAMPLE_ID, 'p1')
8
+ const plan = createEmptyPlan({ name: 'Aggressive saver to early retirement', now: exampleFixedNow, newId: exampleIdFactory(EXAMPLE_ID) })
9
+ plan.household = {
10
+ filingStatus: 'single',
11
+ hasQualifyingDependent: false,
12
+ state: 'WA',
13
+ stateMoves: [],
14
+ capitalLossCarryforward: 0,
15
+ people: [
16
+ { id: p1, name: 'Taylor', dob: '1996-01-01', sex: 'average', retirementAge: 45, longevity: { planningAge: 95, source: 'manual' } },
17
+ ],
18
+ }
19
+ plan.accounts = [
20
+ {
21
+ type: 'traditional',
22
+ id: exampleEntityId(EXAMPLE_ID, 'pretax-401k'),
23
+ name: 'Pre-tax 401(k)',
24
+ ownerPersonId: p1,
25
+ annualReturnPct: 8,
26
+ kind: 'employer',
27
+ balance: 100_000,
28
+ annualContribution: 23_000,
29
+ employerMatch: {
30
+ matchPct: 100,
31
+ capPctOfPay: 5,
32
+ },
33
+ },
34
+ {
35
+ type: 'roth',
36
+ id: exampleEntityId(EXAMPLE_ID, 'roth-ira'),
37
+ name: 'Roth IRA',
38
+ ownerPersonId: p1,
39
+ annualReturnPct: 8,
40
+ kind: 'ira',
41
+ balance: 50_000,
42
+ annualContribution: 7_000,
43
+ },
44
+ {
45
+ type: 'taxable',
46
+ id: exampleEntityId(EXAMPLE_ID, 'taxable-brokerage'),
47
+ name: 'Taxable Brokerage',
48
+ ownerPersonId: null,
49
+ annualReturnPct: 8,
50
+ balance: 150_000,
51
+ costBasis: 120_000,
52
+ annualContribution: 0,
53
+ contributionSchedule: [
54
+ {
55
+ annualAmount: 30_000,
56
+ fromAge: 30,
57
+ toAge: 45,
58
+ escalationPct: 5,
59
+ },
60
+ ],
61
+ },
62
+ ]
63
+ plan.incomes = [
64
+ {
65
+ type: 'wages',
66
+ id: exampleEntityId(EXAMPLE_ID, 'wages'),
67
+ personId: p1,
68
+ annualGross: 120_000,
69
+ endAge: null,
70
+ realGrowthPct: 5,
71
+ },
72
+ {
73
+ type: 'socialSecurity',
74
+ id: exampleEntityId(EXAMPLE_ID, 'ss'),
75
+ personId: p1,
76
+ piaMonthly: 3_000,
77
+ earnings: null,
78
+ claimAge: { years: 67, months: 0 },
79
+ },
80
+ ]
81
+ plan.expenses = {
82
+ baseAnnual: 45_000,
83
+ phases: [],
84
+ oneTimeGoals: [],
85
+ healthcare: { pre65MonthlyPremiumPerPerson: 500, applyAcaCredit: true, medicareExtrasMonthlyPerPerson: 200 },
86
+ }
87
+ plan.assumptions = {
88
+ inflationPct: 2.5,
89
+ healthcareExtraInflationPct: 2,
90
+ defaultReturnPct: 6,
91
+ ssCola: { mode: 'matchInflation' },
92
+ ssHaircut: null,
93
+ stateEffectiveTaxPct: 0,
94
+ localIncomeTaxPct: 0,
95
+ recentAnnualMagi: 120_000,
96
+ heirTaxRatePct: 25,
97
+ safeWithdrawalRatePct: 3.5,
98
+ }
99
+ const parsed = parsePlan(plan)
100
+ if (!parsed.ok) throw new Error(`aggressive-saver invalid: ${parsed.issues.join('; ')}`)
101
+ return parsed.plan
102
+ }