Repository navigation
Add a Tools menu to the PolicyEngine header, built by policyengine.org's rule - #39
Merged
Merged
Conversation
Zone apps render ui-kit's header, which had no Tools item, so a visitor on CliffWatch couldn't reach the OBBBA explorer from it. policyengine.org's own header gained a Tools menu in PolicyEngine/policyengine-app-v2#1421; this adds the same menu to getPolicyEngineNavItems, right after Research. US: CliffWatch, OBBBA household explorer, State legislative tracker. UK: UK CliffWatch (/uk/uk-cliff-watch). Others: no Tools item. Each list ends with "All interactive tools". Rows carry a new `external` flag that renders a plain <a>, skipping linkComponent and onNavigate, because each tool is its own zone. Dropdown panels now size to their longest row (width: max-content), as #1421 did, so "OBBBA household explorer" stays on one line. ui-kit keeps a copy of app-v2's featuredTools.json because app-v2 is on ui-kit 0.4 and can't import it yet; a parity test reads app-v2's file and fails when the two differ, on every PR and weekly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Rows inherit `external` from their item unless they set their own, on desktop and mobile; the Tools item is external too. `NavItemChildConfig` names the row type. - Cap dropdown width at min(28rem, 100vw - 32px). - Let the mobile sheet scroll on short phones and sit above the sticky header. - Freeze the featured-tools data and type it as read-only and partial. - Move the network parity check out of `bun run test` into `bun run test:network`; its workflow runs on changes to the copy and weekly, and opens an issue on drift outside a pull request. - Split the changelog into added, changed and fixed entries. - Tests for each of the above; all 31 mutants are killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 tasks done
PolicyEngine/policyengine-app-v2#1424 replaced the hand-picked list with a rule: the PolicyEngine app first, then every calculator that works for any household under current law, sorted by label, from the `toolsMenu` labels in apps.json. Follow it. - PolicyEngineTools.ts holds each country's tools (slug and label), the main app's label and its default URL. US: CliffWatch, Coverage Compass, GiveCalc, Marriage calculator, QBI deduction calculator, SPM threshold calculator, TANF calculator. UK: Marriage calculator, Student loan calculator, UK CliffWatch. - The Tools menu lists the main app at <appUrl>/<country>, then the tools. "All interactive tools" is gone, as on policyengine.org. - getPolicyEngineNavItems takes an optional app URL; PolicyEngineHeader takes an appUrl prop. - The parity check applies app-v2's rule to apps.json on main and also compares the main app's label and URL; it retries a failed connection once. The workflow is renamed tools-menu-parity.yml. - Exports renamed: policyEngineTools, getPolicyEngineTools, getPolicyEngineAppUrl, POLICYENGINE_APP_LABEL, DEFAULT_POLICYENGINE_APP_URL, PolicyEngineTool. - Tests for the new rule; all 50 mutants are killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Keep a dropdown panel 16px inside the viewport when it would run past either edge, measuring afresh on each opening. - Scroll the mobile menu's links under a fixed title and close button, and lift the sheet's overlay above the sticky header too (SheetContent gains overlayClassName and overlayStyle). - PolicyEngineShell takes appUrl; export PolicyEngineToolLink; avoid Object.hasOwn for Safari before 15.4. - The parity check also reads the website header's MAIN_APP_LABEL and CALCULATOR_URL, and says what it can't see. - Tests for each, including the mobile rows in order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The delta review found the clamp measured only when the panel opened, so a resize while open could leave it off screen; that a scaled ancestor made the shift the wrong size; and that a shift inside the animated transform could be measured mid-slide. - Place the panel from its trigger's position, on opening and on every resize or scroll while open, removing the listeners on close. - Shift it with `left`, which doesn't animate. - Convert the shift to the panel's own pixels under a scaled ancestor. - Test SheetContent's overlay props and the sheet's column layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 26, 2026 14:22
MaxGhenis
added a commit
that referenced
this pull request
Oct 26, 2026
Releases publish to npm with this repository's GitHub Actions identity (OIDC trusted publishing) instead of the stored NPM_TOKEN, which npm refused when #39's release ran (E404). Node is pinned to 24.21.0 (npm 11.19.0); workflow_dispatch allows a retry without a commit; the job runs only on main, one release at a time, and releases only a run's own commit while it is main's tip, handing a stale run over to a fresh one so npm's provenance names the published code. commit_and_push.sh rebases the release commit onto main before pushing. npm's side (trusted publisher for push.yml, direct npm publish allowed) is still to be set up; until then a run that reaches publish fails there and leaves main unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A JEC analyst found CliffWatch hard to reach from policyengine.org. PolicyEngine/policyengine-app-v2#1421 gave the website header a Tools menu, and PolicyEngine/policyengine-app-v2#1424 (merged as 3c536338c, live in production) replaced its hand-picked list with a rule. Zone apps don't use that header, though. They render ui-kit's
PolicyEngineShell/PolicyEngineHeader, whose nav (getPolicyEngineNavItems) had no Tools item. So once a visitor is on one tool, the header can't take them to the others.What
getPolicyEngineNavItems(country, baseUrl, appUrl?)now inserts Tools right after Research, built by policyengine.org's rule. Max's ruling: a tool works for any household under current law; research about one policy, bill, event or finding stays out, however interactive it is. The menu lists the main app first, then the country's tools sorted by label:ushttps://app.policyengine.org/us· CliffWatch →/us/cliffwatch· Coverage Compass →/us/coverage-compass· GiveCalc →/us/givecalc· Marriage calculator →/us/marriage· QBI deduction calculator →/us/qbi-calculator· SPM threshold calculator →/us/spm-calculator· TANF calculator →/us/tanf-calculatorukhttps://app.policyengine.org/uk· Marriage calculator →/uk/marriage· Student loan calculator →/uk/uk-student-loan-calculator· UK CliffWatch →/uk/uk-cliff-watchbaseUrl, like the rest of the shared nav. The default,https://policyengine.org, 307-redirects tohttps://www.policyengine.org, where all ten tool pages return 200 today.appUrl, defaulthttps://app.policyengine.org.PolicyEngineHeaderandPolicyEngineShelltake it as a prop. The Tools item's ownhrefis the main app too. A dropdown trigger renders as a button, so nothing links it today./uk/uk-cliff-watch, never/uk/cliffwatch. Since PolicyEngine/policyengine-app-v2#1423, that path 308-redirects to/uk/uk-cliff-watch; before it, it showed the US app.Every row is a full page load. The main app is on another domain, and each tool is its own zone. For example, the website rewrites
/us/cliffwatchtocliff-watch.vercel.app/, which has no basePath. So a client-side router transition can't reach any of them.NavItemConfigitems and dropdown rows gain an optionalexternalflag. On desktop and mobile it renders a plain<a>, skipslinkComponentandonNavigate, and doesn't callpreventDefault. Rows inherit the flag from their item unless they set their own, and the newNavItemChildConfigtype names a row. The Tools item and every row in it set it. Items without the flag behave as before.Dropdown width and position. ui-kit's dropdown panel is absolutely positioned under a narrow trigger. With no width it shrank to
minWidth: 220px, and longer rows wrapped: I measured the About rows' descriptions wrapping at 1200px, ui-kit'slg. Today's Tools labels fit in 220px; a longer one would wrap the same way. The panel now uses thewidth: max-contentfix from PolicyEngine/policyengine-app-v2#1421, capped atmin(28rem, 100vw - 32px)so a long description can't widen the page. A wider panel, centred under a trigger near the edge, could run off the viewport and add a horizontal scrollbar. So the panel now places itself to stay 16px inside the viewport, moving sideways from centred under its trigger (the left edge wins if it can't fit at all). It does this when it opens, and again whenever the window resizes or anything scrolls while it's open. It works from the trigger's position, so a placement never depends on the last one. The shift is set withleft, which doesn't animate, so the panel never slides sideways and a measurement never catches it mid-slide. Under a scaled ancestor it converts the shift to the panel's own pixels. At 1280px, Tools measures 220px and About 344px, every row on one line.Mobile sheet. Fixes the reviews found while checking the longer menu:
z-50, under the sticky header'szIndex: 1000. On a page where the header is at the root, the header covered the sheet's title and close button and stayed undimmed. Both are now at 1001.SheetContentgainsoverlayClassNameandoverlayStyleprops to set this.New exports from
@policyengine/ui-kitand/layout:policyEngineTools: each country's{slug, label}entries, sorted by label.getPolicyEngineTools(country, baseUrl): the same entries with absolutehrefs, typedPolicyEngineToolLink.getPolicyEngineAppUrl(country, appUrl).POLICYENGINE_APP_LABELandDEFAULT_POLICYENGINE_APP_URL.PolicyEngineTool,PolicyEngineToolLinkandNavItemChildConfigtypes.The data is frozen and typed read-only, since every header in a bundle shares it. Its doc comment says the export may move once app-v2 and ui-kit read one list. The demo gallery gains a "PolicyEngine header" section with a country switcher covering US, UK and Nigeria.
Where the list lives: a copy in ui-kit, checked against apps.json
policyengine.org builds the menu from the
toolsMenu: { label }field on entries in app-v2'sapp/src/data/apps/apps.json(app/src/utils/toolsMenu.tsfor the calculator,website/src/lib/toolsMenu.tsfor the website, which reads the same file through a symlink). ui-kit can't read that file:app/andwebsite/package.json files all pin@policyengine/ui-kitto^0.4.0, and a caret range on 0.x won't reach 0.13. Even after an upgrade, the website couldn't read data from the entries this PR adds to. ui-kit's build prefixes the main entry and/layoutwith"use client"(preserveClientDirectivesinvite.config.ts). The homepage'sHomeToolsPreview.tsxis a server component, imported bywebsite/src/app/[countryId]/page.tsx, and a server component that imports a value from a"use client"module gets a client reference, not the data.So ui-kit keeps a copy, in
src/layout/PolicyEngineTools.ts, and a test fails when it drifts:The check.
tests/layout/toolsMenuParity.network.test.tsfetches app-v2'smaincopy of apps.json and applies app-v2's rule to it: entries with atoolsMenulabel, by country, sorted withlocaleCompare(…, 'en'). It assertstoStrictEqualwith ui-kit's copy: same countries, slugs, labels and order. It also readsMAIN_APP_LABELandCALCULATOR_URL's default from both headers' sources (app/src/utils/toolsMenu.tswithapp/src/constants.ts, andwebsite/src/lib/toolsMenu.ts), and compares them withPOLICYENGINE_APP_LABELandDEFAULT_POLICYENGINE_APP_URL. It reports a failed fetch, a non-JSON response or a source pattern it can't find plainly, and retries a failed connection once.What it can't see, as its doc comment says:
website/src/__tests__/lib/toolsMenu.test.tspins its side.CALCULATOR_URL's default in the code. A deployment that setsNEXT_PUBLIC_CALCULATOR_URLorVITE_CALCULATOR_URLwould link the main app elsewhere, unseen.When it runs. Because it needs the network,
bun run testskips*.network.test.tsfiles, andbun run test:networkruns them. Thetools-menu-parity.ymlworkflow runs it:mainthat touch the copy, the test, the workflow,package.jsonorvitest.config.ts;A failure outside a pull request opens an issue titled "Tools menu differs from policyengine.org", or comments on the open one. The job has a 10-minute timeout and only
contents: readandissues: write.Invariants (property-tested with fast-check)
For every country C (including
'',US,constructor,__proto__,toStringand arbitrary strings), base URL B and app URL A (http or https, any domain, optional port, 0–3 trailing slashes):usoruk, and never otherwise. Inherited object keys never yield a menu, because the lookup checks own properties only (withhasOwnProperty, sinceObject.hasOwnneeds Safari 15.4). The main app never stands alone in a menu.<A without trailing slashes>/C, and the Tools item's href is the same. Then come exactly C's tools in data order, each at<B without trailing slashes>/C/<slug>on B's origin, with no query, sorted by label. Every row and the item areexternal. Labels and hrefs are unique.obbba-households) or the bill tracker (bill-tracker), and the UK menu contains/uk/uk-cliff-watchand no/uk/cliffwatchpath.The rendering tests cover desktop and mobile, with and without
linkComponentandonNavigate:onNavigateisn't called.onNavigate.externalfrom their item, and a row's own flag wins, both ways.appUrlandbaseUrlreach the rendered links, from the header and from the shell.ca, the panel'smax-contentwidth and cap.SheetContent's overlay props reach the overlay and nowhere else, and the overlay is unchanged without them.Mutation check. I applied 78 mutants, and all 78 are caught.
external: dropping it on each code path, breaking inheritance or the row override on desktop and mobile;max-contentor the width cap; for its position, ignoring either edge, letting the right edge win, measuring the panel instead of the trigger, no margin, measuringinnerWidth, no resize or scroll listener, a scroll listener that doesn't capture, leaking either listener, dropping the unmeasured-panel guard, not applying the shift, ignoring the scale, animatingleftorall;flexorminHeight; scrolling the whole sheet again; laying the sheet out in a row; dropping the sheet's or the overlay's z-index;SheetContentignoringoverlayClassNameoroverlayStyle;appUrlin the function, the nav, the header prop or the shell prop; a wrong shell default; giving every country the US list; showing the main app alone;/uk/cliffwatch, swapping the order, changing a label, adding the OBBBA explorer, dropping a UK tool, changing the main app's label or URL.CALCULATOR_URLpattern, which can't read the website's two-line declaration.Verification
bun run build && bun run test: 31 files, 346 tests pass, including the consumer-types harness against the builtdist/. The fixture imports the new exports,NavItemChildConfig,PolicyEngineToolLink, theexternalfields, the header's and shell'sappUrlprops andSheetContent'soverlayStyle.bun run test:network: 3 tests pass against app-v2'smain(3c536338c and later).bun run typecheck: clean.actionlinton the workflow: clean.transform: scale(0.8), at 1240px, About's panel renders 276px wide (344px in its own pixels) and ends at 1224px, 16px in.zIndex: 1000standing in for a zone's sticky header, the overlay covers it outside the sheet, and the sheet's title and close button are on top. At the oldz-50, the stand-in covers the close button and stays undimmed.Rollout: who needs to bump
Merging releases 0.13.0 to npm, because a fragment is
added. For the census I mapped every zone rewrite in app-v2 to its repo:website/src/data/appZoneRoutes.ts,website/next.config.tsandwebsite/vercel.json. I then checked each repo's header code,package.jsonand lockfile on its default branch.45 zone repos render
PolicyEngineShellwith the default nav and need only a bump to^0.13.0, except that the marriage zone also needs the country fix in the follow-ups for its UK page. All pin^0.12.x, and a caret range on 0.x stops at 0.12, so none picks this up on its own. None passeslinkComponentoronNavigate, so for current consumersexternalis a safeguard rather than a fix. The repos: 2024-election-dashboard, 2025-year-in-review, ACA-Calc, ai-inequality, april-fools-2026, Autumn-budget-local-area, california-wealth-tax, cancelling-fuel-duty-rise, cliff-watch, Coverage-Compass, crfb-tob-impacts, ctc-calculator, energy-price-shock, givecalc, keep-your-pay-act, llm-econ-beliefs, marriage, nics-exemption-inactive-employees, oregon-kicker-refund, policyengine-ads-dashboard, policyengine-scorecard, ri-ctc-calculator, salt-amt-calculator, scotland-income-tax-reform, scottish-budget-2026-2027, snap-district-map, spm-calculator, student-loan-calculator, tanf-calculator, uc-rebalancing, uk-2024-manifestos-comparison, uk-autumn-budget-dashboard, uk-cliff-watch, uk-energy-autumn-budget, uk-land-value-tax, uk-public-services-imputation, uk-salary-sacrifice-analysis, uk-spring-statement-2026, uk-two-child-limit-app, us-state-eitcs-ctcs, utah-2026-tax-changes, vance-harris-ctc-comparison, working-Americans-tax-cut-act-, working-parents-tax-relief-act, wv-sb392-tax-cut. Every tool but the QBI calculator is among them: cliff-watch, Coverage-Compass, givecalc, marriage (both countries), spm-calculator, tanf-calculator, student-loan-calculator and uk-cliff-watch.Zones a bump won't reach, because they build their own nav:
qbi-visualizer, the QBI deduction calculator: ui-kit's low-levelHeaderwith a hand-writtenNAV_ITEMSinfrontend/src/app/Shell.tsx, pinned^0.9.0. It needsgetPolicyEngineNavItems('us')as well as the bump.obbba-household-by-household: SvelteKit, with a hand-writtenNAV_LINKSinsrc/routes/+layout.svelte.policyengine-model: the model pages, with its ownPEHeader.tsx.state-legislative-tracker: an app header with no site nav.household-api-docs: the/apiand/pythonpages, with its ownHeader.jsx.PolicyEngineHeader.jsxcopies: bus-fare-cap, electricity-vat-cut, free-childcare-reform, impact-iran-war-living-standards, uk-energy-reforms, uk-triple-lock and young-worker-nics.PolicyEngineShellorPolicyEngineHeaderin the census. Several build their own header:child-poverty-impact-dashboard(AppV2Header.tsx);futa-wage-base-dashboard,missouri-income-tax-eliminationandrefundable-credit-conversion(frontend/components/Header.tsx); andpolicybench(SiteHeader.tsx). The others arepolicyengine-slides,snap-qc-sim,student-loan-visualisation,tutorial,nj-ctc-increase,policyengine-taxsim,south-carolina-2026-tax-changesandpolicyengine-be-demo; the follow-up should check each.These need Tools added by hand, or a move onto
PolicyEngineShell. Both are follow-ups and not part of this PR.Follow-ups, not in this PR
^0.13.0after release. Until then, and until each zone rebuilds, a zone keeps its old header, so a change to policyengine.org's list reaches zones only through a ui-kit release and those bumps./uk/marriagefrom the same app as/us/marriage, with?country=uk(website/src/data/appZoneRoutes.ts), but the zone'sapp/layout.jsxrenders<PolicyEngineShell country="us">. So at/uk/marriageits header shows the US nav today, and after the bump it would show the US Tools menu.external. Research, Model, API, About and Donate also live in other zones, so a consumer that passeslinkComponentwould get a client transition that can't reach them. No current consumer passes one, so nothing is broken today.https://policyengine.org/uk/cliffwatch(frontend/app/layout.jsx), now a redirect to/uk/uk-cliff-watch. The session that shipped #1423 has proposed that fix.🤖 Generated with Claude Code