$npx -y skills add PolicyEngine/policyengine-claude --skill policyengine-designLoad when styling any PolicyEngine frontend — choosing colors, fonts, spacing, chart colors, or consuming @policyengine/ui-kit. Covers the two-import globals.css, the three-layer theme.css (@theme vs @theme inline), verified 0.4.0 token values and component exports, the dark-mode
| 1 | # PolicyEngine design system |
| 2 | |
| 3 | Design tokens live as CSS custom properties in **`@policyengine/ui-kit/theme.css`**. Every |
| 4 | frontend imports that one file. The canonical version is **`@policyengine/ui-kit ^0.4.0`** — |
| 5 | the version app-v2 pins. Ignore claims tied to 0.5/0.6/0.9; treat 0.4.0 as ground truth and |
| 6 | re-check `node_modules/@policyengine/ui-kit` when precision matters. |
| 7 | |
| 8 | ## Setup: the two imports |
| 9 | |
| 10 | ```css |
| 11 | /* globals.css — order matters; Tailwind must come first */ |
| 12 | @import "tailwindcss"; |
| 13 | @import "@policyengine/ui-kit/theme.css"; |
| 14 | ``` |
| 15 | |
| 16 | Next.js needs `postcss.config.mjs` = `{ plugins: { "@tailwindcss/postcss": {} } }` (install |
| 17 | `@tailwindcss/postcss` + `postcss`). A Vite app uses the `@tailwindcss/vite` plugin instead — |
| 18 | no PostCSS config. Do **not** add `postcss-import` or `autoprefixer` (`@tailwindcss/postcss` |
| 19 | handles both), and do **not** add a second `@import "tailwindcss"` (theme.css does not import |
| 20 | it). If the consumer's own utility classes go missing in a monorepo, scope source detection: |
| 21 | `@import "tailwindcss" source("./src");`. |
| 22 | |
| 23 | ## The three-layer theme.css |
| 24 | |
| 25 | `theme.css` (source: ui-kit `src/theme/tokens.css`, vendored from design-system v0.3.0) has |
| 26 | three layers: |
| 27 | |
| 28 | ``` |
| 29 | Layer 1 :root { --primary: #2C7A7B; --chart-1: #319795; ... } raw shadcn/ui semantic values |
| 30 | Layer 2 @theme inline { --color-primary: var(--primary); } bridge :root → Tailwind utilities |
| 31 | Layer 3 @theme { --color-teal-500: #319795; --text-sm: 14px; } brand palette, fonts, sizes, spacing |
| 32 | ``` |
| 33 | |
| 34 | ### `@theme` vs `@theme inline` — the mechanism |
| 35 | |
| 36 | - **`@theme inline`** keeps the `var()` reference, so the utility resolves it **at runtime**. |
| 37 | Use it whenever a token points at a `:root` variable — that is what lets `bg-primary` follow |
| 38 | a `.dark` override. All shadcn/ui semantic tokens use `@theme inline`. |
| 39 | - **`@theme`** bakes a **literal** value into the generated CSS at build time. Use it for the |
| 40 | fixed brand palette (`teal-500` is always `#319795`), font sizes, spacing, breakpoints. |
| 41 | |
| 42 | Getting these backwards is the classic bug: `@theme { --color-primary: var(--primary); }` bakes |
| 43 | the literal string `var(--primary)` and never re-resolves. |
| 44 | |
| 45 | ## Consuming tokens by context |
| 46 | |
| 47 | | Context | How | Example | |
| 48 | |---|---|---| |
| 49 | | React (semantic) | Tailwind semantic class | `className="bg-primary text-foreground"` | |
| 50 | | React (brand) | Tailwind brand class | `className="bg-teal-500 text-gray-600"` | |
| 51 | | Recharts / SVG | `var()` directly in fill/stroke | `fill="var(--chart-1)"` | |
| 52 | | Inline style | `var()` | `style={{ color: "var(--primary)" }}` | |
| 53 | | Python / Plotly | hex + CSS-var comment | `TEAL = "#319795" # --chart-1` | |
| 54 | |
| 55 | Namespace → utility mapping (Tailwind v4): `--color-*` → `bg-*`/`text-*`/`border-*`/`fill-*`; |
| 56 | `--text-*` → font-size utilities; `--font-*` → `font-*`; `--radius-*` → `rounded-*`; |
| 57 | `--spacing-*` → `p-*`/`m-*`/`gap-*`/`w-*`/`h-*`; `--breakpoint-*` → `sm:`/`md:`/`lg:`. |
| 58 | **Never hardcode hex or font names** when a token exists. |
| 59 | |
| 60 | ## Colors (verified 0.4.0) |
| 61 | |
| 62 | **Teal (brand):** 50 `#E6FFFA` · 100 `#B2F5EA` · 200 `#81E6D9` · 300 `#4FD1C5` · 400 `#38B2AC` |
| 63 | · 500 `#319795` (main brand) · 600 `#2C7A7B` (= `--primary`) · 700 `#285E61` · 800 `#234E52` |
| 64 | · 900 `#1D4044`. |
| 65 | |
| 66 | **Semantic (`:root`):** `--primary` `#2C7A7B` · `--background` `#FFFFFF` · `--foreground` |
| 67 | `#000000` · `--muted` `#F2F4F7` · `--muted-foreground` `#475569` · `--border` `#E2E8F0` · |
| 68 | `--card` `#FFFFFF` · `--destructive` `#EF4444` · `--ring` `#319795`. |
| 69 | |
| 70 | **Charts (`--chart-1` … `--chart-5`):** `#319795` (teal) · `#0EA5E9` (blue) · `#285E61` |
| 71 | (dark teal) · `#026AA2` (dark blue) · `#64748B` (slate). Exactly five. |
| 72 | |
| 73 | **Status fills:** success `#22C55E` · warning `#FEC601` · error `#EF4444` · info `#1890FF`. |
| 74 | These are fills for dots/badges/tints — not guaranteed AA as text on white. |
| 75 | |
| 76 | **Gray is Slate-flavored:** 500 `#64748B`, 600 `#475569`, 700 `#344054`, 900 `#101828`. |
| 77 | |
| 78 | ## Typography, spacing, radius |
| 79 | |
| 80 | - **Two fonts only:** Inter (`--font-sans`, everything) and JetBrains Mono (`--font-mono`, |
| 81 | code). Load Inter via Google Fonts (`family=Inter:wght@400;500;600;700&display=swap`). |
| 82 | - **Sizes:** `text-xs` 12 · `sm` 14 · `base` 16 · `lg` 18 · `xl` 20 · `2xl` 24 · `3xl` 28 · |
| 83 | `4xl` 32 (each with a matching line-height). |
| 84 | - **Radius:** base `--radius` 6px; semantic `--radius-ch |