@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,57 @@
1
+ /**
2
+ * Article page (/learn/:slug).
3
+ *
4
+ * Routes a slug to one of three states: a finished article (rendered through
5
+ * ArticleShell), an intentional "planned" placeholder for stubs, or a not-found
6
+ * state for unknown slugs. Stubs and unknown slugs never render an empty page.
7
+ */
8
+
9
+ import { Link, useParams } from 'react-router-dom'
10
+ import { getArticle, getCategory, isReadable, type LearningArticle } from './learningRegistry'
11
+ import { ArticleShell, BackLink } from './components/ArticleShell'
12
+
13
+ function ArticleNotFound() {
14
+ return (
15
+ <article className="page learn-article">
16
+ <BackLink />
17
+ <h1>Article not found</h1>
18
+ <p className="lede">We couldn’t find that article. It may have moved, or the link may be out of date.</p>
19
+ <p>
20
+ <Link to="/learn">Browse the Learning Center</Link> to find what you’re looking for.
21
+ </p>
22
+ </article>
23
+ )
24
+ }
25
+
26
+ function PlannedArticle({ article }: { article: LearningArticle }) {
27
+ const category = getCategory(article.category)
28
+ return (
29
+ <article className="page learn-article">
30
+ <BackLink />
31
+ <div className="learn-card-head">
32
+ {category && <span className="learn-card-cat">{category.label}</span>}
33
+ <span className="learn-badge learn-badge--planned">Planned</span>
34
+ </div>
35
+ <h1>{article.title}</h1>
36
+ <p className="lede">{article.description}</p>
37
+ <div className="learn-callout learn-callout--note">
38
+ <p>
39
+ This article is planned but not written yet — it’s on the V9 content roadmap. In the meantime, explore the
40
+ planner or browse other topics.
41
+ </p>
42
+ </div>
43
+ <p>
44
+ <Link to="/learn">Back to the Learning Center</Link>
45
+ </p>
46
+ </article>
47
+ )
48
+ }
49
+
50
+ export function ArticlePage() {
51
+ const { slug } = useParams()
52
+ const article = slug ? getArticle(slug) : undefined
53
+
54
+ if (!article) return <ArticleNotFound />
55
+ if (!isReadable(article)) return <PlannedArticle article={article} />
56
+ return <ArticleShell article={article} />
57
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Glossary index (/learn/glossary).
3
+ *
4
+ * A flat, alphabetized definition list. Each term has a stable anchor id so
5
+ * tooltips and articles can deep-link to a specific definition.
6
+ */
7
+
8
+ import { Link } from 'react-router-dom'
9
+ import { GLOSSARY_TERMS } from './glossary'
10
+
11
+ export function GlossaryPage() {
12
+ return (
13
+ <article className="page learn-glossary">
14
+ <Link to="/learn" className="learn-back">
15
+ ← Learning Center
16
+ </Link>
17
+ <h1>Glossary</h1>
18
+ <p className="lede">Plain-language definitions of the terms and acronyms used across RetireGolden.</p>
19
+
20
+ <dl className="learn-glossary-list">
21
+ {GLOSSARY_TERMS.map((t) => (
22
+ <div key={t.id} className="learn-glossary-item" id={t.id}>
23
+ <dt className="learn-glossary-term">
24
+ {t.term}
25
+ {t.expansion && <span className="learn-glossary-expansion"> — {t.expansion}</span>}
26
+ </dt>
27
+ <dd className="learn-glossary-def">{t.definition}</dd>
28
+ </div>
29
+ ))}
30
+ </dl>
31
+ </article>
32
+ )
33
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * LearnAboutScreen (V9 PR3) — the standard contextual learning cluster for a
3
+ * planner screen.
4
+ *
5
+ * Given a planner route pattern (e.g. `/plan/:planId/optimize`), it lists the
6
+ * readable Learning Center articles that relate to that screen, each via
7
+ * {@link LearnLink} so they carry a return path. The list is derived from
8
+ * article metadata, so it stays in sync with the registry and renders nothing
9
+ * when no ready article relates yet — every screen can host it safely.
10
+ */
11
+
12
+ import { articlesForRoute, byListingOrder } from './learningRegistry'
13
+ import { LearnLink } from './LearnLink'
14
+
15
+ /** Keep the cluster focused — the doc's guidance is that 3-5 links is plenty. */
16
+ const MAX_LINKS = 5
17
+
18
+ export function LearnAboutScreen({
19
+ route,
20
+ title = 'Learn about this screen',
21
+ limit = MAX_LINKS,
22
+ }: {
23
+ route: string
24
+ title?: string
25
+ limit?: number
26
+ }) {
27
+ const articles = articlesForRoute(route).slice().sort(byListingOrder).slice(0, limit)
28
+ if (articles.length === 0) return null
29
+ return (
30
+ <aside className="learn-screen" aria-label={title}>
31
+ <span className="learn-screen-title">{title}</span>
32
+ <ul className="learn-screen-list">
33
+ {articles.map((a) => (
34
+ <li key={a.slug}>
35
+ <LearnLink slug={a.slug} label={a.title} />
36
+ </li>
37
+ ))}
38
+ </ul>
39
+ </aside>
40
+ )
41
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared presentational cards for the Learning Center.
3
+ *
4
+ * A readable article renders as a clickable card; a planned (stub) article
5
+ * renders as a non-interactive "Planned" card so users are never routed to an
6
+ * empty page (spec §7.3).
7
+ */
8
+
9
+ import { Link } from 'react-router-dom'
10
+ import { getCategory, isReadable, type LearningArticle } from './learningRegistry'
11
+
12
+ export function ArticleCard({ article }: { article: LearningArticle }) {
13
+ const readable = isReadable(article)
14
+ const category = getCategory(article.category)
15
+
16
+ const inner = (
17
+ <>
18
+ <div className="learn-card-head">
19
+ {category && <span className="learn-card-cat">{category.label}</span>}
20
+ {readable ? null : <span className="learn-badge learn-badge--planned">Planned</span>}
21
+ </div>
22
+ <span className="learn-card-title">{article.title}</span>
23
+ <span className="learn-card-desc">{article.description}</span>
24
+ </>
25
+ )
26
+
27
+ if (readable) {
28
+ return (
29
+ <Link to={`/learn/${article.slug}`} className="learn-card learn-card--link">
30
+ {inner}
31
+ </Link>
32
+ )
33
+ }
34
+
35
+ return (
36
+ <div className="learn-card learn-card--planned">
37
+ {inner}
38
+ <span className="sr-only">Planned article, not yet available.</span>
39
+ </div>
40
+ )
41
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * LearnLink (V9 PR3) — the single, consistent way the planner links out to the
3
+ * Learning Center.
4
+ *
5
+ * Every contextual "Learn more" in the app should go through this component so
6
+ * links behave the same everywhere: the slug is validated against the registry,
7
+ * an optional in-article anchor is supported, the look is consistent per
8
+ * variant, and the originating planner path is preserved so the article's back
9
+ * link can return the reader to the screen they came from instead of the
10
+ * Learning Center home.
11
+ *
12
+ * The slug is intentionally typed `string`: most call sites pass a hook from
13
+ * `planner/learnLinks.ts` (validated by a broken-slug test) or a registry slug
14
+ * (validated by the registry-integrity tests). An unknown slug renders nothing
15
+ * and logs in dev rather than producing a dead link.
16
+ */
17
+
18
+ import { Link, useLocation } from 'react-router-dom'
19
+ import { getArticle } from './learningRegistry'
20
+
21
+ export type LearnHook = {
22
+ /** Article slug in the Learning Center registry. */
23
+ slug: string
24
+ /** Link text. Defaults to "Learn more". */
25
+ label?: string
26
+ /** Optional anchor (without `#`) to jump to within the article. */
27
+ anchor?: string
28
+ }
29
+
30
+ type LearnLinkProps = LearnHook & {
31
+ /**
32
+ * - `inline`: a quiet inline link (default), e.g. inside a screen cluster.
33
+ * - `tip`: sized and spaced to sit at the bottom of a help bubble.
34
+ * - `button`: a compact pill for page headers/callouts.
35
+ */
36
+ variant?: 'inline' | 'tip' | 'button'
37
+ className?: string
38
+ }
39
+
40
+ /** Title-case a single planner route segment into a human label for the back link. */
41
+ const SEGMENT_LABELS: Record<string, string> = {
42
+ household: 'Household',
43
+ 'social-security': 'Social Security',
44
+ accounts: 'Accounts',
45
+ insurance: 'Insurance',
46
+ income: 'Income',
47
+ spending: 'Spending',
48
+ strategy: 'Strategy',
49
+ assumptions: 'Assumptions',
50
+ 'assumptions-card': 'Your assumptions',
51
+ insights: 'Insights',
52
+ 'social-security-analysis': 'Social Security Optimizer',
53
+ results: 'Results',
54
+ 'monte-carlo': 'Monte Carlo',
55
+ scenarios: 'Scenarios',
56
+ survivor: 'Survivor transition',
57
+ optimize: 'Roth & Tax Optimizer',
58
+ report: 'Report',
59
+ }
60
+
61
+ /** A short, human label for where a return path points (planner screen name). */
62
+ function originLabel(pathname: string): string | undefined {
63
+ const segment = pathname.match(/\/plan\/[^/]+\/([^/]+)/)?.[1]
64
+ return segment ? SEGMENT_LABELS[segment] : undefined
65
+ }
66
+
67
+ export function LearnLink({ slug, label = 'Learn more', anchor, variant = 'inline', className }: LearnLinkProps) {
68
+ const location = useLocation()
69
+ const article = getArticle(slug)
70
+ if (!article) {
71
+ if (import.meta.env.DEV) console.error(`LearnLink: unknown article slug "${slug}"`)
72
+ return null
73
+ }
74
+
75
+ const to = anchor ? `/learn/${slug}#${anchor}` : `/learn/${slug}`
76
+ // Preserve an internal-only return path so the article can offer "← Back to …".
77
+ const from = location.pathname + location.search
78
+ const safeFrom = from.startsWith('/') && !from.startsWith('//') ? from : undefined
79
+ const classes = ['learn-link', `learn-link--${variant}`, className].filter(Boolean).join(' ')
80
+
81
+ return (
82
+ <Link
83
+ to={to}
84
+ className={classes}
85
+ state={safeFrom ? { learnFrom: safeFrom, learnFromLabel: originLabel(location.pathname) } : undefined}
86
+ >
87
+ {label}
88
+ <span aria-hidden="true"> →</span>
89
+ </Link>
90
+ )
91
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Learning Center home (/learn): a fast index, not long copy.
3
+ *
4
+ * Search, featured high-value topics, a browse-by-category grid, and links to
5
+ * the glossary and sources.
6
+ */
7
+
8
+ import { useId, useState } from 'react'
9
+ import { Link } from 'react-router-dom'
10
+ import {
11
+ articlesInCategory,
12
+ byListingOrder,
13
+ categorySummaries,
14
+ featuredArticles,
15
+ searchArticles,
16
+ } from './learningRegistry'
17
+ import { ArticleCard } from './LearnCards'
18
+
19
+ export function LearningCenterPage() {
20
+ const [query, setQuery] = useState('')
21
+ const searchInputId = useId()
22
+ const trimmed = query.trim()
23
+ const results = trimmed ? searchArticles(trimmed) : null
24
+
25
+ const featured = featuredArticles()
26
+
27
+ return (
28
+ <article className="page learn-home">
29
+ <h1>Learning Center</h1>
30
+ <p className="lede">
31
+ Plain-language explanations of the ideas behind your plan — taxes, Social Security, withdrawals, risk, and
32
+ more. Read a topic, then come back to the planner with a clearer picture.
33
+ </p>
34
+
35
+ <div className="learn-search">
36
+ <label htmlFor={searchInputId} className="sr-only">
37
+ Search the Learning Center
38
+ </label>
39
+ <input
40
+ id={searchInputId}
41
+ type="search"
42
+ className="learn-search-input"
43
+ placeholder="Search topics, e.g. Roth, IRMAA, sequence risk"
44
+ value={query}
45
+ onChange={(e) => setQuery(e.target.value)}
46
+ />
47
+ </div>
48
+
49
+ {results ? (
50
+ <section className="learn-section" aria-label="Search results">
51
+ <h2 className="learn-section-title">
52
+ {results.length} result{results.length === 1 ? '' : 's'} for “{trimmed}”
53
+ </h2>
54
+ {results.length === 0 ? (
55
+ <p className="muted">No matching topics yet. Try a broader word, or browse the categories below.</p>
56
+ ) : (
57
+ <div className="learn-card-grid">
58
+ {results.map((a) => (
59
+ <ArticleCard key={a.slug} article={a} />
60
+ ))}
61
+ </div>
62
+ )}
63
+ </section>
64
+ ) : (
65
+ <>
66
+ {featured.length > 0 && (
67
+ <section className="learn-section" aria-label="Featured topics">
68
+ <h2 className="learn-section-title">Featured topics</h2>
69
+ <div className="learn-card-grid">
70
+ {featured.map((a) => (
71
+ <ArticleCard key={a.slug} article={a} />
72
+ ))}
73
+ </div>
74
+ </section>
75
+ )}
76
+
77
+ <section className="learn-section" aria-label="Browse by category">
78
+ <h2 className="learn-section-title">Browse by category</h2>
79
+ {categorySummaries().map(({ category, count }) => (
80
+ <div key={category.id} className="learn-category">
81
+ <div className="learn-category-head">
82
+ <h3 className="learn-category-title">{category.label}</h3>
83
+ <span className="muted small">
84
+ {count} article{count === 1 ? '' : 's'}
85
+ </span>
86
+ </div>
87
+ <p className="learn-category-blurb muted">{category.blurb}</p>
88
+ <div className="learn-card-grid">
89
+ {articlesInCategory(category.id)
90
+ .slice()
91
+ .sort(byListingOrder)
92
+ .map((a) => (
93
+ <ArticleCard key={a.slug} article={a} />
94
+ ))}
95
+ </div>
96
+ </div>
97
+ ))}
98
+ </section>
99
+
100
+ <section className="learn-section learn-utility-links" aria-label="Reference">
101
+ <Link to="/learn/glossary" className="learn-utility-link">
102
+ <strong>Glossary</strong>
103
+ <span className="muted small">Plain definitions of the terms used across the app.</span>
104
+ </Link>
105
+ <Link to="/learn/sources" className="learn-utility-link">
106
+ <strong>Sources &amp; review methodology</strong>
107
+ <span className="muted small">Where the rules come from and how often they are reviewed.</span>
108
+ </Link>
109
+ </section>
110
+ </>
111
+ )}
112
+ </article>
113
+ )
114
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Sources & review methodology (/learn/sources).
3
+ *
4
+ * Explains where rule statements come from, how current-year values are
5
+ * date-stamped, and how often content is reviewed. Each article also carries
6
+ * its own source list and last-reviewed date.
7
+ */
8
+
9
+ import { Link } from 'react-router-dom'
10
+
11
+ type PrimarySource = { name: string; scope: string; href: string }
12
+
13
+ const PRIMARY_SOURCES: PrimarySource[] = [
14
+ {
15
+ name: 'IRS (irs.gov)',
16
+ scope: 'Tax brackets, deductions, retirement-account limits, RMD and QCD rules.',
17
+ href: 'https://www.irs.gov/',
18
+ },
19
+ {
20
+ name: 'Social Security Administration (ssa.gov)',
21
+ scope: 'Benefit formulas, claiming rules, and actuarial assumptions.',
22
+ href: 'https://www.ssa.gov/',
23
+ },
24
+ {
25
+ name: 'Medicare / CMS (medicare.gov)',
26
+ scope: 'Medicare premiums and IRMAA income brackets.',
27
+ href: 'https://www.medicare.gov/',
28
+ },
29
+ {
30
+ name: 'HealthCare.gov / CMS',
31
+ scope: 'ACA premium tax credit rules and marketplace eligibility.',
32
+ href: 'https://www.healthcare.gov/',
33
+ },
34
+ ]
35
+
36
+ export function SourcesPage() {
37
+ return (
38
+ <article className="page learn-sources-page">
39
+ <Link to="/learn" className="learn-back">
40
+ ← Learning Center
41
+ </Link>
42
+ <h1>Sources &amp; review methodology</h1>
43
+ <p className="lede">How Learning Center articles stay accurate, and where their rule statements come from.</p>
44
+
45
+ <h2>Primary sources first</h2>
46
+ <p>
47
+ For anything involving tax, Medicare, Social Security, contribution limits, or ACA subsidies, articles cite
48
+ primary sources rather than secondary commentary. Secondary sources are used only for framing or examples,
49
+ never as the sole basis for a rule.
50
+ </p>
51
+ <table className="data-table">
52
+ <thead>
53
+ <tr>
54
+ <th>Source</th>
55
+ <th>What it covers</th>
56
+ </tr>
57
+ </thead>
58
+ <tbody>
59
+ {PRIMARY_SOURCES.map((s) => (
60
+ <tr key={s.href}>
61
+ <td>
62
+ <a href={s.href} target="_blank" rel="noreferrer">
63
+ {s.name}
64
+ </a>
65
+ </td>
66
+ <td>{s.scope}</td>
67
+ </tr>
68
+ ))}
69
+ </tbody>
70
+ </table>
71
+
72
+ <h2>Date-stamped, current-year values</h2>
73
+ <p>
74
+ Rules and dollar figures change — sometimes retroactively. Articles that depend on current-year numbers are
75
+ marked for annual review, carry a “last reviewed” date, and either pull live values from the app’s parameter
76
+ data or clearly stamp the year a figure applies to.
77
+ </p>
78
+
79
+ <h2>Simplifications are called out</h2>
80
+ <p>
81
+ RetireGolden models rules at planning precision, not return-filing precision. When an article describes something
82
+ the engine simplifies or omits, it says so, so you know where to verify a number that matters against the
83
+ official source.
84
+ </p>
85
+
86
+ <h2>Reviews</h2>
87
+ <p>
88
+ Each article declares a review cadence: <strong>annual</strong> for current-year-sensitive topics,{' '}
89
+ <strong>rule-change</strong> for topics that move only when a law changes, and <strong>stable</strong> for
90
+ evergreen concepts. The last-reviewed date on each article reflects its most recent check.
91
+ </p>
92
+
93
+ <p className="muted small">
94
+ See the full <Link to="/disclaimer">disclaimer</Link> for the authoritative legal terms.
95
+ </p>
96
+ </article>
97
+ )
98
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Renders a `figure` block: a registered chart or an image, wrapped in a
3
+ * semantic <figure>/<figcaption> with a screen-reader text equivalent and an
4
+ * optional source note. The visual never carries meaning by colour alone — the
5
+ * `alt` text equivalent always describes what the figure shows.
6
+ */
7
+
8
+ import type { FigureBlockData } from '../learningRegistry'
9
+ import { renderLearnChart } from './charts'
10
+
11
+ export function ArticleFigure({ block }: { block: FigureBlockData }) {
12
+ const chart = block.chartId ? renderLearnChart(block.chartId) : null
13
+
14
+ return (
15
+ <figure className="learn-figure">
16
+ {chart ? (
17
+ <div className="learn-figure-media" role="img" aria-label={block.alt}>
18
+ {chart}
19
+ </div>
20
+ ) : block.image ? (
21
+ <img className="learn-figure-media" src={block.image.src} alt={block.alt} />
22
+ ) : (
23
+ // No renderable media: fall back to the text equivalent so nothing is lost.
24
+ <p className="learn-figure-fallback">{block.alt}</p>
25
+ )}
26
+ {/* Always expose the text equivalent to assistive tech, even for charts. */}
27
+ <span className="sr-only">{block.alt}</span>
28
+ <figcaption className="learn-figure-caption">
29
+ {block.caption}
30
+ {block.sourceNote && <span className="learn-figure-source"> {block.sourceNote}</span>}
31
+ </figcaption>
32
+ </figure>
33
+ )
34
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The consistent layout for a finished article (spec §6.4): back link, category
3
+ * kicker, title, one-sentence promise, the structured body, related articles,
4
+ * sources, last-reviewed date, and a compact educational disclaimer.
5
+ *
6
+ * ArticlePage delegates the readable case here; the planned/not-found states
7
+ * stay in ArticlePage but share {@link BackLink}.
8
+ */
9
+
10
+ import { Link, useLocation } from 'react-router-dom'
11
+ import { OpenExampleButton } from '../../planner/examples/OpenExampleButton'
12
+ import { getCategory, type LearningArticle } from '../learningRegistry'
13
+ import { ArticleBody } from '../ArticleBody'
14
+ import { RelatedArticles } from './RelatedArticles'
15
+ import { SourceList } from './SourceList'
16
+
17
+ function formatReviewed(iso: string): string {
18
+ const [y, m, d] = iso.split('-').map(Number)
19
+ if (!y || !m || !d) return iso
20
+ return new Date(Date.UTC(y, m - 1, d)).toLocaleDateString('en-US', {
21
+ year: 'numeric',
22
+ month: 'long',
23
+ day: 'numeric',
24
+ timeZone: 'UTC',
25
+ })
26
+ }
27
+
28
+ /**
29
+ * Back link for any /learn page. When the reader arrived from a planner screen
30
+ * via {@link LearnLink}, returns them there ("← Back to Optimize"); otherwise
31
+ * falls back to the Learning Center home.
32
+ */
33
+ export function BackLink() {
34
+ const { state } = useLocation()
35
+ const nav = state as { learnFrom?: unknown; learnFromLabel?: unknown } | null
36
+ const from = nav?.learnFrom
37
+ if (typeof from === 'string' && from.startsWith('/') && !from.startsWith('//')) {
38
+ const label = typeof nav?.learnFromLabel === 'string' ? `← Back to ${nav.learnFromLabel}` : '← Back'
39
+ return (
40
+ <Link to={from} className="learn-back">
41
+ {label}
42
+ </Link>
43
+ )
44
+ }
45
+ return (
46
+ <Link to="/learn" className="learn-back">
47
+ ← Learning Center
48
+ </Link>
49
+ )
50
+ }
51
+
52
+ export function ArticleShell({ article }: { article: LearningArticle }) {
53
+ const category = getCategory(article.category)
54
+
55
+ return (
56
+ <article className="page learn-article">
57
+ <BackLink />
58
+ {category && (
59
+ <div className="learn-article-kicker">
60
+ <span className="learn-card-cat">{category.label}</span>
61
+ </div>
62
+ )}
63
+ <h1>{article.title}</h1>
64
+ <p className="lede">{article.description}</p>
65
+
66
+ {article.exampleId ? (
67
+ <div className="learn-example-cta" style={{ marginBottom: '1.25rem' }}>
68
+ <OpenExampleButton exampleId={article.exampleId} />
69
+ </div>
70
+ ) : null}
71
+
72
+ <div className="learn-article-body">
73
+ <ArticleBody blocks={article.blocks ?? []} />
74
+ </div>
75
+
76
+ <RelatedArticles slugs={article.relatedArticles} />
77
+
78
+ {/* The persistent app footer directly below already carries the
79
+ educational disclaimer, so the article foot no longer repeats it. */}
80
+ <footer className="learn-article-foot">
81
+ <SourceList urls={article.sourceUrls} />
82
+ <p className="muted small">Last reviewed {formatReviewed(article.lastReviewed)}.</p>
83
+ </footer>
84
+ </article>
85
+ )
86
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Renders a `table` block: a small comparison table with a caption and proper
3
+ * header scopes. The first column is a row header; cells support inline markdown.
4
+ */
5
+
6
+ import { renderInline } from '../inlineMarkdown'
7
+ import type { TableBlockData } from '../learningRegistry'
8
+
9
+ export function ComparisonTable({ block }: { block: TableBlockData }) {
10
+ const [rowHeader, ...colHeaders] = block.columns
11
+
12
+ return (
13
+ <div className="learn-table-wrap">
14
+ <table className="learn-table">
15
+ {block.caption && <caption className="learn-table-caption">{block.caption}</caption>}
16
+ <thead>
17
+ <tr>
18
+ <th scope="col">{rowHeader}</th>
19
+ {colHeaders.map((c, i) => (
20
+ <th key={i} scope="col">
21
+ {c}
22
+ </th>
23
+ ))}
24
+ </tr>
25
+ </thead>
26
+ <tbody>
27
+ {block.rows.map((row, r) => {
28
+ const [head, ...cells] = row
29
+ return (
30
+ <tr key={r}>
31
+ <th scope="row">{renderInline(head ?? '')}</th>
32
+ {cells.map((cell, c) => (
33
+ <td key={c}>{renderInline(cell)}</td>
34
+ ))}
35
+ </tr>
36
+ )
37
+ })}
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Renders a `formula` block: the expression, a "where" list defining each
3
+ * symbol, whether the dollars are nominal or in today's dollars, and a note on
4
+ * what the simple formula ignores (spec §8.5).
5
+ */
6
+
7
+ import type { FormulaBlockData } from '../learningRegistry'
8
+
9
+ const BASIS_LABEL: Record<NonNullable<FormulaBlockData['basis']>, string> = {
10
+ nominal: 'Amounts are in future (nominal) dollars.',
11
+ today: "Amounts are in today's dollars.",
12
+ }
13
+
14
+ export function FormulaBlock({ block }: { block: FormulaBlockData }) {
15
+ return (
16
+ <div className="learn-formula" role="group" aria-label="Formula">
17
+ <p className="learn-formula-expr">{block.expression}</p>
18
+
19
+ {block.where && block.where.length > 0 && (
20
+ <dl className="learn-formula-where">
21
+ {block.where.map((v) => (
22
+ <div key={v.symbol} className="learn-formula-row">
23
+ <dt>{v.symbol}</dt>
24
+ <dd>{v.meaning}</dd>
25
+ </div>
26
+ ))}
27
+ </dl>
28
+ )}
29
+
30
+ {block.basis && <p className="learn-formula-note muted small">{BASIS_LABEL[block.basis]}</p>}
31
+ {block.note && <p className="learn-formula-note muted small">{block.note}</p>}
32
+ </div>
33
+ )
34
+ }