diff --git a/.github/build_changelog.py b/.github/build_changelog.py new file mode 100644 index 0000000..bdde7eb --- /dev/null +++ b/.github/build_changelog.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Build CHANGELOG.md from changelog.d/ fragments.""" + +import json +import os +import sys +from datetime import date + +CHANGELOG_DIR = "changelog.d" +CHANGELOG_PATH = "CHANGELOG.md" +PACKAGE_PATH = "package.json" + +TYPE_HEADING = { + "added": "Added", + "changed": "Changed", + "fixed": "Fixed", + "removed": "Removed", + "breaking": "Breaking", +} +TYPE_ORDER = ["Added", "Changed", "Fixed", "Removed", "Breaking"] + + +def get_current_version(): + with open(PACKAGE_PATH) as f: + data = json.load(f) + return data["version"] + + +def get_previous_version(): + """Read the most recent version from existing CHANGELOG.md.""" + if not os.path.exists(CHANGELOG_PATH): + return "0.0.0" + with open(CHANGELOG_PATH) as f: + for line in f: + if line.startswith("## ["): + return line.split("[")[1].split("]")[0] + return "0.0.0" + + +def read_fragments(): + """Read all fragments and group by type heading.""" + groups = {} + for fname in sorted(os.listdir(CHANGELOG_DIR)): + if fname == ".gitkeep": + continue + parts = fname.rsplit(".", 2) + if len(parts) != 3 or parts[2] != "md": + continue + fragment_type = parts[1] + heading = TYPE_HEADING.get(fragment_type, fragment_type.capitalize()) + with open(os.path.join(CHANGELOG_DIR, fname)) as f: + content = f.read().strip() + if content: + groups.setdefault(heading, []).append(content) + return groups + + +def build_section(version, groups): + """Build a Keep a Changelog section.""" + lines = [f"## [{version}] - {date.today().isoformat()}", ""] + for heading in TYPE_ORDER: + if heading not in groups: + continue + lines.append(f"### {heading}") + lines.append("") + for entry in groups[heading]: + for raw_line in entry.splitlines(): + raw_line = raw_line.strip() + if not raw_line: + continue + if not raw_line.startswith("- "): + raw_line = f"- {raw_line}" + lines.append(raw_line) + lines.append("") + return "\n".join(lines) + + +def update_changelog(new_section, version, old_version): + """Prepend new section to CHANGELOG.md and add comparison link.""" + if os.path.exists(CHANGELOG_PATH): + with open(CHANGELOG_PATH) as f: + content = f.read() + else: + content = "# Changelog\n" + + # Insert new section before the first existing ## entry + marker = "\n## [" + pos = content.find(marker) + if pos == -1: + # No existing entries — append after header + content = content.rstrip() + "\n\n" + new_section + "\n" + else: + header = content[: pos + 1] # include the trailing newline + rest = content[pos + 1 :] + content = header + new_section + "\n" + rest + + # Add comparison link at the top of the link reference section + link = ( + f"[{version}]: https://github.com/PolicyEngine/" + f"policyengine-ui-kit/compare/{old_version}...{version}" + ) + lines = content.split("\n") + new_lines = [] + link_inserted = False + for line in lines: + if not link_inserted and line.startswith("[") and "]: https://" in line: + new_lines.append(link) + link_inserted = True + new_lines.append(line) + if not link_inserted: + new_lines.append("") + new_lines.append(link) + content = "\n".join(new_lines) + + with open(CHANGELOG_PATH, "w") as f: + f.write(content) + + +def delete_fragments(): + """Remove consumed fragments, keeping .gitkeep.""" + for fname in os.listdir(CHANGELOG_DIR): + if fname == ".gitkeep": + continue + os.remove(os.path.join(CHANGELOG_DIR, fname)) + + +def main(): + has_fragments = any(f != ".gitkeep" for f in os.listdir(CHANGELOG_DIR)) + if not has_fragments: + print("No changelog fragments found. Nothing to build.") + sys.exit(0) + + version = get_current_version() + old_version = get_previous_version() + groups = read_fragments() + + new_section = build_section(version, groups) + update_changelog(new_section, version, old_version) + delete_fragments() + + print(f"Built changelog for version {version}") + + +if __name__ == "__main__": + main() diff --git a/.github/bump_version.py b/.github/bump_version.py new file mode 100644 index 0000000..dec9e9b --- /dev/null +++ b/.github/bump_version.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Bump version in package.json based on changelog.d/ fragments.""" + +import json +import os +import sys + +CHANGELOG_DIR = "changelog.d" +PACKAGE_PATH = "package.json" + +# Lower number = higher priority bump +BUMP_PRIORITY = { + "breaking": 0, # major + "added": 1, # minor + "removed": 1, # minor + "changed": 2, # patch + "fixed": 2, # patch +} + + +def get_current_version(): + with open(PACKAGE_PATH) as f: + data = json.load(f) + return data["version"] + + +def get_bump_level(): + """Scan fragment filenames to determine the highest-priority bump type.""" + level = None + for fname in os.listdir(CHANGELOG_DIR): + if fname == ".gitkeep": + continue + # Expected format: {name}.{type}.md + parts = fname.rsplit(".", 2) + if len(parts) != 3 or parts[2] != "md": + continue + fragment_type = parts[1] + priority = BUMP_PRIORITY.get(fragment_type, 2) # default to patch + if level is None or priority < level: + level = priority + return level + + +def compute_new_version(version, level): + major, minor, patch = map(int, version.split(".")) + if level == 0: + return f"{major + 1}.0.0" + elif level == 1: + return f"{major}.{minor + 1}.0" + else: + return f"{major}.{minor}.{patch + 1}" + + +def main(): + fragments = [f for f in os.listdir(CHANGELOG_DIR) if f != ".gitkeep"] + if not fragments: + print("No changelog fragments found. Nothing to bump.") + sys.exit(0) + + old_version = get_current_version() + level = get_bump_level() + if level is None: + print("No valid fragments found. Nothing to bump.") + sys.exit(0) + + new_version = compute_new_version(old_version, level) + + with open(PACKAGE_PATH) as f: + data = json.load(f) + data["version"] = new_version + with open(PACKAGE_PATH, "w") as f: + json.dump(data, f, indent=2) + f.write("\n") + + print(f"Bumped version: {old_version} → {new_version}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..484f508 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,66 @@ +name: Version, Changelog, and Publish + +on: + push: + branches: [main] + +jobs: + release: + name: Bump version, build changelog, publish to npm + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'Release @policyengine/ui-kit')" + + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.POLICYENGINE_GITHUB }} + + - name: Check for changelog fragments + id: check + run: | + FRAGMENTS=$(ls changelog.d/ | grep -v .gitkeep | wc -l) + echo "count=$FRAGMENTS" >> "$GITHUB_OUTPUT" + + - name: Bump version + if: steps.check.outputs.count != '0' + run: python .github/bump_version.py + + - name: Build changelog + if: steps.check.outputs.count != '0' + run: python .github/build_changelog.py + + - name: Set up Bun + if: steps.check.outputs.count != '0' + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + if: steps.check.outputs.count != '0' + run: bun install --frozen-lockfile + + - name: Build + if: steps.check.outputs.count != '0' + run: bun run build + + - name: Set up Node for npm publish + if: steps.check.outputs.count != '0' + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + + - name: Publish to npm + if: steps.check.outputs.count != '0' + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Commit and push + if: steps.check.outputs.count != '0' + uses: EndBug/add-and-commit@v9 + with: + message: "Release @policyengine/ui-kit" + default_author: github_actions + add: | + package.json + CHANGELOG.md + changelog.d/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/CLAUDE.md b/CLAUDE.md index 6a00e75..3b82f28 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # @policyengine/ui-kit -PolicyEngine UI kit — design tokens, Tailwind CSS v4 theme, and React 19 components for dashboards and calculators. +PolicyEngine UI kit — CSS-first design tokens, Tailwind CSS v4 theme, shadcn/ui primitives, and React 19 components for dashboards and calculators. ## Commands @@ -8,22 +8,32 @@ PolicyEngine UI kit — design tokens, Tailwind CSS v4 theme, and React 19 compo - Build: `bun run build` - Test: `bun run test` - Type check: `bun run typecheck` +- Demo: `bun run dev:demo` ## Architecture - **Vite library mode** — builds ESM + CJS + types + styles.css -- **Tailwind CSS v4** with `tw:` prefix (mirrors policyengine-app-v2) +- **Tailwind CSS v4** with standard class names (no prefix) +- **shadcn/ui** pattern for primitives (Button, Badge, Card, Tabs) - **CVA** (class-variance-authority) for component variants - **Recharts** for chart components (peer dependency) ## Design tokens -Tokens are in `src/tokens/` — colors, typography, spacing, charts. These are the source of truth for all PolicyEngine applications. The same values appear as CSS custom properties in `src/app.css` via the `@theme` block. +Tokens are in `src/theme/tokens.css` — the single source of truth for all frontend projects. This CSS file defines: + +1. **Layer 1 (`:root`)**: shadcn/ui semantic variables (`--primary`, `--background`, `--chart-1`, etc.) +2. **Layer 2 (`@theme inline`)**: Bridges `:root` vars to Tailwind utilities (`bg-primary`, `text-foreground`) +3. **Layer 3 (`@theme`)**: Brand palette (`bg-teal-500`, `text-gray-600`), font sizes, spacing, breakpoints + +Consumers import: `@import "@policyengine/ui-kit/theme.css";` ## Styling rules -- All Tailwind classes use `tw:` prefix (e.g. `tw:bg-primary-500`) +- Use standard Tailwind classes (`bg-primary`, `text-muted-foreground`, `border-border`, `rounded-lg`) +- Use brand palette classes for specific colors (`bg-teal-500`, `text-gray-600`) - Use `cn()` from `src/utils/cn.ts` for class merging -- Never hardcode hex colors — use token classes or imports +- Recharts charts use CSS vars directly: `fill="var(--chart-1)"` +- Never hardcode hex colors in component code - Sentence case for all UI text - Every component accepts `className` and `styles` props diff --git a/changelog.d/pe-token-alignment.changed.md b/changelog.d/pe-token-alignment.changed.md new file mode 100644 index 0000000..3b07302 --- /dev/null +++ b/changelog.d/pe-token-alignment.changed.md @@ -0,0 +1 @@ +Align with PE design system: remove tw: prefix, adopt pe-* token naming convention, bridge @theme values to @policyengine/design-system CSS variables. diff --git a/components.json b/components.json new file mode 100644 index 0000000..76048ee --- /dev/null +++ b/components.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/theme/tokens.css", + "baseColor": "neutral", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/utils", + "ui": "@/primitives", + "lib": "@/utils" + } +} diff --git a/demo/Demo.tsx b/demo/Demo.tsx index efb9d0f..0685867 100644 --- a/demo/Demo.tsx +++ b/demo/Demo.tsx @@ -1,4 +1,24 @@ import { useState } from 'react'; +import { + Home, + Settings, + Users, + BarChart3, + FileText, + Bell, + Search, + Mail, + Calculator, + Globe, + ChevronDown, + ExternalLink, + Bold, + Italic, + Underline, + Copy, + Trash2, + PenLine, +} from 'lucide-react'; // Primitives import { Button } from '../src/primitives/Button'; @@ -9,9 +29,73 @@ import { CardDescription, CardContent, CardFooter, + CardAction, } from '../src/primitives/Card'; import { Badge } from '../src/primitives/Badge'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '../src/primitives/Tabs'; +import { Text } from '../src/primitives/Text'; +import { Title } from '../src/primitives/Title'; +import { Spinner } from '../src/primitives/Spinner'; +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, + DialogClose, +} from '../src/primitives/Dialog'; +import { + Sheet, + SheetTrigger, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, +} from '../src/primitives/Sheet'; +import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../src/primitives/Tooltip'; +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + SelectGroup, + SelectLabel, +} from '../src/primitives/Select'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuLabel, +} from '../src/primitives/DropdownMenu'; +import { Input } from '../src/primitives/Input'; +import { Label } from '../src/primitives/Label'; +import { Separator } from '../src/primitives/Separator'; +import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '../src/primitives/Accordion'; +import { Alert, AlertTitle, AlertDescription } from '../src/primitives/Alert'; +import { Popover, PopoverTrigger, PopoverContent } from '../src/primitives/Popover'; +import { Switch } from '../src/primitives/Switch'; +import { RadioGroup, RadioGroupItem } from '../src/primitives/RadioGroup'; +import { Checkbox } from '../src/primitives/Checkbox'; +import { ScrollArea } from '../src/primitives/ScrollArea'; +import { Skeleton } from '../src/primitives/Skeleton'; +import { Progress } from '../src/primitives/Progress'; +import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../src/primitives/Collapsible'; +import { Textarea } from '../src/primitives/Textarea'; +import { + Command, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandSeparator, +} from '../src/primitives/Command'; +import { SegmentedControl } from '../src/primitives/SegmentedControl'; // Layout import { DashboardShell } from '../src/layout/DashboardShell'; @@ -20,6 +104,14 @@ import { SidebarLayout } from '../src/layout/SidebarLayout'; import { SingleColumnLayout } from '../src/layout/SingleColumnLayout'; import { InputPanel } from '../src/layout/InputPanel'; import { ResultsPanel } from '../src/layout/ResultsPanel'; +import { Stack } from '../src/layout/Stack'; +import { Group } from '../src/layout/Group'; +import { Container } from '../src/layout/Container'; +import { SidebarNavItem } from '../src/layout/SidebarNavItem'; +import { SidebarSection } from '../src/layout/SidebarSection'; +import { SidebarDivider } from '../src/layout/SidebarDivider'; +import { Footer } from '../src/layout/Footer'; +import { HomeHeader } from '../src/layout/homeHeader'; // Inputs import { CurrencyInput } from '../src/inputs/CurrencyInput'; @@ -42,8 +134,8 @@ import { PELineChart } from '../src/charts/PELineChart'; import { PEAreaChart } from '../src/charts/PEAreaChart'; import { PEWaterfallChart } from '../src/charts/PEWaterfallChart'; -// Tokens (for display) -import { formatCurrency } from '../src/tokens/charts'; +// Utils +import { formatCurrency } from '../src/utils/formatters'; // Assets import { logos } from '../src/assets'; @@ -151,6 +243,33 @@ const filingOptions = [ { label: 'Head of household', value: 'hoh' }, ]; +const homeHeaderNavItems = [ + { + label: 'Policy', + href: '#', + children: [ + { label: 'Compute your taxes', href: '#', description: 'See how policy changes affect your household' }, + { label: 'Explore reforms', href: '#', description: 'Browse contributed policy reforms' }, + ], + }, + { + label: 'Research', + href: '#', + children: [ + { label: 'Blog', href: '#', description: 'Latest analysis and updates' }, + { label: 'Publications', href: '#', description: 'Academic papers and reports' }, + ], + }, + { label: 'About', href: '#' }, + { label: 'Donate', href: '#' }, +]; + +const homeHeaderCountries = [ + { id: 'us', label: 'United States', flagEmoji: '\u{1F1FA}\u{1F1F8}' }, + { id: 'uk', label: 'United Kingdom', flagEmoji: '\u{1F1EC}\u{1F1E7}' }, + { id: 'ng', label: 'Nigeria', flagEmoji: '\u{1F1F3}\u{1F1EC}' }, +]; + // --------------------------------------------------------------------------- // Section wrapper // --------------------------------------------------------------------------- @@ -163,8 +282,8 @@ function Section({ children: React.ReactNode; }) { return ( -
-

+
+

{title}

{children} @@ -180,8 +299,8 @@ function SubSection({ children: React.ReactNode; }) { return ( -
-

+
+

{title}

{children} @@ -201,554 +320,1150 @@ export function Demo() { const [includeState, setIncludeState] = useState(true); const [ubiAmount, setUbiAmount] = useState(500); + // New state for expanded primitives + const [switchOn, setSwitchOn] = useState(false); + const [radioValue, setRadioValue] = useState('option-1'); + const [checkboxChecked, setCheckboxChecked] = useState(false); + const [segmentValue, setSegmentValue] = useState('household'); + const [collapsibleOpen, setCollapsibleOpen] = useState(false); + const [selectValue, setSelectValue] = useState(''); + const [progressValue] = useState(65); + const [homeCountry, setHomeCountry] = useState('us'); + return ( - -
-
-

- @policyengine/ui-kit -

-

- Component gallery — every component rendered with example data -

-
+ + + console.log('Navigate:', href)} + /> +
+
+

+ @policyengine/ui-kit +

+

+ Component gallery — every component rendered with example data +

+
- {/* ================================================================ */} - {/* PRIMITIVES */} - {/* ================================================================ */} -
- -
- - - - -
-
- - - - -
-
- - -
-
- - -
- Default - Secondary - Outline - Success - Warning - Error -
-
+ {/* ================================================================ */} + {/* PRIMITIVES — Typography & Feedback */} + {/* ================================================================ */} +
+ + + Text size xs — Fine print and labels + Text size sm — Secondary content + Text size md — Default body text + Text size lg — Emphasized content + Text size xl — Large callouts + + Normal weight + Medium weight + Bold weight + Dimmed color + + + - -
- - - Universal Basic Income - - $500/month payment to every adult citizen - - - -

- This reform would provide a monthly payment of $500 to every - adult US citizen, funded through a combination of income tax - increases and spending reductions. -

-
- - - - -
- - - Child Tax Credit expansion - - Increase CTC from $2,000 to $3,600 per child - - - -

- Expanding the Child Tax Credit to $3,600 per child under 6 - and $3,000 for children 6-17, with full refundability for - all qualifying families. -

-
- - - -
-
-
- - - - - Overview - - Distributional impact - - Budget impact - - - - -

- This tab shows the overview of the policy reform, - including headline metrics and a summary of key impacts - across income deciles. -

-
-
-
- + + + Title order 1 (h1) + Title order 2 (h2) + Title order 3 (h3) + Title order 4 (h4) + Title order 5 (h5) + Title order 6 (h6) + + + + + + + + Small + + + + Medium + + + + Large + + + + + + + + Default alert + + This is a default alert with no variant specified. + + + + Destructive alert + + Something went wrong with the simulation. Please check your inputs. + + + + + + + + + + + + + + + + + + + + + + +
+ Simulation progress: {progressValue}% + +
+
+ Loading data: 30% + +
+
+
+
+ + {/* ================================================================ */} + {/* PRIMITIVES — Buttons, Badges, Cards */} + {/* ================================================================ */} +
+ +
+ + + + + + +
+
+ + + + +
+
+ + + + +
+
+ + +
+
+ + +
+ Default + Secondary + Outline + Success + Warning + Error + Ghost + Destructive +
+
+ + +
- -

- Distributional analysis shows how the reform affects - different income groups, with the bottom 5 deciles - gaining and the top 4 deciles bearing the net cost. + + Universal Basic Income + + $500/month payment to every adult citizen + + + + + + +

+ This reform would provide a monthly payment of $500 to every + adult US citizen, funded through a combination of income tax + increases and spending reductions.

+ + + +
- - - -

- The budget impact analysis estimates this reform would - cost approximately $160 billion annually, partially - offset by increased economic activity. + + Child Tax Credit expansion + + Increase CTC from $2,000 to $3,600 per child + + + +

+ Expanding the Child Tax Credit to $3,600 per child under 6 + and $3,000 for children 6-17, with full refundability.

+ + +
-
- - -
- - {/* ================================================================ */} - {/* LAYOUT */} - {/* ================================================================ */} -
- -
- } - actions={ - <> - Research - About - Donate - - - } - > - - UBI Calculator - -
-
- - -
- } - actions={ - <> - Research - About - Donate - - - } - > - - Policy calculator - -
-
- - -
- - - Teal wordmark (SVG) - tealWordmark - - - - - Teal wordmark (PNG) - tealWordmarkPng - - - - - White wordmark (SVG) - whiteWordmark - - - - - White wordmark (PNG) - whiteWordmarkPng - - - - - Teal square (SVG) - tealSquare - - - - - Teal square (PNG) - tealSquarePng - - - - - Teal square transparent (PNG) - tealSquareTransparent +
+
+ + + +
+ Default variant: + + + Overview + Distributional + Budget + + + + + + Overview of the policy reform with headline metrics. + + + + + + + + + Distributional analysis across income groups. + + + + + + + + + Budget impact analysis with revenue projections. + + + + + +
+
+ Line variant: + + + Household + Society-wide + + + Household-level impacts for selected parameters. + + + Society-wide distributional and budgetary impacts. + + +
+
+
+ + + + + Size sm: + + + + Size xs: + + + + + + + + Content above separator + + Content below separator + + Left + + Center + + Right + + + +
+ + {/* ================================================================ */} + {/* PRIMITIVES — Form Controls */} + {/* ================================================================ */} +
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +