$npx -y skills add PolicyEngine/policyengine-claude --skill policyengine-toolsLoad when building a standalone PolicyEngine interactive tool, calculator, or dashboard that deploys to Vercel and embeds into policyengine.org as a Next.js multizone. Covers the required stack (Next App Router + Tailwind v4 + @policyengine/ui-kit + bun), multizone zone config (b
| 1 | # PolicyEngine standalone tools |
| 2 | |
| 3 | How to build calculators, dashboards, and visualizations that deploy independently to Vercel |
| 4 | and slot into **policyengine.org** as Next.js multizones. New tools are their own repos; the |
| 5 | host (`policyengine-app-v2/website/`) proxies a URL path to each tool's Vercel deployment. |
| 6 | |
| 7 | ## Required stack |
| 8 | |
| 9 | - **Next.js (App Router)** — not Pages Router, not Vite as the app bundler (Vite only backs |
| 10 | Vitest). `next.config.ts` + `app/` with `layout.tsx` + `page.tsx`, TypeScript. |
| 11 | - **Tailwind v4, CSS-first** — no `tailwind.config.{ts,js}`. `globals.css` is exactly: |
| 12 | ```css |
| 13 | @import "tailwindcss"; |
| 14 | @import "@policyengine/ui-kit/theme.css"; /* both required; Tailwind must come first */ |
| 15 | ``` |
| 16 | - **PostCSS is required for Next** — create `postcss.config.mjs` with |
| 17 | `{ plugins: { "@tailwindcss/postcss": {} } }` and install `@tailwindcss/postcss` + `postcss` |
| 18 | as devDeps. (A Vite tool uses the `@tailwindcss/vite` plugin instead and needs no PostCSS |
| 19 | config.) The theme.css header itself states `@tailwindcss/postcss` is mandatory — the older |
| 20 | "no postcss.config" guidance was wrong. |
| 21 | - **`@policyengine/ui-kit`** (`bun add @policyengine/ui-kit`) — use its components before |
| 22 | building your own. See policyengine-design for the full 0.4.0 export list. |
| 23 | - **bun** for everything (`bun install`, `bun run build`, `bunx`; `bun.lock` committed). |
| 24 | - **Vercel** deploy under the `policy-engine` scope. Static-only tools may use |
| 25 | `output: 'export'`; tools with a Modal backend stay server-rendered. |
| 26 | |
| 27 | Scaffold: `bunx create-next-app@latest my-tool --ts --app --tailwind --eslint --import-alias "@/*"`, |
| 28 | then `bun add @policyengine/ui-kit` and set up `globals.css` as above. |
| 29 | |
| 30 | ## ui-kit components (verified 0.4.0) |
| 31 | |
| 32 | Prefer these over hand-rolled equivalents; import from `@policyengine/ui-kit`: |
| 33 | |
| 34 | | Need | Component | |
| 35 | |---|---| |
| 36 | | Page shell / layout | `DashboardShell`, `SidebarLayout`, `SingleColumnLayout`, `InputPanel`, `ResultsPanel`, `Stack`, `Group`, `Container` | |
| 37 | | Header + footer | `Header`, `Footer` (Header props: `navItems`, `logoSrc`, `linkComponent`, `countries` — **not** `variant`/`navLinks`) | |
| 38 | | Inputs | `CurrencyInput`, `NumberInput`, `SelectInput`, `CheckboxInput`, `SliderInput`, `InputGroup` | |
| 39 | | Primitives | `Button`, `Card`, `Badge`, `Tabs`, `Dialog`, `Select`, `Tooltip`, `Alert`, `Switch`, `SegmentedControl`, … | |
| 40 | | Display | `MetricCard`, `SummaryText`, `DataTable`, `PolicyEngineWatermark` | |
| 41 | | Charts | `ChartContainer`, `PEBarChart`, `PELineChart`, `PEAreaChart`, `PEWaterfallChart` | |
| 42 | | Maps | `USDistrictChoroplethMap`, `UKConstituencyChoroplethMap`, `HexagonalMap`, `HouseholdGraph` | |
| 43 | | Utils | `formatCurrency`, `formatPercent`, `formatNumber`, `cn`, `logos` | |
| 44 | |
| 45 | Only build custom when nothing above fits. `getCssVar()` does not exist — SVG accepts `var()`. |
| 46 | |
| 47 | ## Multizone integration |
| 48 | |
| 49 | The host proxies a path to your tool via `rewrites` in |
| 50 | `policyengine-app-v2/website/next.config.ts`. `basePath` governs **route URLs**; |
| 51 | `assetPrefix` governs **static asset URLs** (`_next/static/*`). Pick the zone pattern from |
| 52 | how many public paths the zone owns: |
| 53 | |
| 54 | - **Path-mounted** (one public path matching the kebab repo name): `basePath: '/us/my-tool'`. |
| 55 | Server-rendered builds need no `assetPrefix` — `basePath` scopes `_next/static` automatically. |
| 56 | - **Root-served** (multiple public paths, e.g. `/us/api` + `/uk/api`): no `basePath`; set |
| 57 | `assetPrefix: '/_zones/<repo-name>'` so assets don't collide with the host. |
| 58 | |
| 59 | Host rewrite (in `beforeFiles`, so it beats the dynamic `[slug]` route), hardcoded to the |
| 60 | zone's **production Vercel URL**: |
| 61 | |
| 62 | ```ts |
| 63 | { source: '/us/my-tool', destination: 'https://my-tool.vercel.app/us/my-tool' }, |
| 64 | { source: '/us/my-tool/:path*', destination: 'https://my-tool.vercel.app/us/my-tool/:path*' }, |
| 65 | ``` |
| 66 | |
| 67 | **Static-export zones** (`output: 'export'`) need three coordinated pieces — omit any and |
| 68 | that environment 404s its JS/CSS: |
| 69 | |
| 70 | 1. Zone `next.config.mjs` exports a **function** and phase-gates the prefix so `next dev` |
| 71 | still resolves assets: `assetPrefix: phase === PHASE_DEVELOPMENT_SERVER ? undefined : '/_zones/my-tool'`, |
| 72 | plus `output: 'export'`, `basePath`, `trailingSl |