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
+ Outline
+ Ghost
+ Destructive
+
+
+ Small
+ Default
+ Large
+ +
+
+
+ Disabled
+
+ Disabled outline
+
+
+
+
+
+
+ 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.
+
+
+
+ View details
+
+ Compare
+
+
+
+
+
+ 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.
+
+
+
+ View details
+
+
+
+
+
+
+
+
+ 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
+ Sign in
+ >
+ }
+ >
+
+ UBI Calculator
+
+
+
+
+
+
+ }
+ actions={
+ <>
+ Research
+ About
+ Donate
+ Sign in
+ >
+ }
+ >
+
+ Policy calculator
+
+
+
+
+
+
+
+
+
+ tealWordmark
+
+
+
+
+
+ tealWordmarkPng
+
+
+
+
+
+ whiteWordmark
+
+
+
+
+
+ whiteWordmarkPng
+
+
+
+
+
+ tealSquare
+
+
+
+
+
+ tealSquarePng
+
+
+
+
+
+ tealSquareTransparent
+
+
+
+
+
+ tealSquarePadded
+
+
+
+
+
+ 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',
+ },
+});