Whealthy Simplified — Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Rebuild Whealthy as a story-driven “answer first” wealth longevity calculator with simplified scope and Unicorn.Land design system integration, deploying to whealthy.unicorn.land.
Architecture: Rebuild the UI layer in-place within the existing whealthy/ directory. Keep the simulation engine (lib/simulation.ts, lib/tax.ts) untouched. Create a simplified params interface that maps to the full Params type internally. Replace all UI components with the new answer-first design. Switch deployment from GitHub Pages to Cloudflare Pages.
Tech Stack: Next.js 16 (static export), React 19, TypeScript, Tailwind 4 with Unicorn.Land theme tokens, Recharts, Zustand 5, Zod
Task 1: Design System Integration — Fonts & Tokens
Files:
- Create: whealthy/src/app/fonts.ts
- Modify: whealthy/src/app/globals.css
- Modify: whealthy/src/app/layout.tsx
- Modify: whealthy/tailwind.config.ts
Step 1: Install Outfit font
Currently uses Geist. Switch to Outfit (Google Fonts) + keep JetBrains Mono.
Create whealthy/src/app/fonts.ts:
import { Outfit, JetBrains_Mono } from "next/font/google";
export const outfit = Outfit({
subsets: ["latin"],
variable: "--font-outfit",
display: "swap",
});
export const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-mono",
display: "swap",
});
Step 2: Replace globals.css with Unicorn.Land tokens
Replace the entire CSS custom properties section in whealthy/src/app/globals.css with the --ul-* token system from design-system/css/unicorn-land.css. Key mappings:
@import "tailwindcss";
:root {
/* Unicorn.Land Opal Scale */
--ul-opal-50: #eefbf8;
--ul-opal-100: #d5f5ef;
--ul-opal-200: #aeebe0;
--ul-opal-300: #80dbce;
--ul-opal-400: #56dbd0;
--ul-opal-500: #2fb8ad;
--ul-opal-600: #0d9488;
--ul-opal-700: #0f766e;
--ul-opal-800: #115e59;
--ul-opal-900: #134e4a;
/* Unicorn.Land Stone Neutrals (warm, NOT blue-gray) */
--ul-stone-50: #fafaf9;
--ul-stone-100: #f5f5f4;
--ul-stone-200: #e7e5e4;
--ul-stone-300: #d6d3d1;
--ul-stone-400: #a8a29e;
--ul-stone-500: #78716c;
--ul-stone-600: #57534e;
--ul-stone-700: #44403c;
--ul-stone-800: #292524;
--ul-stone-900: #1c1917;
/* Semantic */
--ul-accent: #56dbd0;
--ul-accent-hover: #2fb8ad;
--ul-bg: #fafaf9;
--ul-bg-surface: #f5f5f4;
--ul-border: #e7e5e4;
--ul-text: #1c1917;
--ul-text-secondary: #78716c;
--ul-text-muted: #a8a29e;
--ul-error: #dc2626;
--ul-warning: #ca8a04;
/* Whealthy-specific */
--wh-coral: #f87171;
--wh-coral-muted: #fecaca;
--wh-gold: #ca8a04;
--wh-gold-muted: #fef3c7;
/* Typography */
--ul-font-sans: "Outfit", ui-sans-serif, system-ui, sans-serif;
--ul-font-mono: "JetBrains Mono", ui-monospace, monospace;
/* Spacing */
--ul-radius-sm: 4px;
--ul-radius-md: 6px;
--ul-radius-lg: 8px;
--ul-radius-xl: 12px;
/* Shadows */
--ul-shadow-sm: 0 1px 3px rgba(28, 25, 23, 0.06), 0 1px 2px rgba(28, 25, 23, 0.04);
--ul-shadow-md: 0 4px 6px rgba(28, 25, 23, 0.06), 0 2px 4px rgba(28, 25, 23, 0.04);
/* Rainbow shimmer gradient */
--ul-rainbow: linear-gradient(135deg, #ff6b9d, #ff8a65, #ffd54f, #81c784, #64b5f6, #ba68c8);
}
@media (prefers-color-scheme: dark) {
:root {
--ul-bg: #1c1917;
--ul-bg-surface: #292524;
--ul-border: #44403c;
--ul-text: #f5f5f4;
--ul-text-secondary: #a8a29e;
--ul-text-muted: #78716c;
}
}
/* Rainbow shimmer animation */
@keyframes rainbow-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.rainbow-shimmer {
background: var(--ul-rainbow);
background-size: 300% 300%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: rainbow-shift 6s ease infinite;
}
/* Base styles */
body {
font-family: var(--ul-font-sans);
background: var(--ul-bg);
color: var(--ul-text);
}
Step 3: Update layout.tsx
Replace Geist fonts with Outfit + JetBrains Mono imports from fonts.ts. Update <html> className to use the new font variables. Update metadata title to “Whealthy — Wealth Longevity Planner” and description.
Step 4: Update tailwind.config.ts
Extend theme with Unicorn.Land color tokens (opal scale, stone scale, semantic aliases) and font families. Map --ul-* vars to Tailwind utilities so we can use bg-opal-400, text-stone-900, etc.
Step 5: Commit
git add -A && git commit -m "feat: integrate Unicorn.Land design tokens, swap to Outfit + JetBrains Mono"
Task 2: Simplified Types & Defaults
Files:
- Create: whealthy/src/lib/simplified-types.ts
- Create: whealthy/src/lib/simplified-defaults.ts
Step 1: Define simplified params interface
Create whealthy/src/lib/simplified-types.ts. This is the user-facing interface — much simpler than the full Params type. It maps to Params internally.
import { z } from "zod";
export const simplifiedParamsSchema = z.object({
// Mode
calculationMode: z.enum(["forward", "reverse"]).default("reverse"),
// Core (the editable sentence)
currentAge: z.number().min(18).max(100).default(35),
deathAge: z.number().min(30).max(120).default(85),
annualSpending: z.number().min(0).default(80000),
currency: z.string().default("$"),
// Mode-specific
startWealth: z.number().min(0).default(1000000), // forward mode input
desiredTerminalWealth: z.number().min(0).default(500000), // reverse mode input
// Spending rule
spendingRule: z.enum(["fixed", "%wealth", "guardrails"]).default("fixed"),
expenseInflation: z.number().min(0).max(0.2).default(0.025),
spendPctWealth: z.number().min(0).max(1).default(0.04),
guardrailTarget: z.number().min(0).max(1).default(0.04),
guardrailMinChange: z.number().min(-1).max(0).default(-0.025),
guardrailMaxChange: z.number().min(0).max(1).default(0.05),
// Returns (historical defaults)
equityReturn: z.number().default(0.07),
equityWeight: z.number().min(0).max(1).default(0.60),
cashReturn: z.number().default(0.02),
cashWeight: z.number().min(0).max(1).default(0.40),
equityVolatility: z.number().default(0.16),
cashVolatility: z.number().default(0.01),
dividendYield: z.number().default(0.02),
// Tax
taxJurisdiction: z.enum(["germany", "us", "uk", "custom"]).default("us"),
effectiveTaxRate: z.number().min(0).max(1).default(0.21), // for custom
dividendTaxRate: z.number().min(0).max(1).default(0.21),
capitalGainsTaxRate: z.number().min(0).max(1).default(0.21),
interestTaxRate: z.number().min(0).max(1).default(0.21),
// Monte Carlo
runMonteCarlo: z.boolean().default(false),
numPaths: z.number().min(100).max(2000).default(500),
});
export type SimplifiedParams = z.infer<typeof simplifiedParamsSchema>;
Step 2: Create defaults and mapping function
Create whealthy/src/lib/simplified-defaults.ts. Export DEFAULT_SIMPLIFIED_PARAMS and a toFullParams(simplified: SimplifiedParams): Params function that maps simplified params to the full Params type expected by the simulation engine. Fill in all the complex fields the engine needs (private equity disabled, philanthropy zeroed, real mode false, etc.).
import type { Params } from "./types";
import type { SimplifiedParams } from "./simplified-types";
export const DEFAULT_SIMPLIFIED_PARAMS: SimplifiedParams = {
calculationMode: "reverse",
currentAge: 35,
deathAge: 85,
annualSpending: 80000,
currency: "$",
startWealth: 1000000,
desiredTerminalWealth: 500000,
spendingRule: "fixed",
expenseInflation: 0.025,
spendPctWealth: 0.04,
guardrailTarget: 0.04,
guardrailMinChange: -0.025,
guardrailMaxChange: 0.05,
equityReturn: 0.07,
equityWeight: 0.60,
cashReturn: 0.02,
cashWeight: 0.40,
equityVolatility: 0.16,
cashVolatility: 0.01,
dividendYield: 0.02,
taxJurisdiction: "us",
effectiveTaxRate: 0.21,
dividendTaxRate: 0.21,
capitalGainsTaxRate: 0.21,
interestTaxRate: 0.21,
runMonteCarlo: false,
numPaths: 500,
};
export function toFullParams(s: SimplifiedParams): Params {
// Map simplified → full Params for the simulation engine
// Sets all omitted fields to safe defaults (no private equity, no philanthropy, no one-offs)
return {
currency: s.currency,
calculationMode: s.calculationMode,
startWealth: s.startWealth,
liquidShare: 1, // all liquid (no private equity)
currentAge: s.currentAge,
deathAge: s.deathAge,
spendingRule: s.spendingRule,
annualExpenseNow: s.annualSpending,
expenseInflation: s.expenseInflation,
spendPctWealth: s.spendPctWealth,
guardrails: {
targetPctWealth: s.guardrailTarget,
minChange: s.guardrailMinChange,
maxChange: s.guardrailMaxChange,
},
philanthropyMode: "fixed",
philanthropyFixedNow: 0,
philanthropyPercent: 0,
taxJurisdiction: s.taxJurisdiction === "custom" ? "custom" : s.taxJurisdiction,
taxResidences: [s.taxJurisdiction === "custom" ? "us" : s.taxJurisdiction],
taxResidenceWeights: { [s.taxJurisdiction === "custom" ? "us" : s.taxJurisdiction]: 1 },
doubleTaxRelief: 0,
taxInterest: s.interestTaxRate,
taxDividends: s.dividendTaxRate,
taxRealizedGains: s.capitalGainsTaxRate,
holdingCompanyStructure: {
publicDividendSource: s.taxJurisdiction === "germany" ? "germany" : "us",
privateDividendSource: "us",
usOwnershipPct: 0,
germanOwnershipPct: 0,
isVermoegensverwaltend: false,
germanTradeTaxRate: 0,
usCorporateTaxRate: 0.21,
},
assetAlloc: { public: s.equityWeight * 100, private: 0, cash: s.cashWeight * 100 },
assetReturn: { public: s.equityReturn, private: 0, cash: s.cashReturn },
assetVol: { public: s.equityVolatility, private: 0, cash: s.cashVolatility },
publicDivYield: s.dividendYield,
publicRealizationRate: 0.25,
privateRealizationRate: 0,
privCommit: { enabled: false, totalCommitment: 0, callYears: 0, distLagYears: 0, distYears: 0, distMultiple: 0 },
runMonteCarlo: s.runMonteCarlo,
numPaths: s.numPaths,
oneOffs: [],
desiredTerminalWealth: s.desiredTerminalWealth,
lifetimeSpendingTotal: 0,
realMode: false,
};
}
Note: The exact field names and structure must match the existing Params type in lib/types.ts. Verify against the actual type definition during implementation and adjust as needed.
Step 3: Run existing tests to make sure nothing breaks
Run: cd whealthy && npx vitest run
Expected: All existing tests pass (we haven’t touched engine files).
Step 4: Commit
git add src/lib/simplified-types.ts src/lib/simplified-defaults.ts
git commit -m "feat: add simplified params interface with mapping to full engine params"
Task 3: Simplified Zustand Store
Files:
- Create: whealthy/src/store/useSimplifiedStore.ts
Step 1: Write the store
New Zustand store using SimplifiedParams. Persists to localStorage under a new key ("whealthy-v2-params"). Provides setParam, updateParams, reset. No JSON import/export, no presets, no one-offs.
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { SimplifiedParams } from "@/lib/simplified-types";
import { DEFAULT_SIMPLIFIED_PARAMS } from "@/lib/simplified-defaults";
type SimplifiedStore = {
params: SimplifiedParams;
setParam: <K extends keyof SimplifiedParams>(key: K, value: SimplifiedParams[K]) => void;
updateParams: (patch: Partial<SimplifiedParams>) => void;
reset: () => void;
};
export const useSimplifiedStore = create<SimplifiedStore>()(
persist(
(set) => ({
params: DEFAULT_SIMPLIFIED_PARAMS,
setParam: (key, value) =>
set((state) => ({ params: { ...state.params, [key]: value } })),
updateParams: (patch) =>
set((state) => ({ params: { ...state.params, ...patch } })),
reset: () => set({ params: DEFAULT_SIMPLIFIED_PARAMS }),
}),
{
name: "whealthy-v2-params",
storage: createJSONStorage(() => localStorage),
}
)
);
Step 2: Commit
git add src/store/useSimplifiedStore.ts
git commit -m "feat: add simplified Zustand store with localStorage persistence"
Task 4: Hero Component — The Answer + Editable Sentence
Files:
- Create: whealthy/src/components/hero.tsx
Step 1: Build the Hero component
The hero is the centerpiece. It shows: - The big number (rainbow shimmer when healthy, opal when neutral, coral when concerning) - The editable sentence with inline number inputs - The mode toggle (reverse ↔ forward)
Props:
type HeroProps = {
mode: "forward" | "reverse";
answer: number; // the computed answer (required wealth or ruin age)
answerLabel: string; // "You need" or "Your money lasts until age"
currency: string;
currentAge: number;
deathAge: number;
annualSpending: number;
desiredTerminalWealth: number;
startWealth: number;
ruinProbability?: number; // from Monte Carlo, drives visual state
onModeChange: (mode: "forward" | "reverse") => void;
onParamChange: <K extends keyof SimplifiedParams>(key: K, value: SimplifiedParams[K]) => void;
};
The hero number uses font-mono (JetBrains Mono) at 4rem/64px bold. Apply .rainbow-shimmer class when ruinProbability is undefined or < 0.1, text-[--ul-accent] when 0.1–0.3, text-[--wh-coral] when > 0.3.
Inline editable values: render as <span> with contentEditable or as a slim <input type="number"> styled to look like text (no border, underline on hover, font-mono). The sentence reads:
Reverse mode: “I’m [35], I want to die at [85], I spend [$80,000]/year, and I want [$500,000] left.”
Forward mode: “I’m [35] with [$1,000,000], I spend [$80,000]/year, and I want to know what happens by age [85].”
Mode toggle: small pill toggle below the sentence, text-sm, stone-500 text, opal highlight on active side.
Step 2: Commit
git add src/components/hero.tsx
git commit -m "feat: add Hero component with answer-first display and editable sentence"
Task 5: Key Stats Strip
Files:
- Create: whealthy/src/components/key-stats-strip.tsx
Step 1: Build the KeyStatsStrip component
Horizontal row of 3-4 stat cards between hero and charts. Each stat: JetBrains Mono number + Outfit label + optional (i) tooltip.
Stats shown depend on mode: - Reverse: Required wealth (hero handles this), total lifetime spending, total lifetime taxes, effective annual return - Forward: Terminal wealth or ruin age, total lifetime spending, total lifetime taxes, effective annual return - If Monte Carlo on: Ruin probability replaces one stat
Each stat is a <div> with font-mono text-xl font-bold for the number and text-sm text-[--ul-text-secondary] for the label.
Step 2: Commit
git add src/components/key-stats-strip.tsx
git commit -m "feat: add KeyStatsStrip component"
Task 6: Wealth Timeline Chart
Files:
- Create: whealthy/src/components/wealth-timeline.tsx
Step 1: Build the WealthTimeline chart
Recharts ComposedChart (or AreaChart) that handles both deterministic and Monte Carlo rendering in one component.
Props:
type WealthTimelineProps = {
currency: string;
deterministicData: { age: number; wealth: number }[];
monteCarloData?: { age: number; p5: number; p25: number; p50: number; p75: number; p95: number }[];
targetWealth?: number; // horizontal dashed line (reverse mode)
showMonteCarlo: boolean;
};
Deterministic mode: Single <Area> with opal stroke and translucent opal fill (fill="var(--ul-opal-400)", fillOpacity={0.1}).
Monte Carlo mode: Layered <Area> components for each percentile band. p5/p95 at 0.05 opacity, p25/p75 at 0.1, p50 as solid opal line. All fills use opal color at varying opacities.
Target line: <ReferenceLine> with dashed stroke at targetWealth value (or at 0 in forward mode).
Styling: Axes in JetBrains Mono (fontFamily: "var(--ul-font-mono)"), gridlines in var(--ul-stone-200), tooltip with warm stone background.
Step 2: Commit
git add src/components/wealth-timeline.tsx
git commit -m "feat: add WealthTimeline chart with deterministic and Monte Carlo modes"
Task 7: Breakdown Chart
Files:
- Create: whealthy/src/components/breakdown-chart.tsx
Step 1: Build the BreakdownChart
Recharts AreaChart showing cumulative composition of wealth over time. Stacked areas for: returns (opal), starting wealth carry-forward (gold), spending drain (stone), taxes drain (coral).
Props:
type BreakdownChartProps = {
currency: string;
data: {
age: number;
cumulativeReturns: number;
cumulativeSpending: number;
cumulativeTaxes: number;
wealth: number;
}[];
};
The data needs to be derived from SimulationRow[] — sum up returns, spending, and taxes year over year. Create a helper function deriveBreakdownData(rows: SimulationRow[]) in this file.
Step 2: Commit
git add src/components/breakdown-chart.tsx
git commit -m "feat: add BreakdownChart showing cumulative returns/spending/taxes"
Task 8: Tooltip Component
Files:
- Create: whealthy/src/components/info-tooltip.tsx
Step 1: Build the InfoTooltip component
Wraps Radix Tooltip. Shows a small (i) icon in text-[--ul-text-muted] that shows a popover on hover/tap.
type InfoTooltipProps = {
content: string;
};
Renders: <TooltipProvider> → <Tooltip> → <TooltipTrigger> (the (i) icon using lucide-react Info icon, 14px) → <TooltipContent> (warm stone bg, text-sm, max-w-xs, var(--ul-bg-surface) background).
This is used throughout the assumptions section. Keep it simple.
Step 2: Commit
git add src/components/info-tooltip.tsx
git commit -m "feat: add InfoTooltip component for inline explainers"
Task 9: Assumptions Section — Returns Card
Files:
- Create: whealthy/src/components/assumptions/returns-card.tsx
- Create: whealthy/src/components/assumptions/assumption-card.tsx
Step 1: Build the shared AssumptionCard wrapper
A collapsible card component. Closed by default. Click header to expand. Flat white background, 1px var(--ul-border) border, no shadow. Smooth max-height or grid-rows transition.
type AssumptionCardProps = {
title: string;
children: React.ReactNode;
defaultOpen?: boolean;
};
Step 2: Build the ReturnsCard
Shows asset allocation (equity % / cash %) and expected returns with historical defaults. Each input is a slider + number display. Include (i) tooltips explaining: - What equity returns mean historically (~7% nominal long-term average for diversified stocks) - What cash returns mean (~2% nominal, savings/bonds) - What allocation means (how your money is split) - What dividend yield means - What volatility means (for Monte Carlo)
Step 3: Commit
git add src/components/assumptions/
git commit -m "feat: add AssumptionCard wrapper and ReturnsCard"
Task 10: Assumptions Section — Tax Card
Files:
- Create: whealthy/src/components/assumptions/tax-card.tsx
Step 1: Build the TaxCard
Top-level: jurisdiction picker (dropdown: US, Germany, UK, Custom). When a jurisdiction is selected, auto-fills the tax rates from taxProfiles.ts.
Simple view: single “effective tax rate” display (read-only, computed as weighted average of the three rates).
Expandable detail: three separate rate inputs — dividend tax, capital gains tax, interest tax. Editable when jurisdiction is “custom”, read-only (with override option) for preset jurisdictions.
Tooltips explaining: - What dividend tax is (tax on income from stock dividends) - What capital gains tax is (tax on profit when you sell investments) - What interest tax is (tax on income from cash/bonds) - What each jurisdiction assumes
Step 2: Commit
git add src/components/assumptions/tax-card.tsx
git commit -m "feat: add TaxCard with jurisdiction picker and rate breakdown"
Task 11: Assumptions Section — Spending & Monte Carlo Cards
Files:
- Create: whealthy/src/components/assumptions/spending-card.tsx
- Create: whealthy/src/components/assumptions/monte-carlo-card.tsx
Step 1: Build SpendingCard
Spending rule selector (fixed / % of wealth / guardrails). Each mode shows relevant inputs. Fixed shows inflation rate. % of wealth shows the percentage. Guardrails shows target %, min change, max change.
Tooltips explaining each mode and inflation.
Step 2: Build MonteCarloCard
Toggle switch to enable/disable. When enabled: slider for number of simulations (100–2,000). Brief explanation of what Monte Carlo does (“runs thousands of scenarios with randomized returns to show the range of possible outcomes”).
Ruin probability stat shown inline when enabled.
Step 3: Commit
git add src/components/assumptions/spending-card.tsx src/components/assumptions/monte-carlo-card.tsx
git commit -m "feat: add SpendingCard and MonteCarloCard assumption panels"
Task 12: Main Page — Wire Everything Together
Files:
- Rewrite: whealthy/src/app/page.tsx
Step 1: Build the new page
Replace the entire page.tsx with the new layout. The page orchestrates:
- Read params from
useSimplifiedStore - Debounce params (150ms) via
useDebouncedValue - Run simulation via
useTransition— calltoFullParams()thensimulateDeterministic()/calculateReverse()/simulateMonteCarlo()as appropriate - Render the three zones:
<main className="max-w-5xl mx-auto px-6 py-12">
{/* Zone 1: Hero */}
<Hero ... />
{/* Zone 2: Visualization */}
<KeyStatsStrip ... />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-8">
<WealthTimeline ... />
<BreakdownChart ... />
</div>
{/* Zone 3: Assumptions */}
<section className="mt-12 grid grid-cols-1 md:grid-cols-2 gap-4">
<ReturnsCard ... />
<TaxCard ... />
<SpendingCard ... />
<MonteCarloCard ... />
</section>
{/* Footer */}
<footer className="mt-16 text-center">
<unicorn-footer></unicorn-footer>
</footer>
</main>
Step 2: Verify it builds
Run: cd whealthy && npm run build
Expected: Static export succeeds.
Step 3: Commit
git add src/app/page.tsx
git commit -m "feat: wire up answer-first page layout with all components"
Task 13: Delete Unused Components
Files:
- Delete: whealthy/src/components/params-form.tsx
- Delete: whealthy/src/components/deterministic-chart.tsx
- Delete: whealthy/src/components/monte-carlo-chart.tsx
- Delete: whealthy/src/components/key-stats.tsx
- Delete: whealthy/src/components/results-table.tsx
- Delete: whealthy/src/components/events-table.tsx
- Delete: whealthy/src/components/persist-controls.tsx
- Delete: whealthy/src/components/__tests__/ (snapshot tests for old charts)
- Keep: whealthy/src/components/ui/ (button, input, label, switch, tabs, tooltip — still used)
Step 1: Remove old components
Delete the files listed above. Keep ui/ primitives.
Step 2: Remove old store if no longer imported
Check if anything still imports useParamsStore. If not, delete whealthy/src/store/useParamsStore.ts. Keep it if the engine tests reference it.
Step 3: Verify build
Run: cd whealthy && npm run build
Expected: Builds cleanly with no import errors.
Step 4: Run tests
Run: cd whealthy && npx vitest run
Expected: Engine tests still pass. Old snapshot tests (if deleted) no longer run.
Step 5: Commit
git add -A
git commit -m "chore: remove old UI components replaced by answer-first design"
Task 14: Footer — Made in Unicorn.Land
Files:
- Modify: whealthy/src/app/layout.tsx
Step 1: Add the footer
Either import the <unicorn-footer> web component (load the script from the design system) or build a simple React footer component that replicates it: “Made in Unicorn.Land” text with the rainbow shimmer on the link, Outfit uppercase at 0.75rem.
The simplest approach is a React component since we’re already in React. Use the .rainbow-shimmer class defined in globals.css.
function Footer() {
return (
<footer className="mt-16 py-8 text-center">
<span className="text-xs uppercase tracking-widest text-[--ul-text-muted]">
Made in{" "}
<a href="https://unicorn.land" className="rainbow-shimmer font-semibold">
Unicorn.Land
</a>
</span>
</footer>
);
}
Add <Footer /> to the layout or page.
Step 2: Commit
git add src/app/layout.tsx
git commit -m "feat: add Made in Unicorn.Land footer with rainbow shimmer"
Task 15: Deployment — Cloudflare Pages
Files:
- Modify: whealthy/next.config.ts
- Create: whealthy/wrangler.toml (if using Wrangler) or configure via Cloudflare dashboard
Step 1: Update next.config.ts
Remove the GitHub Pages conditional (basePath, assetPrefix for GITHUB_PAGES). Keep static export (output: "export"). The out/ directory is the publish root for Cloudflare Pages.
Step 2: Configure Cloudflare Pages
Create a simple config or document the dashboard setup:
- Project name: whealthy
- Build command: npm run build
- Build output: out
- Custom domain: whealthy.unicorn.land
- Environment variables: none needed (all client-side)
Step 3: Verify build output
Run: cd whealthy && npm run build && ls out/
Expected: Static HTML + assets in out/ directory.
Step 4: Commit
git add next.config.ts wrangler.toml
git commit -m "feat: configure Cloudflare Pages deployment for whealthy.unicorn.land"
Task 16: Responsive Polish & Dark Mode Verification
Files: - Modify: various component files as needed
Step 1: Test responsive breakpoints
Run npm run dev and test at:
- Mobile (375px): hero number should be ~2.5rem, single column stack, charts full-width
- Tablet (768px): two-column assumptions grid
- Desktop (1024px+): full layout — charts side by side, assumptions 2-col
Step 2: Test dark mode
Toggle system preference to dark. Verify: - Background switches to stone-900 - Text switches to stone-100 - Cards use stone-800 background - Charts use appropriate dark gridlines - Rainbow shimmer still works on dark background - Opal accent is still visible
Step 3: Fix any issues found
Adjust Tailwind classes and CSS variables as needed.
Step 4: Commit
git add -A
git commit -m "fix: responsive layout and dark mode polish"
Task 17: Tooltip Content — Write All Explainers
Files:
- Create: whealthy/src/lib/tooltips.ts
Step 1: Write tooltip content
Create a single file with all tooltip strings. This keeps copy centralized and easy to update.
export const TOOLTIPS = {
// Returns
equityReturn: "The average annual return you expect from stocks. Historically, a diversified stock portfolio has returned about 7% per year after inflation.",
cashReturn: "The return on cash and bonds. Typically 1-3% per year — safer but lower growth.",
equityWeight: "What percentage of your money is in stocks vs. cash. Higher stock allocation means higher expected returns but more volatility.",
dividendYield: "The portion of stock returns paid as dividends each year. Dividends are taxed differently than capital gains in most jurisdictions.",
volatility: "How much returns vary year to year. Higher volatility means your actual returns could differ significantly from the average. Used in Monte Carlo simulations.",
// Tax
dividendTax: "Tax on income received from stock dividends. Rates vary by country — typically 15-30%.",
capitalGainsTax: "Tax on profit when investments are sold. Only applies to realized gains — unrealized appreciation is not taxed.",
interestTax: "Tax on income from cash, savings, and bonds.",
jurisdiction: "Select your tax jurisdiction to auto-fill rates. Choose 'Custom' to set your own rates.",
// Spending
fixedSpending: "You spend a fixed amount each year, adjusted for inflation. Simple and predictable.",
pctWealthSpending: "You spend a percentage of your current wealth each year. Spending rises and falls with your portfolio — you'll never run out, but spending may drop significantly in bad years.",
guardrailsSpending: "A hybrid approach: target a percentage of wealth, but limit how much your spending can change year-to-year. Smooths out the volatility of percentage-based spending.",
inflation: "How much your spending increases each year due to rising prices. Historical average is about 2-3%.",
// Monte Carlo
monteCarlo: "Instead of assuming fixed returns every year, Monte Carlo runs hundreds of scenarios with randomized returns. This shows you the range of possible outcomes — from lucky to unlucky.",
ruinProbability: "The percentage of simulated scenarios where your money runs out before you die. Lower is better.",
numPaths: "How many random scenarios to simulate. More paths = more accurate probability estimates, but slower to compute.",
} as const;
Step 2: Wire tooltips into assumption cards
Update each assumption card component to import from TOOLTIPS and pass to <InfoTooltip>.
Step 3: Commit
git add src/lib/tooltips.ts src/components/assumptions/
git commit -m "feat: add tooltip explainer content for all assumptions"
Task 18: Final Integration Test
Step 1: Full build
Run: cd whealthy && npm run build
Expected: Clean static export, no errors.
Step 2: Run all tests
Run: cd whealthy && npx vitest run
Expected: All engine tests pass.
Step 3: Manual smoke test
Run: cd whealthy && npm run dev
Verify: - [ ] Page loads with reverse mode default - [ ] Hero shows a computed “You need $X” number - [ ] Editing sentence values updates the answer - [ ] Toggling to forward mode changes the sentence and answer - [ ] Wealth timeline chart renders - [ ] Breakdown chart renders - [ ] Key stats strip shows 4 stats - [ ] Each assumption card opens/closes - [ ] Changing return rates updates the answer - [ ] Changing tax jurisdiction updates rates - [ ] Enabling Monte Carlo shows probability bands - [ ] Dark mode works - [ ] Mobile layout stacks correctly - [ ] Rainbow shimmer on hero number when healthy - [ ] “Made in Unicorn.Land” footer shows rainbow shimmer - [ ] Page persists state to localStorage (reload preserves inputs)
Step 4: Commit any final fixes
git add -A
git commit -m "fix: final integration polish"