diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 0000000..0d39616 --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,43 @@ +name: PR checks + +on: + pull_request: + +jobs: + changelog: + name: Changelog fragment + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install towncrier + run: pip install towncrier + + - name: Check for changelog fragment + run: towncrier check --compare-with origin/main + + test: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run tests + run: bun run test diff --git a/changelog.d/.gitkeep b/changelog.d/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/changelog.d/demo-site.added.md b/changelog.d/demo-site.added.md new file mode 100644 index 0000000..88473d3 --- /dev/null +++ b/changelog.d/demo-site.added.md @@ -0,0 +1 @@ +Add demo site showcasing all components with light/dark Header variants and logo gallery. diff --git a/demo/Demo.tsx b/demo/Demo.tsx new file mode 100644 index 0000000..efb9d0f --- /dev/null +++ b/demo/Demo.tsx @@ -0,0 +1,754 @@ +import { useState } from 'react'; + +// Primitives +import { Button } from '../src/primitives/Button'; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from '../src/primitives/Card'; +import { Badge } from '../src/primitives/Badge'; +import { Tabs, TabsList, TabsTrigger, TabsContent } from '../src/primitives/Tabs'; + +// Layout +import { DashboardShell } from '../src/layout/DashboardShell'; +import { Header } from '../src/layout/Header'; +import { SidebarLayout } from '../src/layout/SidebarLayout'; +import { SingleColumnLayout } from '../src/layout/SingleColumnLayout'; +import { InputPanel } from '../src/layout/InputPanel'; +import { ResultsPanel } from '../src/layout/ResultsPanel'; + +// Inputs +import { CurrencyInput } from '../src/inputs/CurrencyInput'; +import { NumberInput } from '../src/inputs/NumberInput'; +import { SelectInput } from '../src/inputs/SelectInput'; +import { CheckboxInput } from '../src/inputs/CheckboxInput'; +import { SliderInput } from '../src/inputs/SliderInput'; +import { InputGroup } from '../src/inputs/InputGroup'; + +// Display +import { MetricCard } from '../src/display/MetricCard'; +import { SummaryText } from '../src/display/SummaryText'; +import { DataTable } from '../src/display/DataTable'; +import { PolicyEngineWatermark } from '../src/display/PolicyEngineWatermark'; + +// Charts +import { ChartContainer } from '../src/charts/ChartContainer'; +import { PEBarChart } from '../src/charts/PEBarChart'; +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'; + +// Assets +import { logos } from '../src/assets'; + +// --------------------------------------------------------------------------- +// Example data +// --------------------------------------------------------------------------- + +const barData = [ + { category: 'Income tax', amount: 320 }, + { category: 'Payroll tax', amount: 180 }, + { category: 'Corporate tax', amount: 95 }, + { category: 'Excise tax', amount: 45 }, + { category: 'Estate tax', amount: 22 }, + { category: 'UBI cost', amount: -480 }, +]; + +const lineData = Array.from({ length: 10 }, (_, i) => ({ + year: 2024 + i, + baseline: 11.5 - i * 0.15 + Math.random() * 0.5, + reform: 11.5 - i * 0.15 - 2.1 + Math.random() * 0.3, +})); + +const areaData = [ + { decile: '1st', income_tax: 200, payroll_tax: 800, benefits: -3200 }, + { decile: '2nd', income_tax: 600, payroll_tax: 1400, benefits: -2400 }, + { decile: '3rd', income_tax: 1200, payroll_tax: 2100, benefits: -1600 }, + { decile: '4th', income_tax: 2400, payroll_tax: 2800, benefits: -800 }, + { decile: '5th', income_tax: 4200, payroll_tax: 3600, benefits: -200 }, + { decile: '6th', income_tax: 6800, payroll_tax: 4200, benefits: 0 }, + { decile: '7th', income_tax: 10200, payroll_tax: 5100, benefits: 0 }, + { decile: '8th', income_tax: 15400, payroll_tax: 6000, benefits: 0 }, + { decile: '9th', income_tax: 24000, payroll_tax: 7200, benefits: 0 }, + { decile: '10th', income_tax: 62000, payroll_tax: 9400, benefits: 0 }, +]; + +const waterfallData = [ + { name: 'Income tax', value: 320 }, + { name: 'Payroll tax', value: 180 }, + { name: 'Corporate tax', value: 95 }, + { name: 'Excise tax', value: 45 }, + { name: 'UBI cost', value: -480 }, + { name: 'Net impact', value: 0, isTotal: true }, +]; + +const tableColumns = [ + { key: 'decile', header: 'Income decile' }, + { + key: 'avgIncome', + header: 'Average income', + align: 'right' as const, + format: (v: unknown) => formatCurrency(v as number), + }, + { + key: 'taxChange', + header: 'Tax change', + align: 'right' as const, + format: (v: unknown) => { + const n = v as number; + return n >= 0 ? `+${formatCurrency(n)}` : formatCurrency(n); + }, + }, + { + key: 'benefitChange', + header: 'Benefit change', + align: 'right' as const, + format: (v: unknown) => `+${formatCurrency(v as number)}`, + }, + { + key: 'netChange', + header: 'Net change', + align: 'right' as const, + format: (v: unknown) => { + const n = v as number; + return n >= 0 ? `+${formatCurrency(n)}` : formatCurrency(n); + }, + }, +]; + +const tableData = [ + { decile: '1st (poorest)', avgIncome: 12400, taxChange: 200, benefitChange: 6000, netChange: 5800 }, + { decile: '2nd', avgIncome: 24800, taxChange: 600, benefitChange: 6000, netChange: 5400 }, + { decile: '3rd', avgIncome: 36200, taxChange: 1200, benefitChange: 6000, netChange: 4800 }, + { decile: '4th', avgIncome: 48600, taxChange: 2400, benefitChange: 6000, netChange: 3600 }, + { decile: '5th', avgIncome: 62000, taxChange: 4200, benefitChange: 6000, netChange: 1800 }, + { decile: '6th', avgIncome: 78500, taxChange: 6800, benefitChange: 6000, netChange: -800 }, + { decile: '7th', avgIncome: 98000, taxChange: 10200, benefitChange: 6000, netChange: -4200 }, + { decile: '8th', avgIncome: 128000, taxChange: 15400, benefitChange: 6000, netChange: -9400 }, + { decile: '9th', avgIncome: 185000, taxChange: 24000, benefitChange: 6000, netChange: -18000 }, + { decile: '10th (richest)', avgIncome: 420000, taxChange: 62000, benefitChange: 6000, netChange: -56000 }, +]; + +const stateOptions = [ + { label: 'California', value: 'CA' }, + { label: 'New York', value: 'NY' }, + { label: 'Texas', value: 'TX' }, + { label: 'Florida', value: 'FL' }, + { label: 'Washington', value: 'WA' }, +]; + +const filingOptions = [ + { label: 'Single', value: 'single' }, + { label: 'Married filing jointly', value: 'mfj' }, + { label: 'Married filing separately', value: 'mfs' }, + { label: 'Head of household', value: 'hoh' }, +]; + +// --------------------------------------------------------------------------- +// Section wrapper +// --------------------------------------------------------------------------- + +function Section({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

+ {title} +

+ {children} +
+ ); +} + +function SubSection({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

+ {title} +

+ {children} +
+ ); +} + +// --------------------------------------------------------------------------- +// Demo app +// --------------------------------------------------------------------------- + +export function Demo() { + const [income, setIncome] = useState(75000); + const [dependents, setDependents] = useState(2); + const [state, setState] = useState('CA'); + const [filing, setFiling] = useState('mfj'); + const [includeState, setIncludeState] = useState(true); + const [ubiAmount, setUbiAmount] = useState(500); + + return ( + +
+
+

+ @policyengine/ui-kit +

+

+ Component gallery — every component rendered with example data +

+
+ + {/* ================================================================ */} + {/* PRIMITIVES */} + {/* ================================================================ */} +
+ +
+ + + + +
+
+ + + + +
+
+ + +
+
+ + +
+ Default + Secondary + Outline + Success + Warning + Error +
+
+ + +
+ + + 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. +

+
+
+
+ + + +

+ 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. +

+
+
+
+ + + +

+ The budget impact analysis estimates this reform would + cost approximately $160 billion annually, partially + offset by increased economic activity. +

+
+
+
+
+
+
+ + {/* ================================================================ */} + {/* 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 + + + + + Teal square padded (SVG) + tealSquarePadded + + + + + White square (SVG) + whiteSquare + + +
+
+ + +
+ + + + + + + + + + + + + } + > + +
+ + + +
+
+
+
+
+ + +
+ +

+ Policy summary +

+ + This reform introduces a $500 monthly Universal + Basic Income for all adult US citizens. The + program would be funded through a combination of income + tax surcharges on high earners and reductions in existing + means-tested transfer programs. The bottom 50% of + households would see a net income increase, while the top + 30% would see a net decrease. + +
+
+
+
+ + {/* ================================================================ */} + {/* INPUTS */} + {/* ================================================================ */} +
+
+ + +
+ + + + +
+
+
+ + +
+ + `$${v}`} + /> + + {}} + min={0} + max={100} + /> + {}} + min={0} + /> + {}} + /> + +
+
+
+
+
+ + {/* ================================================================ */} + {/* DISPLAY */} + {/* ================================================================ */} +
+ +
+ + + + +
+
+ + + + Under this reform, a married couple in California earning{' '} + {formatCurrency(income)} per year with{' '} + {dependents} dependents would receive{' '} + $12,000 in annual UBI payments and pay an + additional $8,200 in income taxes, for a{' '} + + net gain of $3,800 + {' '} + per year. + + + + + + + + + + +
+ + {/* ================================================================ */} + {/* CHARTS */} + {/* ================================================================ */} +
+
+ + Download CSV + + } + > + `$${v}B`} + /> + + + + + `${v.toFixed(1)}%`} + /> + + + + + formatCurrency(v)} + /> + + + + + `$${v}B`} + /> + + +
+
+ +
+ @policyengine/ui-kit v0.1.0 — All components shown with example data +
+
+
+ ); +} diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..5b00f56 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,18 @@ + + + + + + @policyengine/ui-kit — Component Gallery + + + + + +
+ + + diff --git a/demo/main.tsx b/demo/main.tsx new file mode 100644 index 0000000..07de5f6 --- /dev/null +++ b/demo/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import '../src/app.css'; +import { Demo } from './Demo'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/package.json b/package.json index 527902c..6cb0d3c 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ ], "scripts": { "dev": "vite", + "dev:demo": "vite --config vite.demo.config.ts", "build": "vite build && mv dist/ui-kit.css dist/styles.css && tsc -p tsconfig.build.json --emitDeclarationOnly", "test": "vitest run", "test:watch": "vitest", diff --git a/src/assets/index.ts b/src/assets/index.ts new file mode 100644 index 0000000..240b939 --- /dev/null +++ b/src/assets/index.ts @@ -0,0 +1 @@ +export { logos } from './logos'; diff --git a/src/assets/logos/index.ts b/src/assets/logos/index.ts new file mode 100644 index 0000000..0cdb483 --- /dev/null +++ b/src/assets/logos/index.ts @@ -0,0 +1,21 @@ +import tealWordmark from './policyengine/teal.svg'; +import tealWordmarkPng from './policyengine/teal.png'; +import tealSquare from './policyengine/teal-square.svg'; +import tealSquarePng from './policyengine/teal-square.png'; +import tealSquareTransparent from './policyengine/teal-square-transparent.png'; +import tealSquarePadded from './policyengine/teal-square-padded.svg'; +import whiteWordmark from './policyengine/white.svg'; +import whiteWordmarkPng from './policyengine/white.png'; +import whiteSquare from './policyengine/white-square.svg'; + +export const logos = { + tealWordmark, + tealWordmarkPng, + tealSquare, + tealSquarePng, + tealSquareTransparent, + tealSquarePadded, + whiteWordmark, + whiteWordmarkPng, + whiteSquare, +} as const; diff --git a/src/assets/logos/policyengine/teal-square-padded.svg b/src/assets/logos/policyengine/teal-square-padded.svg new file mode 100644 index 0000000..d4e0b81 --- /dev/null +++ b/src/assets/logos/policyengine/teal-square-padded.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/logos/policyengine/teal-square-transparent.png b/src/assets/logos/policyengine/teal-square-transparent.png new file mode 100644 index 0000000..8787fbe Binary files /dev/null and b/src/assets/logos/policyengine/teal-square-transparent.png differ diff --git a/src/assets/logos/policyengine/teal-square.png b/src/assets/logos/policyengine/teal-square.png new file mode 100644 index 0000000..617f558 Binary files /dev/null and b/src/assets/logos/policyengine/teal-square.png differ diff --git a/src/assets/logos/policyengine/teal-square.svg b/src/assets/logos/policyengine/teal-square.svg new file mode 100644 index 0000000..c6d967c --- /dev/null +++ b/src/assets/logos/policyengine/teal-square.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/logos/policyengine/teal.png b/src/assets/logos/policyengine/teal.png new file mode 100644 index 0000000..49f5145 Binary files /dev/null and b/src/assets/logos/policyengine/teal.png differ diff --git a/src/assets/logos/policyengine/teal.svg b/src/assets/logos/policyengine/teal.svg new file mode 100644 index 0000000..81fbe04 --- /dev/null +++ b/src/assets/logos/policyengine/teal.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/logos/policyengine/white-square.svg b/src/assets/logos/policyengine/white-square.svg new file mode 100644 index 0000000..f96f1c7 --- /dev/null +++ b/src/assets/logos/policyengine/white-square.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/logos/policyengine/white.png b/src/assets/logos/policyengine/white.png new file mode 100644 index 0000000..04a7029 Binary files /dev/null and b/src/assets/logos/policyengine/white.png differ diff --git a/src/assets/logos/policyengine/white.svg b/src/assets/logos/policyengine/white.svg new file mode 100644 index 0000000..253c046 --- /dev/null +++ b/src/assets/logos/policyengine/white.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 036f4b2..1135a2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,3 +21,6 @@ export * from './display'; // Charts export * from './charts'; + +// Assets +export * from './assets'; diff --git a/src/inputs/CheckboxInput.tsx b/src/inputs/CheckboxInput.tsx index 82113d5..005daae 100644 --- a/src/inputs/CheckboxInput.tsx +++ b/src/inputs/CheckboxInput.tsx @@ -24,7 +24,7 @@ export const CheckboxInput = forwardRef( type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} - className="tw:h-4 tw:w-4 tw:rounded tw:border tw:border-border-light tw:text-primary tw:focus:ring-2 tw:focus:ring-ring tw:cursor-pointer" + className="tw:h-4 tw:w-4 tw:rounded tw:border tw:border-border-light tw:accent-primary tw:focus:ring-2 tw:focus:ring-ring tw:cursor-pointer" style={styles?.input} {...props} /> diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx index a117fe3..4805356 100644 --- a/src/layout/Header.tsx +++ b/src/layout/Header.tsx @@ -1,29 +1,76 @@ +import { cva, type VariantProps } from 'class-variance-authority'; import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'; import { cn } from '../utils/cn'; -export interface HeaderProps extends HTMLAttributes { +const headerVariants = cva( + 'tw:flex tw:items-center tw:justify-between tw:h-[58px] tw:px-2xl', + { + variants: { + variant: { + light: 'tw:bg-white tw:border-b tw:border-border-light', + dark: 'tw:bg-primary-600 tw:text-white tw:shadow-md tw:border-b tw:border-border-dark', + }, + }, + defaultVariants: { variant: 'light' }, + }, +); + +const actionsVariants = cva( + 'tw:flex tw:items-center tw:gap-3xl', + { + variants: { + variant: { + light: + '[&_a]:tw:text-text-secondary [&_a]:tw:text-lg [&_a]:tw:font-medium [&_a]:tw:no-underline [&_a]:tw:hover:text-text-primary [&_button]:tw:text-text-secondary', + dark: + '[&_a]:tw:text-white [&_a]:tw:text-lg [&_a]:tw:font-medium [&_a]:tw:no-underline [&_a]:tw:hover:opacity-80 [&_button]:tw:text-white', + }, + }, + defaultVariants: { variant: 'light' }, + }, +); + +const subtitleVariants = cva( + 'tw:flex tw:items-center tw:gap-md', + { + variants: { + variant: { + light: '[&>span]:tw:text-text-secondary [&>span]:tw:text-lg [&>span]:tw:font-medium', + dark: '[&>span]:tw:text-white/70 [&>span]:tw:text-lg [&>span]:tw:font-medium', + }, + }, + defaultVariants: { variant: 'light' }, + }, +); + +export interface HeaderProps + extends HTMLAttributes, + VariantProps { logo?: ReactNode; actions?: ReactNode; styles?: { root?: React.CSSProperties }; } export const Header = forwardRef( - ({ logo, actions, className, styles, children, ...props }, ref) => ( + ({ logo, actions, variant, className, styles, children, ...props }, ref) => (
-
+
{logo} {children}
- {actions &&
{actions}
} + {actions && ( +
+ {actions} +
+ )}
), ); Header.displayName = 'Header'; + +export { headerVariants }; diff --git a/src/layout/index.ts b/src/layout/index.ts index 17b4cb3..285e50d 100644 --- a/src/layout/index.ts +++ b/src/layout/index.ts @@ -1,6 +1,6 @@ export { DashboardShell, type DashboardShellProps } from './DashboardShell'; export { SidebarLayout, type SidebarLayoutProps } from './SidebarLayout'; export { SingleColumnLayout, type SingleColumnLayoutProps } from './SingleColumnLayout'; -export { Header, type HeaderProps } from './Header'; +export { Header, headerVariants, type HeaderProps } from './Header'; export { InputPanel, type InputPanelProps } from './InputPanel'; export { ResultsPanel, type ResultsPanelProps } from './ResultsPanel'; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..2f1d21f --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,9 @@ +declare module '*.svg' { + const src: string; + export default src; +} + +declare module '*.png' { + const src: string; + export default src; +} diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 0000000..28e80a1 --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,21 @@ +[tool.towncrier] +directory = "changelog.d" +filename = "CHANGELOG.md" +title_format = "## [{version}] - {project_date}" +issue_format = "" +underlines = ["", "", ""] + +[tool.towncrier.fragment.breaking] +name = "Breaking changes" + +[tool.towncrier.fragment.added] +name = "Added" + +[tool.towncrier.fragment.changed] +name = "Changed" + +[tool.towncrier.fragment.fixed] +name = "Fixed" + +[tool.towncrier.fragment.removed] +name = "Removed" diff --git a/vite.demo.config.ts b/vite.demo.config.ts new file mode 100644 index 0000000..5fb864e --- /dev/null +++ b/vite.demo.config.ts @@ -0,0 +1,16 @@ +import { resolve } from 'path'; +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), react()], + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + server: { + open: '/demo/index.html', + }, +});