dodil/erp-ui
src/gallery/main.tsx
/** The gallery: every erp-ui component, in the default theme, with realistic CRM data. `npm run dev`. */
import { Box, Button, Container, Flex, Heading, Section, Text } from "@radix-ui/themes";
import { useState } from "react";
import { createRoot } from "react-dom/client";
import {
ActionButton, ConfirmDialog, DataTable, DetailPage, EmptyState, ErpTheme, ErrorState, FilterBar, FormDialog, KanbanBoard,
Stat, StatGrid, Timeline, Toaster, formatMoney, sumDecimal, useToast, type Column,
} from "../index";
type Deal = { id: string; name: string; stage: string; amount: string };
const deals: Deal[] = [
{ id: "O-1", name: "Autumn print campaign", stage: "negotiation", amount: "48000.00" },
{ id: "O-2", name: "Lumen spring collection", stage: "proposal", amount: "24000.00" },
{ id: "O-3", name: "Baltic launch", stage: "proposal", amount: "42000.00" },
];
const stages = ["discovery", "proposal", "negotiation", "closed_won"];
function Actions() {
const toast = useToast();
return (
<Flex gap="2" wrap="wrap">
<FormDialog trigger={<Button>New deal</Button>} title="New deal" submitLabel="Open deal"
fields={[{ name: "name", label: "Name", required: true }, { name: "amount", label: "Amount", kind: "money", currency: "EUR" },
{ name: "close", label: "Expected close", kind: "date" }]}
onSubmit={(v) => toast({ title: "Deal opened", description: `${v.name} · ${formatMoney(v.amount, "EUR")}` })} />
<ConfirmDialog trigger={<Button color="red" variant="soft">Disqualify</Button>} title="Disqualify Karin Ek?" confirmLabel="Disqualify"
tone="danger" reason={{ label: "Why" }} onConfirm={() => toast({ title: "Disqualified" })} />
<ActionButton label="Approve discount" disabledReason="Only the deal desk approves discounts (quote:quote:approve)" />
</Flex>
);
}
type Partner = { bp_id: string; legal_name: string; country: string; status: string; open_pipeline: string; since: string };
const partners: Partner[] = [
{ bp_id: "54848654143250435", legal_name: "Acme AB", country: "SE", status: "active", open_pipeline: "124500.00", since: "2024-03-01" },
{ bp_id: "33077777037974209", legal_name: "Acme Group Ltd", country: "GB", status: "active", open_pipeline: "1850000.00", since: "2023-11-12" },
{ bp_id: "48281858453591338", legal_name: "Acme DE GmbH", country: "DE", status: "blocked", open_pipeline: "0", since: "2025-01-20" },
{ bp_id: "20998875476607981", legal_name: "Framework Co", country: "FR", status: "archived", open_pipeline: "9800.50", since: "2022-06-30" },
];
const columns: Column<Partner>[] = [
{ key: "legal_name", header: "Partner" },
{ key: "country", header: "Country" },
{ key: "status", header: "Status", kind: "status" },
{ key: "open_pipeline", header: "Open pipeline (EUR)", kind: "money" },
{ key: "since", header: "Customer since", kind: "date" },
];
function Gallery() {
const [q, setQ] = useState("");
const [board, setBoard] = useState(deals);
const [status, setStatus] = useState<string | null>(null);
const rows = partners.filter((p) => p.legal_name.toLowerCase().includes(q.toLowerCase()) && (!status || p.status === status));
return (
<Container size="4" p="5">
<Heading size="7" mb="1">erp-ui</Heading>
<Text color="gray">The core ERP components, in the default theme.</Text>
<Section size="1">
<Heading size="4" mb="3">List — FilterBar + DataTable</Heading>
<Flex direction="column" gap="3">
<FilterBar search={{ value: q, onChange: setQ, placeholder: "Search partners" }}
filters={[{ key: "status", label: "Status", value: status, onChange: setStatus,
options: [{ value: "active", label: "Active" }, { value: "blocked", label: "Blocked" }, { value: "archived", label: "Archived" }] }]} />
<DataTable label="Partners" rows={rows} columns={columns} rowKey={(r) => r.bp_id} onRowClick={() => {}} />
</Flex>
</Section>
<Section size="1">
<Heading size="4" mb="3">Record — DetailPage with another module's panel</Heading>
<Box>
<DetailPage
title="Acme DE GmbH" subtitle="acme-de · DE · subsidiary of Acme Group Ltd" status="blocked"
actions={[{ label: "Unblock", onClick: () => {}, tone: "primary" },
{ label: "New quote", onClick: () => {}, disabledReason: "Acme DE GmbH is blocked: unpaid invoices" }]}
sections={[{ title: "Partner", fields: [
{ label: "Tax id", value: "DE811234567" }, { label: "Country", value: "DE" },
{ label: "Roles", value: "customer, supplier" }, { label: "Open pipeline", value: "0", kind: "money", currency: "EUR" },
{ label: "Blocked since", value: "2026-10-02T09:12:00Z", kind: "datetime" }, { label: "Customer since", value: "2025-01-20", kind: "date" }] }]}
panels={[{ title: "Timeline", content: <Timeline items={[
{ id: "1", kind: "CALL", title: "Rang Anna about the overdue invoice", body: "Promised payment by Friday.", at: "2026-10-14T10:20:00Z", actor: "tom@co.example" },
{ id: "2", kind: "TASK", title: "Check the payment landed", at: "2026-10-17T00:00:00Z", actor: "tom@co.example", status: "open" },
{ id: "3", kind: "EMAIL", title: "Sent the statement", at: "2026-10-02T08:00:00Z", actor: "finance@co.example" },
]} /> }]}
/>
</Box>
</Section>
<Section size="1">
<Heading size="4" mb="3">Numbers — StatGrid</Heading>
<StatGrid label="Today">
<Stat label="Overdue" value="3" tone="danger" hint="tasks" />
<Stat label="Weighted" value={formatMoney(sumDecimal(board.map((d) => d.amount)), "EUR")} hint="exact, never a float" />
<Stat label="Won" value="12,000.00 EUR" tone="success" />
<Stat label="Open deals" value={board.length} />
</StatGrid>
</Section>
<Section size="1">
<Heading size="4" mb="3">Board — KanbanBoard (move with ⋯ or by dragging)</Heading>
<KanbanBoard label="Pipeline" items={board} itemKey={(d) => d.id} columnOf={(d) => d.stage} itemLabel={(d) => d.name}
columns={stages.map((s) => ({ id: s, title: s.replace("_", " "), closedReason: s === "closed_won" ? "needs an accepted quote" : undefined,
footer: <Text size="1" color="gray">{formatMoney(sumDecimal(board.filter((d) => d.stage === s).map((d) => d.amount)), "EUR")}</Text> }))}
renderCard={(d) => <Flex direction="column"><Text weight="medium">{d.name}</Text><Text size="2">{formatMoney(d.amount, "EUR")}</Text></Flex>}
onMove={(d, to) => setBoard((b) => b.map((x) => (x.id === d.id ? { ...x, stage: to } : x)))} />
</Section>
<Section size="1">
<Heading size="4" mb="3">Actions — FormDialog, ConfirmDialog, ActionButton, Toaster</Heading>
<Actions />
</Section>
<Section size="1">
<Heading size="4" mb="3">States — EmptyState, ErrorState</Heading>
<Flex direction="column" gap="3">
<EmptyState title="No leads yet" description="Marketing imports trade-show lists." action={<Button variant="soft">Import a list</Button>} />
<ErrorState error="missing permission: quote:quote:approve" onRetry={() => {}} />
</Flex>
</Section>
</Container>
);
}
createRoot(document.getElementById("root")!).render(<ErpTheme><Toaster><Gallery /></Toaster></ErpTheme>);