@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.
- package/LICENSE +661 -0
- package/README.md +181 -0
- package/package.json +77 -0
- package/src/App.tsx +246 -0
- package/src/RouteErrorBoundary.tsx +45 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/data/fedInvestClient.ts +113 -0
- package/src/data/localStore.ts +42 -0
- package/src/data/planOrigin.ts +24 -0
- package/src/data/planStore.ts +165 -0
- package/src/data/v2Backup.ts +101 -0
- package/src/import/ImportPage.tsx +347 -0
- package/src/import/ReviewChecklistView.tsx +38 -0
- package/src/import/brokerCsv.ts +395 -0
- package/src/import/csv.ts +133 -0
- package/src/import/genericCsv.ts +224 -0
- package/src/import/projectionLab.ts +350 -0
- package/src/import/reviewChecklist.ts +33 -0
- package/src/import/tenForty.ts +275 -0
- package/src/index.css +630 -0
- package/src/index.ts +16 -0
- package/src/learn/ArticleBody.tsx +78 -0
- package/src/learn/ArticlePage.tsx +57 -0
- package/src/learn/GlossaryPage.tsx +33 -0
- package/src/learn/LearnAboutScreen.tsx +41 -0
- package/src/learn/LearnCards.tsx +41 -0
- package/src/learn/LearnLink.tsx +91 -0
- package/src/learn/LearningCenterPage.tsx +114 -0
- package/src/learn/SourcesPage.tsx +98 -0
- package/src/learn/components/ArticleFigure.tsx +34 -0
- package/src/learn/components/ArticleShell.tsx +86 -0
- package/src/learn/components/ComparisonTable.tsx +42 -0
- package/src/learn/components/FormulaBlock.tsx +34 -0
- package/src/learn/components/PurchasingPowerChart.tsx +41 -0
- package/src/learn/components/RelatedArticles.tsx +27 -0
- package/src/learn/components/ScenarioCard.tsx +24 -0
- package/src/learn/components/SourceList.tsx +23 -0
- package/src/learn/components/charts.tsx +21 -0
- package/src/learn/content/about-retiregolden.ts +100 -0
- package/src/learn/content/aca-premium-tax-credits-and-magi.ts +103 -0
- package/src/learn/content/account-types-overview.ts +106 -0
- package/src/learn/content/after-tax-estate.ts +111 -0
- package/src/learn/content/agi-magi-and-taxable-income.ts +112 -0
- package/src/learn/content/appealing-irmaa-ssa-44.ts +95 -0
- package/src/learn/content/assumption-general-inflation.ts +82 -0
- package/src/learn/content/assumption-healthcare-inflation.ts +85 -0
- package/src/learn/content/assumption-heir-tax-rate.ts +79 -0
- package/src/learn/content/assumption-investment-returns.ts +90 -0
- package/src/learn/content/assumption-longevity-planning-age.ts +78 -0
- package/src/learn/content/assumption-recent-magi.ts +83 -0
- package/src/learn/content/assumption-social-security-cola.ts +89 -0
- package/src/learn/content/assumption-social-security-trust-fund.ts +83 -0
- package/src/learn/content/assumption-state-tax-override.ts +79 -0
- package/src/learn/content/beneficiaries-and-account-titling.ts +99 -0
- package/src/learn/content/break-even-useful-lens.ts +94 -0
- package/src/learn/content/building-a-retirement-spending-budget.ts +100 -0
- package/src/learn/content/cola-and-inflation-protection.ts +102 -0
- package/src/learn/content/divorced-spousal-and-survivor-records.ts +104 -0
- package/src/learn/content/dynamic-spending-guardrails.ts +90 -0
- package/src/learn/content/earnings-test-before-fra.ts +100 -0
- package/src/learn/content/employer-match-and-contribution-order.ts +104 -0
- package/src/learn/content/examplePlanArticles.ts +525 -0
- package/src/learn/content/fees-expense-ratios-and-compounding-drag.ts +98 -0
- package/src/learn/content/fi-number-and-four-percent-rule.ts +64 -0
- package/src/learn/content/filling-a-tax-bracket-with-roth-conversions.ts +98 -0
- package/src/learn/content/funded-ratio.ts +70 -0
- package/src/learn/content/healthcare-after-65.ts +103 -0
- package/src/learn/content/healthcare-before-65.ts +104 -0
- package/src/learn/content/historical-vs-random-return-models.ts +101 -0
- package/src/learn/content/how-assumptions-change-the-answer.ts +105 -0
- package/src/learn/content/how-much-can-i-spend.ts +105 -0
- package/src/learn/content/how-social-security-is-taxed.ts +95 -0
- package/src/learn/content/how-the-optimizer-thinks.ts +102 -0
- package/src/learn/content/how-the-optimizer-values-after-tax-estate.ts +97 -0
- package/src/learn/content/how-to-model-accumulation.ts +67 -0
- package/src/learn/content/how-to-read-a-retirement-projection.ts +115 -0
- package/src/learn/content/hsas-and-qualified-medical-expenses.ts +108 -0
- package/src/learn/content/hsas-as-retirement-accounts.ts +101 -0
- package/src/learn/content/inflation-risk.ts +98 -0
- package/src/learn/content/inherited-ira-10-year-rule.ts +105 -0
- package/src/learn/content/insurance-in-your-retirement-plan.ts +103 -0
- package/src/learn/content/irmaa-two-year-lookback.ts +99 -0
- package/src/learn/content/long-term-care-costs-and-insurance.ts +103 -0
- package/src/learn/content/long-term-care-insurance-as-risk-transfer.ts +98 -0
- package/src/learn/content/longevity-risk.ts +99 -0
- package/src/learn/content/marginal-vs-effective-tax-rate.ts +98 -0
- package/src/learn/content/medicare-part-b-vs-part-d-irmaa.ts +102 -0
- package/src/learn/content/mortality-weighted-social-security.ts +113 -0
- package/src/learn/content/moving-to-retiregolden.ts +86 -0
- package/src/learn/content/niit-high-income-investment-tax.ts +98 -0
- package/src/learn/content/ordinary-income-vs-capital-gains.ts +103 -0
- package/src/learn/content/paying-conversion-taxes-taxable-vs-ira.ts +102 -0
- package/src/learn/content/pensions-and-annuities.ts +101 -0
- package/src/learn/content/permanent-life-insurance-in-a-plan.ts +106 -0
- package/src/learn/content/pia-aime-and-bend-points.ts +103 -0
- package/src/learn/content/planner-overview.ts +106 -0
- package/src/learn/content/planning-for-couples-and-survivor-years.ts +108 -0
- package/src/learn/content/privacy-what-stays-in-your-browser.ts +99 -0
- package/src/learn/content/qcds-qualified-charitable-distributions.ts +101 -0
- package/src/learn/content/reading-the-results-page.ts +96 -0
- package/src/learn/content/reading-the-social-security-analysis-page.ts +106 -0
- package/src/learn/content/real-estate-home-equity-and-debt.ts +100 -0
- package/src/learn/content/reports-csv-exports-and-sharing.ts +101 -0
- package/src/learn/content/risk-based-guardrails.ts +100 -0
- package/src/learn/content/rmds-required-minimum-distributions.ts +100 -0
- package/src/learn/content/roth-conversion-basics.ts +104 -0
- package/src/learn/content/rsus-and-espp.ts +101 -0
- package/src/learn/content/rule-of-55-and-72t.ts +107 -0
- package/src/learn/content/savings-rate-biggest-lever.ts +66 -0
- package/src/learn/content/seed-your-plan-from-your-tax-return.ts +93 -0
- package/src/learn/content/sensitivity-testing-what-changes-the-answer.ts +104 -0
- package/src/learn/content/sequence-of-returns-risk.ts +98 -0
- package/src/learn/content/social-security-bridge.ts +67 -0
- package/src/learn/content/social-security-claiming-age-basics.ts +113 -0
- package/src/learn/content/social-security-taxes-vs-benefits.ts +76 -0
- package/src/learn/content/spending-profiles-and-the-retirement-smile.ts +92 -0
- package/src/learn/content/spousal-and-survivor-benefits.ts +120 -0
- package/src/learn/content/ssdi-and-retirement-planning.ts +72 -0
- package/src/learn/content/standard-deduction-senior-deduction-and-itemizing.ts +97 -0
- package/src/learn/content/state-income-taxes-in-retirement.ts +97 -0
- package/src/learn/content/step-up-in-basis.ts +102 -0
- package/src/learn/content/survivor-planning-for-couples.ts +110 -0
- package/src/learn/content/survivor-spending-in-couple-plans.ts +98 -0
- package/src/learn/content/tax-cliffs-and-bracket-edges.ts +105 -0
- package/src/learn/content/tax-loss-and-gain-harvesting.ts +99 -0
- package/src/learn/content/taxable-brokerage-basis-and-capital-gains.ts +99 -0
- package/src/learn/content/three-big-questions-spending-time-risk.ts +103 -0
- package/src/learn/content/tips-ladders.ts +92 -0
- package/src/learn/content/todays-dollars-vs-future-dollars.ts +107 -0
- package/src/learn/content/traditional-vs-roth-contributions.ts +113 -0
- package/src/learn/content/troubleshooting-surprising-results.ts +105 -0
- package/src/learn/content/trust-fund-haircut-scenarios.ts +101 -0
- package/src/learn/content/understanding-monte-carlo-success-rate.ts +118 -0
- package/src/learn/content/understanding-your-plan-assumptions.ts +134 -0
- package/src/learn/content/using-assumptions-and-provenance.ts +98 -0
- package/src/learn/content/using-scenarios-to-compare-choices.ts +99 -0
- package/src/learn/content/what-changes-when-you-move-states.ts +141 -0
- package/src/learn/content/what-is-fire.ts +65 -0
- package/src/learn/content/what-monte-carlo-proves.ts +98 -0
- package/src/learn/content/what-retiregolden-models.ts +103 -0
- package/src/learn/content/what-retirement-healthcare-really-costs.ts +117 -0
- package/src/learn/content/why-95-percent-is-not-a-guarantee.ts +98 -0
- package/src/learn/content/why-roth-conversions-raise-other-costs.ts +106 -0
- package/src/learn/content/why-small-tax-cliffs-can-matter.ts +109 -0
- package/src/learn/content/widows-penalty-and-survivor-brackets.ts +106 -0
- package/src/learn/content/withdrawal-order-basics.ts +105 -0
- package/src/learn/glossary.ts +191 -0
- package/src/learn/inlineMarkdown.tsx +54 -0
- package/src/learn/learn.css +537 -0
- package/src/learn/learningRegistry.ts +502 -0
- package/src/longevity/LongevityResults.tsx +85 -0
- package/src/longevity/LongevityWizard.tsx +305 -0
- package/src/longevity/constants.ts +15 -0
- package/src/longevity/factors.ts +125 -0
- package/src/longevity/model.ts +31 -0
- package/src/longevity/persistedGuard.ts +129 -0
- package/src/longevity/storage.ts +40 -0
- package/src/mc/messages.ts +118 -0
- package/src/mc/monteCarlo.worker.ts +44 -0
- package/src/mc/pool.ts +267 -0
- package/src/mc/runRequest.ts +125 -0
- package/src/optimize/messages.ts +84 -0
- package/src/optimize/optimize.worker.ts +29 -0
- package/src/optimize/runOptimize.ts +92 -0
- package/src/optimize/runSpendingSolve.ts +47 -0
- package/src/optimize/runner.ts +21 -0
- package/src/optimize/spendingMessages.ts +44 -0
- package/src/optimize/spendingRunner.ts +21 -0
- package/src/optimize/spendingSolve.worker.ts +18 -0
- package/src/planner/AssumptionsCardPage.tsx +136 -0
- package/src/planner/BucketLensCard.tsx +114 -0
- package/src/planner/ComparePlansPage.tsx +219 -0
- package/src/planner/DisclaimerPage.tsx +88 -0
- package/src/planner/HowTestedPage.tsx +159 -0
- package/src/planner/LiveStatus.tsx +15 -0
- package/src/planner/LongevityModal.tsx +55 -0
- package/src/planner/Modal.tsx +97 -0
- package/src/planner/MonteCarloPage.tsx +907 -0
- package/src/planner/OptimizePage.tsx +611 -0
- package/src/planner/PlanContext.tsx +198 -0
- package/src/planner/PlanPickerPage.tsx +124 -0
- package/src/planner/PlanWorkspace.tsx +290 -0
- package/src/planner/ProvenancePanel.tsx +45 -0
- package/src/planner/RelocationComparePage.tsx +485 -0
- package/src/planner/ReportPage.tsx +375 -0
- package/src/planner/ResultsPage.tsx +817 -0
- package/src/planner/ScenariosPage.tsx +285 -0
- package/src/planner/SocialSecuritySection.tsx +556 -0
- package/src/planner/SpendingSolverPage.tsx +512 -0
- package/src/planner/SsAnalysisPage.tsx +1134 -0
- package/src/planner/SurvivalPercentileModal.tsx +161 -0
- package/src/planner/SurvivorTransitionPage.tsx +286 -0
- package/src/planner/assumptionsExport.ts +371 -0
- package/src/planner/bucketLens.ts +89 -0
- package/src/planner/chartFrame.ts +8 -0
- package/src/planner/chartStyle.ts +11 -0
- package/src/planner/dialogViews.tsx +184 -0
- package/src/planner/dialogs.tsx +133 -0
- package/src/planner/examples/ExampleLibrary.tsx +189 -0
- package/src/planner/examples/ExamplePreviewBanner.tsx +55 -0
- package/src/planner/examples/ExamplesPage.tsx +25 -0
- package/src/planner/examples/OpenExampleButton.tsx +61 -0
- package/src/planner/examples/buildAggressiveSaver.ts +102 -0
- package/src/planner/examples/buildAnnuityEstate.ts +137 -0
- package/src/planner/examples/buildBaristaFire.ts +115 -0
- package/src/planner/examples/buildBracketFillRoth.ts +65 -0
- package/src/planner/examples/buildBridgeEarlyRetirement.ts +94 -0
- package/src/planner/examples/buildBrokerageNoHsa.ts +109 -0
- package/src/planner/examples/buildCoastFire.ts +88 -0
- package/src/planner/examples/buildContext.ts +20 -0
- package/src/planner/examples/buildEarlyCareerMatch.ts +93 -0
- package/src/planner/examples/buildEarlyRetireeAca.ts +61 -0
- package/src/planner/examples/buildExampleCouple.ts +103 -0
- package/src/planner/examples/buildFixedTargetSpending.ts +74 -0
- package/src/planner/examples/buildGlidepathAllocation.ts +131 -0
- package/src/planner/examples/buildGuardrailsFlex.ts +120 -0
- package/src/planner/examples/buildHsaPropertyDepth.ts +109 -0
- package/src/planner/examples/buildHsaStealthRetirement.ts +97 -0
- package/src/planner/examples/buildLeanFatFire.ts +109 -0
- package/src/planner/examples/buildLtcShock.ts +62 -0
- package/src/planner/examples/buildMovingStateTax.ts +53 -0
- package/src/planner/examples/buildNoAnnuityBrokerage.ts +92 -0
- package/src/planner/examples/buildRmdIrmaa.ts +55 -0
- package/src/planner/examples/buildSalaryGrowthEscalation.ts +96 -0
- package/src/planner/examples/buildStaticAllocationControl.ts +96 -0
- package/src/planner/examples/buildSurvivorYears.ts +62 -0
- package/src/planner/examples/buildUnderSavedSingle.ts +51 -0
- package/src/planner/examples/exampleCopy.ts +23 -0
- package/src/planner/examples/loadExample.ts +90 -0
- package/src/planner/examples/registry.ts +313 -0
- package/src/planner/explainPanels.tsx +233 -0
- package/src/planner/fields.tsx +381 -0
- package/src/planner/format.ts +33 -0
- package/src/planner/home/DataAndPrivacyCard.tsx +56 -0
- package/src/planner/home/GettingStartedPaths.tsx +46 -0
- package/src/planner/home/GettingStartedReopener.tsx +32 -0
- package/src/planner/home/StartHereLinks.tsx +22 -0
- package/src/planner/home/WelcomeHero.tsx +39 -0
- package/src/planner/home/YourPlans.tsx +72 -0
- package/src/planner/home/importErrorMessage.ts +22 -0
- package/src/planner/home/startHereSlugs.ts +7 -0
- package/src/planner/home/useHomeData.ts +190 -0
- package/src/planner/home/useHomeMode.ts +47 -0
- package/src/planner/householdActions.ts +22 -0
- package/src/planner/insights/InsightCardView.tsx +340 -0
- package/src/planner/insights/InsightsPage.tsx +204 -0
- package/src/planner/insights/categoryLabels.ts +11 -0
- package/src/planner/learnLinks.ts +85 -0
- package/src/planner/marketModelPicker.ts +172 -0
- package/src/planner/optimizePageChart.ts +40 -0
- package/src/planner/optimizePageClaim.ts +64 -0
- package/src/planner/planCompleteness.ts +27 -0
- package/src/planner/planContextCore.ts +26 -0
- package/src/planner/planner.css +2304 -0
- package/src/planner/provenanceLinks.ts +25 -0
- package/src/planner/sections/AccountFields.tsx +872 -0
- package/src/planner/sections/AccountsSection.tsx +89 -0
- package/src/planner/sections/AllocationPanel.tsx +261 -0
- package/src/planner/sections/AssumptionsSection.tsx +256 -0
- package/src/planner/sections/HouseholdSection.tsx +243 -0
- package/src/planner/sections/IncomeFloorSection.tsx +418 -0
- package/src/planner/sections/IncomeSection.tsx +170 -0
- package/src/planner/sections/InsuranceSection.tsx +362 -0
- package/src/planner/sections/SpendingSection.tsx +904 -0
- package/src/planner/sections/StrategySection.tsx +349 -0
- package/src/planner/sections/UpdateBalancesPanel.tsx +182 -0
- package/src/planner/sections/sectionHelpers.ts +48 -0
- package/src/planner/sections/shared.tsx +15 -0
- package/src/planner/sections.tsx +15 -0
- package/src/planner/ssAnalysis.ts +325 -0
- package/src/planner/successBand.ts +20 -0
- package/src/planner/survivorAnalysis.ts +277 -0
- package/src/planner/usStates.ts +19 -0
- package/src/planner/useMcSuccessRate.ts +77 -0
- package/src/planner/useProjection.ts +63 -0
- package/src/relocation/messages.ts +21 -0
- package/src/relocation/relocation.worker.ts +18 -0
- package/src/relocation/runRelocation.ts +17 -0
- package/src/relocation/runner.ts +22 -0
- package/src/report/brandingContext.ts +15 -0
- package/src/report/downloadReport.ts +34 -0
- package/src/report/reportHtml.ts +547 -0
- package/src/routes/LearnRoutes.tsx +46 -0
- package/src/routes/PlanRoutes.tsx +55 -0
- package/src/routes/RouteFallback.tsx +9 -0
- package/src/socialSecurity/breakEven.ts +107 -0
- package/src/socialSecurity/expectedPv.ts +164 -0
- package/src/socialSecurity/explain.ts +92 -0
- package/src/socialSecurity/ficaReturn.ts +81 -0
- package/src/socialSecurity/persistedSsGuard.ts +138 -0
- package/src/socialSecurity/ssFormUtils.ts +48 -0
- package/src/socialSecurity/ssaStatementXml.ts +156 -0
- package/src/socialSecurity/storage.ts +69 -0
- package/src/socialSecurity/survivorSwitching.ts +153 -0
- package/src/testSupport/samplePlan.ts +2 -0
- package/src/workers/run.ts +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# @retiregolden/planner-ui
|
|
2
|
+
|
|
3
|
+
The [RetireGolden](https://retiregolden.app/) planner React UI — the complete
|
|
4
|
+
planner a host application composes inside its own router: plan picker and
|
|
5
|
+
workspace, results/Monte Carlo/scenarios/survivor/relocation pages, the
|
|
6
|
+
Roth & Tax / spending / Social Security optimizers, the import wizard, the
|
|
7
|
+
Learning Center, report export, and browser persistence (IndexedDB +
|
|
8
|
+
localStorage). All money math lives in
|
|
9
|
+
[`@retiregolden/engine`](https://www.npmjs.com/package/@retiregolden/engine)
|
|
10
|
+
(a dependency of this package, not re-exported by it); this package is the UI
|
|
11
|
+
and persistence around it.
|
|
12
|
+
|
|
13
|
+
Source of truth: [github.com/RetireGolden/RetireGolden](https://github.com/RetireGolden/RetireGolden)
|
|
14
|
+
(`packages/planner-ui`). Engineering docs live in the repo's `DOCS/`.
|
|
15
|
+
|
|
16
|
+
## What ships, and the bundler contract
|
|
17
|
+
|
|
18
|
+
This package publishes its **TypeScript source** (`src/`, referenced directly
|
|
19
|
+
by `exports` and `types`) and requires a **Vite-class bundler** — it is not
|
|
20
|
+
consumable from plain Node or from bundlers that don't implement Vite
|
|
21
|
+
semantics. The planner tree relies on features only a Vite-style build
|
|
22
|
+
provides:
|
|
23
|
+
|
|
24
|
+
- `new Worker(new URL('./x.worker.ts', import.meta.url), { type: 'module' })`
|
|
25
|
+
for the Monte Carlo pool, the optimizers, and relocation compare;
|
|
26
|
+
- `import wasmUrl from 'highs/runtime?url'` for the HiGHS LP-solver wasm;
|
|
27
|
+
- `import.meta.glob`, `import.meta.env.DEV`, CSS and image imports.
|
|
28
|
+
|
|
29
|
+
Compiling to plain ESM would not remove any of those requirements, so the
|
|
30
|
+
package doesn't pretend otherwise. Known-good consumers: Vite ≥ 8 and
|
|
31
|
+
electron-vite, with React 19 and react-router v7 (peers).
|
|
32
|
+
|
|
33
|
+
### Required Vite config
|
|
34
|
+
|
|
35
|
+
Dependency pre-bundling must skip this package (esbuild/rolldown pre-bundles
|
|
36
|
+
break the Vite-only constructs above) while still pre-bundling its
|
|
37
|
+
CommonJS-flavoured dependencies for dev:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// vite.config.ts
|
|
41
|
+
export default defineConfig({
|
|
42
|
+
plugins: [react()],
|
|
43
|
+
optimizeDeps: {
|
|
44
|
+
exclude: ['@retiregolden/planner-ui'],
|
|
45
|
+
include: ['@retiregolden/planner-ui > highs', '@retiregolden/planner-ui > recharts'],
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Production builds work without this; dev mode does not — leave it in place.
|
|
51
|
+
TypeScript consumers should use `"moduleResolution": "bundler"` and include
|
|
52
|
+
`vite/client` types (standard in Vite templates), since type checking reads
|
|
53
|
+
this package's source.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install @retiregolden/planner-ui react react-dom react-router-dom
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The host owns the router and mounts the planner under it:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { StrictMode } from 'react'
|
|
65
|
+
import { createRoot } from 'react-dom/client'
|
|
66
|
+
import { BrowserRouter } from 'react-router-dom'
|
|
67
|
+
import '@retiregolden/planner-ui/index.css'
|
|
68
|
+
import { PlannerApp } from '@retiregolden/planner-ui'
|
|
69
|
+
|
|
70
|
+
createRoot(document.getElementById('root')!).render(
|
|
71
|
+
<StrictMode>
|
|
72
|
+
<BrowserRouter>
|
|
73
|
+
<PlannerApp />
|
|
74
|
+
</BrowserRouter>
|
|
75
|
+
</StrictMode>,
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`<PlannerApp />` is everything inside the router: the app chrome (header,
|
|
80
|
+
nav, theme toggle, footer), the route table, and the route error boundary.
|
|
81
|
+
Use `BrowserRouter`, `HashRouter` (Electron `file://` hosts), or
|
|
82
|
+
`MemoryRouter` as the host requires; a `basename` on the host's router is
|
|
83
|
+
respected because the planner only uses relative react-router APIs.
|
|
84
|
+
|
|
85
|
+
### CSS and theming
|
|
86
|
+
|
|
87
|
+
- `@retiregolden/planner-ui/index.css` — required: the design-token layer
|
|
88
|
+
(CSS custom properties for both light and dark themes) plus base styles.
|
|
89
|
+
Import it once in the host.
|
|
90
|
+
- Component styles (`planner.css`, `learn.css`) are imported by the
|
|
91
|
+
components themselves; no extra import is needed.
|
|
92
|
+
|
|
93
|
+
Theming hook: the token layer is plain CSS custom properties on `:root` (and
|
|
94
|
+
`[data-theme]` variants), so a host can re-brand by loading its own
|
|
95
|
+
stylesheet after `index.css` and overriding tokens. Keep overrides
|
|
96
|
+
WCAG-AA-honest — the upstream palette is contrast-guarded by tests.
|
|
97
|
+
|
|
98
|
+
### Host-served static assets (required)
|
|
99
|
+
|
|
100
|
+
The planner references a small set of **root-absolute URLs that the host must
|
|
101
|
+
serve** — they are deliberately not bundled, because several are
|
|
102
|
+
host-specific by nature. `app/public/` in the upstream repo is the reference
|
|
103
|
+
tree to copy from:
|
|
104
|
+
|
|
105
|
+
| Path | Used by | Notes |
|
|
106
|
+
|------|---------|-------|
|
|
107
|
+
| `/favicon.svg` | Header brand mark (mobile), logo fallback | |
|
|
108
|
+
| `/brand/retiregolden-logo-lockup.png`, `/brand/retiregolden-logo-lockup-light.png` | Header logo (dark/light) | Missing files degrade to `/favicon.svg` |
|
|
109
|
+
| `/learn/images/*.webp` | Learning Center article illustrations (~5 MB total) | Copy from `app/public/learn/images/` |
|
|
110
|
+
| `/THIRD-PARTY-NOTICES.txt` | Disclaimer page's third-party attribution link | **Must describe the host's own bundle** — generate it from your dependency tree (the upstream generator is `app/scripts/generate-third-party-notices.mjs`); do not copy the web app's file verbatim |
|
|
111
|
+
|
|
112
|
+
Electron hosts loading over `file://` should use `HashRouter` and a protocol
|
|
113
|
+
handler (or serve the app over a local scheme) so these root-absolute URLs
|
|
114
|
+
resolve against the app bundle, not the filesystem root.
|
|
115
|
+
|
|
116
|
+
### Published API surface
|
|
117
|
+
|
|
118
|
+
The supported product API is the root export (`PlannerApp`) plus
|
|
119
|
+
`./index.css`. The exports map also exposes wildcard `./*.ts` subpaths
|
|
120
|
+
(e.g. `./report/reportHtml`) — these exist for the upstream repo's own test
|
|
121
|
+
and case-runner harnesses, are not covered by any stability promise, and may
|
|
122
|
+
move or change in any release. If a host needs one of them long-term, open an
|
|
123
|
+
upstream issue so it can be promoted to a real export instead.
|
|
124
|
+
|
|
125
|
+
### Report branding
|
|
126
|
+
|
|
127
|
+
Downloaded HTML reports (Results, Report, and Optimizer pages) can carry the
|
|
128
|
+
host's identity via the optional `reportBranding` prop — a generic hook, with
|
|
129
|
+
RetireGolden defaults when omitted:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<PlannerApp
|
|
133
|
+
reportBranding={{
|
|
134
|
+
productName: 'Acme Wealth Planner', // report title, header line, filename
|
|
135
|
+
logoDataUri: 'data:image/png;base64,...', // letterhead logo (data: URI only — reports stay self-contained)
|
|
136
|
+
logoAlt: 'Acme Wealth',
|
|
137
|
+
accentColor: '#1a3a5c', // letterhead rule color
|
|
138
|
+
footerNote: 'Prepared by Acme Wealth Advisors LLC.',
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`buildStandaloneReportHtml` (deep subpath `./report/reportHtml` — the
|
|
144
|
+
stability caveat under "Published API surface" applies) accepts the same
|
|
145
|
+
`branding` object directly for hosts that generate reports outside the
|
|
146
|
+
planner pages. Values are sanitized — the logo must be a base64
|
|
147
|
+
`data:image/...` URI, the accent must parse as a real CSS color (hex,
|
|
148
|
+
`rgb()`/`hsl()`, or a named color; anything else falls back to the default
|
|
149
|
+
gold), and text fields are escaped-and-kept — so the report's no-script
|
|
150
|
+
guarantee holds regardless of input. This branding
|
|
151
|
+
applies to downloaded reports only; the in-app chrome is themed via the CSS
|
|
152
|
+
tokens (above).
|
|
153
|
+
|
|
154
|
+
### Storage
|
|
155
|
+
|
|
156
|
+
Plans persist in the browser profile via IndexedDB (`idb`) with localStorage
|
|
157
|
+
for small preferences, exactly as on retiregolden.app. Hosts that need a
|
|
158
|
+
different persistence story should treat that as an upstream conversation,
|
|
159
|
+
not a fork point.
|
|
160
|
+
|
|
161
|
+
## Relationship to the web app
|
|
162
|
+
|
|
163
|
+
`app/` in the upstream repo is the reference host: it adds the PWA service
|
|
164
|
+
worker, SEO/meta, sitemap and prerender tooling, and static-hosting config,
|
|
165
|
+
then mounts `<PlannerApp />` exactly as in the snippet above. This package
|
|
166
|
+
contains no host-specific behavior — composition points are generic React
|
|
167
|
+
props/slots and CSS tokens, nothing keyed to any particular product.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
**AGPL-3.0-only** (see [LICENSE](LICENSE)). The planner UI is free and
|
|
172
|
+
un-gutted — the full feature set ships in the free web app.
|
|
173
|
+
|
|
174
|
+
RetireGolden, LLC also ships a commercial desktop edition built from this
|
|
175
|
+
same UI under a separate commercial license, which funds the free one. That
|
|
176
|
+
dual-license arrangement is why contributions to the
|
|
177
|
+
[upstream repo](https://github.com/RetireGolden/RetireGolden) require a
|
|
178
|
+
one-time [Contributor License Agreement](https://github.com/RetireGolden/RetireGolden/blob/main/CLA.md)
|
|
179
|
+
— you keep your copyright; the CLA lets the LLC also ship your contribution
|
|
180
|
+
in the commercial edition. See
|
|
181
|
+
[CONTRIBUTING.md](https://github.com/RetireGolden/RetireGolden/blob/main/CONTRIBUTING.md).
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@retiregolden/planner-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The RetireGolden planner React UI: plan picker, planner workspace, results/Monte Carlo/optimizer pages, import wizard, Learning Center, and report export — the full planner tree a host app composes inside its own router. Ships TypeScript source and requires a Vite-class bundler.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/RetireGolden/RetireGolden.git",
|
|
13
|
+
"directory": "packages/planner-ui"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/RetireGolden/RetireGolden/tree/main/packages/planner-ui#readme",
|
|
16
|
+
"bugs": "https://github.com/RetireGolden/RetireGolden/issues",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"retirement",
|
|
19
|
+
"financial-planning",
|
|
20
|
+
"react",
|
|
21
|
+
"planner",
|
|
22
|
+
"monte-carlo"
|
|
23
|
+
],
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"!src/**/*.test.ts",
|
|
27
|
+
"!src/**/*.test.tsx",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"types": "./src/index.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./src/index.ts",
|
|
34
|
+
"./index.css": "./src/index.css",
|
|
35
|
+
"./*": "./src/*.ts",
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -b",
|
|
40
|
+
"lint": "eslint .",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:coverage": "vitest run --coverage"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@retiregolden/engine": "^0.1.0",
|
|
46
|
+
"highs": "^1.14.2",
|
|
47
|
+
"idb": "^8.0.3",
|
|
48
|
+
"recharts": "^3.8.1"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": "^19.2.0",
|
|
52
|
+
"react-dom": "^19.2.0",
|
|
53
|
+
"react-router-dom": "^7.18.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@eslint/js": "^10.0.1",
|
|
57
|
+
"@types/jsdom": "^28.0.3",
|
|
58
|
+
"@types/node": "^24.12.3",
|
|
59
|
+
"@types/react": "^19.2.14",
|
|
60
|
+
"@types/react-dom": "^19.2.3",
|
|
61
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
62
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
63
|
+
"eslint": "^10.3.0",
|
|
64
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
65
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
66
|
+
"fake-indexeddb": "^6.2.5",
|
|
67
|
+
"globals": "^17.6.0",
|
|
68
|
+
"jsdom": "^29.1.1",
|
|
69
|
+
"react": "^19.2.6",
|
|
70
|
+
"react-dom": "^19.2.6",
|
|
71
|
+
"react-router-dom": "^7.18.0",
|
|
72
|
+
"typescript": "~6.0.2",
|
|
73
|
+
"typescript-eslint": "^8.59.2",
|
|
74
|
+
"vite": "^8.0.16",
|
|
75
|
+
"vitest": "^4.0.16"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { lazy, Suspense, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Navigate, NavLink, Route, Routes, useLocation } from 'react-router-dom'
|
|
3
|
+
import { PlanPickerPage } from './planner/PlanPickerPage'
|
|
4
|
+
import { DisclaimerPage } from './planner/DisclaimerPage'
|
|
5
|
+
import { RouteErrorBoundary } from './RouteErrorBoundary.tsx'
|
|
6
|
+
import { RouteFallback } from './routes/RouteFallback'
|
|
7
|
+
import { readLocal, STORAGE_KEYS, writeLocal } from './data/localStore'
|
|
8
|
+
import { ReportBrandingContext } from './report/brandingContext'
|
|
9
|
+
import type { ReportBranding } from './report/reportHtml'
|
|
10
|
+
import './planner/planner.css'
|
|
11
|
+
|
|
12
|
+
const PlanRoutes = lazy(() => import('./routes/PlanRoutes'))
|
|
13
|
+
const LearnRoutes = lazy(() => import('./routes/LearnRoutes'))
|
|
14
|
+
// Lazy like /plan and /learn: Examples pulls in all example builders + the Zod
|
|
15
|
+
// schema, Compare pulls in the whole engine via projectPlan — eager imports
|
|
16
|
+
// here would drag both into the landing entry chunk.
|
|
17
|
+
const ExamplesPage = lazy(() => import('./planner/examples/ExamplesPage').then((m) => ({ default: m.ExamplesPage })))
|
|
18
|
+
const ComparePlansPage = lazy(() => import('./planner/ComparePlansPage').then((m) => ({ default: m.ComparePlansPage })))
|
|
19
|
+
// Lazy so the glob-derived test-suite manifests it embeds stay out of the landing chunk.
|
|
20
|
+
const HowTestedPage = lazy(() => import('./planner/HowTestedPage').then((m) => ({ default: m.HowTestedPage })))
|
|
21
|
+
// Lazy: the import wizard pulls in the per-source mappers and the Zod schema.
|
|
22
|
+
const ImportPage = lazy(() => import('./import/ImportPage').then((m) => ({ default: m.ImportPage })))
|
|
23
|
+
|
|
24
|
+
const navClass = ({ isActive }: { isActive: boolean }) =>
|
|
25
|
+
isActive ? 'nav-link nav-link--active' : 'nav-link'
|
|
26
|
+
|
|
27
|
+
/** Tab/history titles for non-plan routes; plan routes are owned by PlanWorkspace. */
|
|
28
|
+
const ROUTE_TITLES: ReadonlyArray<[prefix: string, title: string]> = [
|
|
29
|
+
['/examples', 'Examples'],
|
|
30
|
+
['/import', 'Import & migrate'],
|
|
31
|
+
['/compare', 'Compare plans'],
|
|
32
|
+
['/learn', 'Learn'],
|
|
33
|
+
['/disclaimer', 'Disclaimer'],
|
|
34
|
+
['/how-tested', 'How RetireGolden is tested'],
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
function routeTitleOf(pathname: string): string | null {
|
|
38
|
+
if (pathname === '/') return 'Your plans'
|
|
39
|
+
for (const [prefix, title] of ROUTE_TITLES) {
|
|
40
|
+
if (pathname === prefix || pathname.startsWith(`${prefix}/`)) return title
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type ThemeMode = 'light' | 'dark' | 'system'
|
|
46
|
+
|
|
47
|
+
const THEME_STORAGE_KEY = STORAGE_KEYS.theme
|
|
48
|
+
const THEME_MODES: ThemeMode[] = ['light', 'dark', 'system']
|
|
49
|
+
|
|
50
|
+
function getInitialThemeMode(): ThemeMode {
|
|
51
|
+
if (typeof window === 'undefined') return 'system'
|
|
52
|
+
const stored = readLocal(THEME_STORAGE_KEY)
|
|
53
|
+
return stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getResolvedTheme(mode: ThemeMode) {
|
|
57
|
+
if (mode !== 'system') return mode
|
|
58
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return 'light'
|
|
59
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PlannerAppProps {
|
|
63
|
+
/**
|
|
64
|
+
* Identity applied to downloaded HTML reports (name, logo, accent color,
|
|
65
|
+
* footer note) — a generic host hook; omit it and reports keep the
|
|
66
|
+
* RetireGolden defaults. In-app chrome is themed via CSS tokens instead
|
|
67
|
+
* (override the custom properties from index.css).
|
|
68
|
+
*/
|
|
69
|
+
reportBranding?: ReportBranding
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function App({ reportBranding }: PlannerAppProps = {}) {
|
|
73
|
+
const location = useLocation()
|
|
74
|
+
const isLanding = location.pathname === '/' || location.pathname === '/examples'
|
|
75
|
+
const [themeMode, setThemeMode] = useState<ThemeMode>(getInitialThemeMode)
|
|
76
|
+
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => getResolvedTheme(getInitialThemeMode()))
|
|
77
|
+
const [logoLoadFailed, setLogoLoadFailed] = useState(false)
|
|
78
|
+
const isFirstRoute = useRef(true)
|
|
79
|
+
|
|
80
|
+
// Page identity: retitle the tab per route (plan routes retitle themselves in
|
|
81
|
+
// PlanWorkspace with the plan name) and move focus to the main landmark on
|
|
82
|
+
// SPA navigation so screen readers hear the new page instead of silence.
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!location.pathname.startsWith('/plan/')) {
|
|
85
|
+
const title = routeTitleOf(location.pathname)
|
|
86
|
+
document.title = title ? `${title} · RetireGolden` : 'RetireGolden'
|
|
87
|
+
}
|
|
88
|
+
if (isFirstRoute.current) {
|
|
89
|
+
// Initial load: the browser's own focus/scroll behavior is correct.
|
|
90
|
+
isFirstRoute.current = false
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
document.getElementById('main-content')?.focus()
|
|
94
|
+
}, [location.pathname])
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const root = document.documentElement
|
|
98
|
+
const themeColor = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
|
|
99
|
+
|
|
100
|
+
const applyTheme = () => {
|
|
101
|
+
const nextResolvedTheme = getResolvedTheme(themeMode)
|
|
102
|
+
|
|
103
|
+
root.dataset.theme = themeMode
|
|
104
|
+
setResolvedTheme(nextResolvedTheme)
|
|
105
|
+
writeLocal(THEME_STORAGE_KEY, themeMode)
|
|
106
|
+
themeColor?.setAttribute('content', nextResolvedTheme === 'dark' ? '#0e1116' : '#f4f6f8')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
applyTheme()
|
|
110
|
+
|
|
111
|
+
if (themeMode !== 'system' || typeof window.matchMedia !== 'function') return undefined
|
|
112
|
+
|
|
113
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
114
|
+
media.addEventListener('change', applyTheme)
|
|
115
|
+
return () => media.removeEventListener('change', applyTheme)
|
|
116
|
+
}, [themeMode])
|
|
117
|
+
|
|
118
|
+
const logoSrc = logoLoadFailed
|
|
119
|
+
? '/favicon.svg'
|
|
120
|
+
: resolvedTheme === 'dark'
|
|
121
|
+
? '/brand/retiregolden-logo-lockup.png'
|
|
122
|
+
: '/brand/retiregolden-logo-lockup-light.png'
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<ReportBrandingContext.Provider value={reportBranding ?? null}>
|
|
126
|
+
<div className={`app-shell planner-shell${isLanding ? ' app-shell--landing' : ''}`}>
|
|
127
|
+
<a className="skip-link" href="#main-content">
|
|
128
|
+
Skip to content
|
|
129
|
+
</a>
|
|
130
|
+
<header className="app-header">
|
|
131
|
+
<NavLink to="/" className="brand brand-logo-link" end aria-label="RetireGolden home">
|
|
132
|
+
<picture>
|
|
133
|
+
{/* Phones get the compact square mark; it's theme-independent and
|
|
134
|
+
skips loading the lockup PNG on mobile. */}
|
|
135
|
+
<source media="(max-width: 640px)" srcSet="/favicon.svg" />
|
|
136
|
+
<img
|
|
137
|
+
className="brand-logo"
|
|
138
|
+
src={logoSrc}
|
|
139
|
+
alt="RetireGolden"
|
|
140
|
+
onError={() => setLogoLoadFailed(true)}
|
|
141
|
+
/>
|
|
142
|
+
</picture>
|
|
143
|
+
</NavLink>
|
|
144
|
+
{/* No hamburger at any width (owner preference): on narrow screens the
|
|
145
|
+
nav shares the logo row and the theme switcher wraps below, keeping
|
|
146
|
+
visual order = DOM order = tab order. */}
|
|
147
|
+
<div className="header-menu" id="header-menu">
|
|
148
|
+
<nav className="nav" aria-label="Primary">
|
|
149
|
+
<NavLink to="/" className={navClass} end>
|
|
150
|
+
Planner
|
|
151
|
+
</NavLink>
|
|
152
|
+
<NavLink to="/examples" className={navClass}>
|
|
153
|
+
Examples
|
|
154
|
+
</NavLink>
|
|
155
|
+
<NavLink to="/learn" className={navClass}>
|
|
156
|
+
Learn
|
|
157
|
+
</NavLink>
|
|
158
|
+
<NavLink to="/disclaimer" className={navClass}>
|
|
159
|
+
Disclaimer
|
|
160
|
+
</NavLink>
|
|
161
|
+
</nav>
|
|
162
|
+
<div className="theme-switcher" role="group" aria-label="Color theme">
|
|
163
|
+
{THEME_MODES.map((mode) => (
|
|
164
|
+
<button
|
|
165
|
+
key={mode}
|
|
166
|
+
className="theme-switcher-button"
|
|
167
|
+
type="button"
|
|
168
|
+
aria-pressed={themeMode === mode}
|
|
169
|
+
onClick={() => setThemeMode(mode)}
|
|
170
|
+
>
|
|
171
|
+
{mode[0].toUpperCase() + mode.slice(1)}
|
|
172
|
+
</button>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</header>
|
|
177
|
+
<main className="app-main" id="main-content" tabIndex={-1}>
|
|
178
|
+
<RouteErrorBoundary>
|
|
179
|
+
<Routes>
|
|
180
|
+
<Route path="/" element={<PlanPickerPage />} />
|
|
181
|
+
<Route
|
|
182
|
+
path="/examples"
|
|
183
|
+
element={
|
|
184
|
+
<Suspense fallback={<RouteFallback />}>
|
|
185
|
+
<ExamplesPage />
|
|
186
|
+
</Suspense>
|
|
187
|
+
}
|
|
188
|
+
/>
|
|
189
|
+
<Route
|
|
190
|
+
path="/compare"
|
|
191
|
+
element={
|
|
192
|
+
<Suspense fallback={<RouteFallback />}>
|
|
193
|
+
<ComparePlansPage />
|
|
194
|
+
</Suspense>
|
|
195
|
+
}
|
|
196
|
+
/>
|
|
197
|
+
<Route
|
|
198
|
+
path="/plan/*"
|
|
199
|
+
element={
|
|
200
|
+
<Suspense fallback={<RouteFallback />}>
|
|
201
|
+
<PlanRoutes />
|
|
202
|
+
</Suspense>
|
|
203
|
+
}
|
|
204
|
+
/>
|
|
205
|
+
<Route
|
|
206
|
+
path="/learn/*"
|
|
207
|
+
element={
|
|
208
|
+
<Suspense fallback={<RouteFallback />}>
|
|
209
|
+
<LearnRoutes />
|
|
210
|
+
</Suspense>
|
|
211
|
+
}
|
|
212
|
+
/>
|
|
213
|
+
<Route
|
|
214
|
+
path="/import"
|
|
215
|
+
element={
|
|
216
|
+
<Suspense fallback={<RouteFallback />}>
|
|
217
|
+
<ImportPage />
|
|
218
|
+
</Suspense>
|
|
219
|
+
}
|
|
220
|
+
/>
|
|
221
|
+
<Route path="/disclaimer" element={<DisclaimerPage />} />
|
|
222
|
+
<Route
|
|
223
|
+
path="/how-tested"
|
|
224
|
+
element={
|
|
225
|
+
<Suspense fallback={<RouteFallback />}>
|
|
226
|
+
<HowTestedPage />
|
|
227
|
+
</Suspense>
|
|
228
|
+
}
|
|
229
|
+
/>
|
|
230
|
+
{/* Retired v1 routes now redirect into the planner. */}
|
|
231
|
+
<Route path="/legacy" element={<Navigate to="/" replace />} />
|
|
232
|
+
<Route path="/longevity" element={<Navigate to="/" replace />} />
|
|
233
|
+
<Route path="/social-security" element={<Navigate to="/" replace />} />
|
|
234
|
+
</Routes>
|
|
235
|
+
</RouteErrorBoundary>
|
|
236
|
+
</main>
|
|
237
|
+
<footer className="app-footer">
|
|
238
|
+
<span className="muted small">
|
|
239
|
+
Educational only — not tax, legal, financial, or medical advice. All data stays on this device.{' '}
|
|
240
|
+
<NavLink to="/disclaimer">Full disclaimer</NavLink>
|
|
241
|
+
</span>
|
|
242
|
+
</footer>
|
|
243
|
+
</div>
|
|
244
|
+
</ReportBrandingContext.Provider>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
|
2
|
+
import { Link } from 'react-router-dom'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children: ReactNode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface State {
|
|
9
|
+
error: Error | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class RouteErrorBoundary extends Component<Props, State> {
|
|
13
|
+
state: State = { error: null }
|
|
14
|
+
|
|
15
|
+
static getDerivedStateFromError(error: Error): State {
|
|
16
|
+
return { error }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
componentDidCatch(error: Error, info: ErrorInfo): void {
|
|
20
|
+
console.error('Route render error:', error, info.componentStack)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
render(): ReactNode {
|
|
24
|
+
if (this.state.error) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="error-boundary-fallback" role="alert">
|
|
27
|
+
<h1>Something went wrong</h1>
|
|
28
|
+
<p className="muted">
|
|
29
|
+
This page hit an unexpected error. You can try reloading, or go back to the home page.
|
|
30
|
+
</p>
|
|
31
|
+
<pre className="error-boundary-stack">{this.state.error.message}</pre>
|
|
32
|
+
<p>
|
|
33
|
+
<button type="button" className="btn btn-primary" onClick={() => window.location.reload()}>
|
|
34
|
+
Reload
|
|
35
|
+
</button>{' '}
|
|
36
|
+
<Link className="btn btn-secondary" to="/">
|
|
37
|
+
Home
|
|
38
|
+
</Link>
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
return this.props.children
|
|
44
|
+
}
|
|
45
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
|