npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@retiregolden/planner-ui

v0.4.0

Published

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 requir

Readme

@retiregolden/planner-ui

The RetireGolden planner React UI — the complete planner a host application composes inside its own router: plan picker and workspace, results/Monte Carlo/scenarios/survivor/relocation pages, the Roth & Tax / spending / Social Security optimizers, the import wizard, the Learning Center, report export, and browser persistence (IndexedDB + localStorage). All money math lives in @retiregolden/engine (a dependency of this package, not re-exported by it); this package is the UI and persistence around it.

Source of truth: github.com/RetireGolden/RetireGolden (packages/planner-ui). Engineering docs live in the repo's DOCS/.

What ships, and the bundler contract

This package publishes its TypeScript source (src/, referenced directly by exports and types) and requires a Vite-class bundler — it is not consumable from plain Node or from bundlers that don't implement Vite semantics. The planner tree relies on features only a Vite-style build provides:

  • new Worker(new URL('./x.worker.ts', import.meta.url), { type: 'module' }) for the Monte Carlo pool, the optimizers, and relocation compare;
  • import wasmUrl from 'highs/runtime?url' for the HiGHS LP-solver wasm;
  • import.meta.glob, import.meta.env.DEV, CSS and image imports.

Compiling to plain ESM would not remove any of those requirements, so the package doesn't pretend otherwise. Known-good consumers: Vite ≥ 8 and electron-vite, with React 19 and react-router v7 (peers).

Required Vite config

Dependency pre-bundling must skip this package (esbuild/rolldown pre-bundles break the Vite-only constructs above) while still pre-bundling its CommonJS-flavoured dependencies for dev:

// vite.config.ts
export default defineConfig({
  plugins: [react()],
  optimizeDeps: {
    exclude: ['@retiregolden/planner-ui'],
    include: ['@retiregolden/planner-ui > highs', '@retiregolden/planner-ui > recharts'],
  },
})

Production builds work without this; dev mode does not — leave it in place. TypeScript consumers should use "moduleResolution": "bundler" and include vite/client types (standard in Vite templates), since type checking reads this package's source.

Usage

npm install @retiregolden/planner-ui react react-dom react-router-dom

The host owns the router and mounts the planner under it:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import '@retiregolden/planner-ui/index.css'
import { PlannerApp } from '@retiregolden/planner-ui'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <PlannerApp />
    </BrowserRouter>
  </StrictMode>,
)

<PlannerApp /> is everything inside the router: the app chrome (header, nav, theme toggle, footer), the route table, and the route error boundary. Use BrowserRouter, HashRouter (Electron file:// hosts), or MemoryRouter as the host requires; a basename on the host's router is respected because the planner only uses relative react-router APIs.

CSS and theming

  • @retiregolden/planner-ui/index.css — required: the design-token layer (CSS custom properties for both light and dark themes) plus base styles. Import it once in the host.
  • Component styles (planner.css, learn.css) are imported by the components themselves; no extra import is needed.

Theming hook: the token layer is plain CSS custom properties on :root (and [data-theme] variants), so a host can re-brand by loading its own stylesheet after index.css and overriding tokens. Keep overrides WCAG-AA-honest — the upstream palette is contrast-guarded by tests.

Host-served static assets (required)

The planner references a small set of root-absolute URLs that the host must serve — they are deliberately not bundled, because several are host-specific by nature. app/public/ in the upstream repo is the reference tree to copy from:

| Path | Used by | Notes | |------|---------|-------| | /favicon.svg | Header brand mark (mobile), logo fallback | | | /brand/retiregolden-logo-lockup.png, /brand/retiregolden-logo-lockup-light.png | Header logo (dark/light) | Missing files degrade to /favicon.svg | | /learn/images/*.webp | Learning Center article illustrations (~5 MB total) | Copy from app/public/learn/images/ | | /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 |

Electron hosts loading over file:// should use HashRouter and a protocol handler (or serve the app over a local scheme) so these root-absolute URLs resolve against the app bundle, not the filesystem root.

Published API surface

The supported product API is:

  • the root exportPlannerApp, the plan-persistence seam (PlanStore, PlanSummary, PlanStoreProvider, indexedDbPlanStore), the read-only capability (readOnly prop + useWorkspaceReadOnly), the route groups (plannerWorkspaceRoutes, plannerContentRoutes, plannerHomeRoutes), and ReportBrandingProvider — see "Hosting the workspace" below;
  • the ./plan-format subpathserializeV2Backup, parseV2Backup, the envelope types, and the kind/version constants. This is the plan interchange format (the same file the web app's backup download produces); its exported names, signatures, and envelope contract only change with a semver-major release. The parser ignores unknown envelope fields, so hosts may extend the envelope with their own top-level keys and the file still imports everywhere. The module is browser-free (no IndexedDB/DOM) and safe to run in Node — e.g. an Electron main process assembling backups;
  • the ./report-model subpath — the edition-neutral report data model: ReportModel and its block types, buildReportModel, the stable REPORT_BLOCK_IDS, serializeReportModel (deterministic JSON), and the CSV table helpers (chartDataCsv, yearLedgerCsv, accountsCsv). See "Report model" under "Hosting the workspace". Like ./plan-format, its exported names, signatures, and block ids only change with a semver-major release (new blocks/fields may be added in minors), and the module is browser-free and safe to run in Node;
  • ./index.css.

The exports map also exposes wildcard ./*.ts subpaths (e.g. ./report/reportHtml) — these exist for the upstream repo's own test and case-runner harnesses, are not covered by any stability promise, and may move or change in any release. If a host needs one of them long-term, open an upstream issue so it can be promoted to a real export instead.

Hosting the workspace

<PlannerApp/> is the batteries-included web composition: chrome, all routes, browser storage. A host with its own plans-management surface (its own library UI, its own chrome) instead mounts parts of the planner and supplies storage. Three hooks make that possible; none of them involve any capability detection — they are plain props, context, and route arrays.

Plan storage: the PlanStore seam

The workspace, Compare, the optimizers, and the import wizard read and write plans through a provider interface:

import type { PlanStore, PlanSummary } from '@retiregolden/planner-ui'

interface PlanStore {
  listPlans(): Promise<PlanSummary[]>   // { id, name, updatedAtIso }
  loadPlan(id: string): Promise<unknown> // stored plan JSON verbatim; null/undefined when absent
  savePlan(plan: Plan): Promise<void>    // already validated + stamped; the autosave path
  deletePlan(id: string): Promise<void>
}

Implementations are storage-dumb by design: loadPlan returns the stored document as-is (any schema version) and planner-ui runs schema migration and Zod validation on it — the same single code path the web app has always used — while savePlan receives a plan that already passed validation and got its updatedAtIso stamp. A store never re-implements plan semantics.

Supply a store with the provider or the planStore prop on <PlannerApp/> (the prop wins when both are present); keep the instance stable — the planner reloads when the store's identity changes:

import { PlanStoreProvider } from '@retiregolden/planner-ui'

<PlanStoreProvider store={myStore}>{/* planner routes */}</PlanStoreProvider>

Omit the provider and the browser IndexedDB implementation applies — it is also exported as indexedDbPlanStore for hosts that want to wrap it.

Deliberate boundaries of the seam:

  • Plan-scoped. No client/household-grouping concepts; a host that keeps per-client libraries maps plan ids to its own structure in its adapter.
  • No change feed. Planner list views refetch after their own mutations, so the interface carries no subscription mechanism.
  • Example demo records never cross it. The example library's editable demo slots (example:* ids) are per-device preview UX and stay in the browser store regardless of provider; "Save to my plans" converts a demo into a user plan and writes that through the seam. Small conveniences (theme, dismissed banners) likewise stay in localStorage.

Read-only mode

The workspace can render read-only: pass readOnly to <PlanStoreProvider> (or <PlannerApp/>). It defaults to false, so omitting it leaves behavior exactly as before — the public web app is unchanged.

<PlanStoreProvider store={myStore} readOnly={!canEdit}>{/* planner routes */}</PlanStoreProvider>

When readOnly is true:

  • the plan cannot mutate — the shared update path is a no-op, so an edit changes nothing on screen and no savePlan is attempted (the store's own throw, below, is never reached). Read-only means no mutation, not merely no autosave, so nothing can silently persist on a later re-enable;
  • plan-editing controls disable — the entry sections (Household, Accounts, Income, …) and the plan-name field render disabled;
  • the discrete write actions hide — duplicate, delete, "Save to my plans", import, and new-plan are unavailable;
  • the explore-page apply/add actions disable — the "Apply", "Add as scenario", "Use", and similar plan-mutating controls on the optimizer, Scenarios, Relocation, Survivor, and Insights pages disable, so those tools stay read/compute-only;
  • read/explore/export keep working — Results, Report, Compare, Monte Carlo, the optimizers' compute + report downloads, and every download/backup path are untouched.

This is a generic, edition-neutral capability: planner-ui knows nothing about why writes are disallowed (entitlements, sign-in, a lapsed subscription). The host decides when to set readOnly and renders its own banner explaining the reason — keep any planner-side text generic. readOnly is the cooperative half of the gate: it stops the planner from attempting writes. The authoritative gate stays the host PlanStoresavePlan is free to throw, and that throw is the backstop if a write is ever attempted anyway.

useWorkspaceReadOnly() reads the same signal inside a host's own chrome mounted under the provider (e.g. to disable a custom toolbar button):

import { useWorkspaceReadOnly } from '@retiregolden/planner-ui'

const readOnly = useWorkspaceReadOnly() // false unless a provider sets it

Route groups

The route table is exported as three react-router v7 RouteObject[] arrays that spread into useRoutes or feed createBrowserRouter. Mount them at the host router's root; to serve the planner under a URL prefix, put the prefix in the router's basename (e.g. <BrowserRouter basename="/planner">) — planner pages navigate with root-absolute paths, which react-router resolves against the basename. Do not nest the groups under a parent route path (path: 'planner/*'): the initial deep link would render, but the first in-app navigation would escape the prefix. Deep links (e.g. /plan/<id>/results) work with only the workspace group mounted:

| Export | Routes | Notes | |--------|--------|-------| | plannerWorkspaceRoutes | plan/:planId/* (sections, results, Monte Carlo, scenarios, survivor, relocation, optimizers, report) + compare | The plan workspace a host wraps in its own chrome | | plannerContentRoutes | examples, learn/*, disclaimer, how-tested | Storage-independent content; workspace pages link into it, so mount it (or redirect those paths) | | plannerHomeRoutes | `` (index), import, retired-route redirects | The web plans-management home — omit it if the host owns plan management |

import { useRoutes } from 'react-router-dom'
import { plannerWorkspaceRoutes, plannerContentRoutes } from '@retiregolden/planner-ui'

function PlannerRoutes() {
  return useRoutes([...plannerWorkspaceRoutes, ...plannerContentRoutes])
}

The groups are chrome-free: no header/nav/footer, no theme toggle, and no document.title management for non-plan routes (plan routes retitle themselves). Workspace pages render links to / ("Your plans") — point that path at your own library surface. Hosts mounting groups directly brand downloaded reports with ReportBrandingProvider (the component form of the reportBranding prop below); <PlannerApp/> remains exactly the composition of all three groups plus the web chrome.

Plan interchange

Use the ./plan-format subpath (see "Published API surface") for import/ export that speaks the same envelope as the web app's backup files:

import { serializeV2Backup, parseV2Backup } from '@retiregolden/planner-ui/plan-format'

Report model

Reports are data before they are documents. The stable ./report-model subpath exposes the edition-neutral ReportModel: everything a report needs — headline metrics, household snapshot, accounts, income sources, assumptions, modeled findings with their evidence, the year-by-year ledger, chart series, parameter sources, disclosures, and provenance (parameter pack years, data vintage, generation timestamp) — assembled from an already-computed projection, independent of any DOM, theme, or layout:

import { buildReportModel, serializeReportModel } from '@retiregolden/planner-ui/report-model'

const model = buildReportModel({ plan, result, summary, startYear })
const json = serializeReportModel(model) // deterministic: same input, same bytes

Engine-computed dollar figures are carried as whole nominal dollars — the precision every report presents, and the same whole-dollar discipline as the repo's case-runner manifests. (Raw engine floats can differ in the last digit across platforms' math libraries, which would make serialized models and golden fixtures machine-dependent.)

Every block carries a stable id from REPORT_BLOCK_IDS; hosts building their own renderers or packet templates should key layout off those ids and warn on ids they don't recognize rather than dropping content. The web app's own downloaded report is rendered from this same model (buildStandaloneReportHtml assembles it internally), so a host renderer and the standard report can never disagree about the underlying numbers. Golden JSON fixtures for the reference cases are committed under src/report/goldens/ and gate changes to the serialized contract.

Boundary notes for hosts (see the decision-support posture in the upstream repo): the modeled-findings block is calculation evidence attributed to the user's selected objective — render it as modeled results, not as advice authored by the software. The advisor-recommendations block is host-authored professional content: buildReportModel copies it verbatim from its input and never populates it on its own, and renderers must keep it visibly attributed to the professional. The disclosures block and the household incompleteData flag are caveats a rendering must keep visible: incompleteData marks a plan that cannot fund spending yet (no income sources, nothing funded), and renderers should surface it as a missing-data warning instead of presenting depletion as a funded plan's failure — the standard report renders this caveat, and the in-app Results page suppresses verdict framing for such plans.

Report branding

Downloaded HTML reports (Results, Report, and Optimizer pages) can carry the host's identity via the optional reportBranding prop — a generic hook, with RetireGolden defaults when omitted:

<PlannerApp
  reportBranding={{
    productName: 'Acme Wealth Planner',        // report title, header line, filename
    logoDataUri: 'data:image/png;base64,...',  // letterhead logo (data: URI only — reports stay self-contained)
    logoAlt: 'Acme Wealth',
    accentColor: '#1a3a5c',                    // letterhead rule color
    footerNote: 'Prepared by Acme Wealth Advisors LLC.',
  }}
/>

buildStandaloneReportHtml (deep subpath ./report/reportHtml — the stability caveat under "Published API surface" applies) accepts the same branding object directly for hosts that generate reports outside the planner pages. Values are sanitized — the logo must be a base64 data:image/... URI, the accent must parse as a real CSS color (hex, rgb()/hsl(), or a named color; anything else falls back to the default gold), and text fields are escaped-and-kept — so the report's no-script guarantee holds regardless of input. This branding applies to downloaded reports only; the in-app chrome is themed via the CSS tokens (above).

Storage

By default, plans persist in the browser profile via IndexedDB (idb) with localStorage for small preferences, exactly as on retiregolden.app. Hosts that need a different persistence story implement the PlanStore seam (see "Hosting the workspace") — anything the seam doesn't cover should be an upstream conversation, not a fork point.

Relationship to the web app

app/ in the upstream repo is the reference host: it adds the PWA service worker, SEO/meta, sitemap and prerender tooling, and static-hosting config, then mounts <PlannerApp /> exactly as in the snippet above. This package contains no host-specific behavior — composition points are generic React props/slots and CSS tokens, nothing keyed to any particular product.

License

AGPL-3.0-only (see LICENSE). The planner UI is free and un-gutted — the full feature set ships in the free web app.

RetireGolden, LLC also ships a commercial desktop edition built from this same UI under a separate commercial license, which funds the free one. That dual-license arrangement is why contributions to the upstream repo require a one-time Contributor License Agreement — you keep your copyright; the CLA lets the LLC also ship your contribution in the commercial edition. See CONTRIBUTING.md.