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