@azex/ledger-react
v0.5.1
Published
React UI + data-layer for the [azex-ai/ledger](https://github.com/azex-ai/ledger) double-entry ledger engine. Ships typed hooks (TanStack Query), a router-agnostic sidebar, dashboard widgets, ready-made admin **page** components, and an all-in-one `<Ledge
Readme
@azex/ledger-react
React UI + data-layer for the azex-ai/ledger
double-entry ledger engine. Ships typed hooks (TanStack Query), a router-agnostic
sidebar, dashboard widgets, ready-made admin page components, and an
all-in-one <LedgerAdmin/> shell.
Install
This package is published to the public npm registry — install it directly, no registry config or auth token required:
npm install @azex/ledger-react @tanstack/react-queryPeer deps: react@^19, react-dom@^19, @tanstack/react-query@^5.
Setup
Wrap your app in
<LedgerProvider>with the ledger API base URL (and optional API key). It owns a TanStack QueryClient unless you pass your own.import { LedgerProvider } from "@azex/ledger-react"; <LedgerProvider config={{ baseUrl: "https://ledger.example.com", apiKey }}> {children} </LedgerProvider>Import the stylesheet once at your app root:
import "@azex/ledger-react/styles.css";Mount
<Toaster/>once so page actions can surface toast feedback. Use the re-exported sonnerToaster(no direct sonner dependency needed):import { Toaster } from "@azex/ledger-react"; <Toaster theme="dark" position="bottom-right" />If you use
<LedgerAdmin/>(below), it mounts its own<Toaster/>— skip this step.
Usage
Option A — <LedgerAdmin/> (zero routing)
The convenience shell renders the sidebar + content area, switches sections via
internal state (no URL), and self-mounts <Toaster/>. Chart-bearing pages are
lazy-loaded so recharts never enters your initial bundle.
import { LedgerProvider, LedgerAdmin } from "@azex/ledger-react";
import "@azex/ledger-react/styles.css";
export default function Admin() {
return (
<LedgerProvider config={{ baseUrl: "https://ledger.example.com" }}>
<LedgerAdmin />
</LedgerProvider>
);
}Option B — individual pages wired to your router
Import the *Page components and wire them to your host router. Each is a
"use client" component. Pages that link out accept an injectable
linkComponent (defaults to a plain <a>); JournalDetailPage takes the
journal id as a prop (extract it from your route param).
import {
JournalsPage,
JournalDetailPage,
ReservationsPage,
DepositsPage,
WithdrawalsPage,
ClassificationsPage,
JournalTypesPage,
TemplatesPage,
CurrenciesPage,
ReconciliationPage,
SnapshotsPage,
} from "@azex/ledger-react";Chart-bearing pages — import from @azex/ledger-react/charts
DashboardPage and BalancesPage render recharts charts, so they ship from
the ./charts subpath to keep recharts out of the root barrel. Import them
(and the BalanceTrend widget) from there:
import { DashboardPage, BalancesPage, BalanceTrend } from "@azex/ledger-react/charts";Server prefetch (RSC) — import from @azex/ledger-react/server
For React Server Components / Route Handlers, prefetch ledger data on the
server and hydrate the client hooks with no client-side waterfall. The /server
entry has no "use client" directive and is server-only:
Never import
@azex/ledger-react/serverfrom a client component.createServerLedgerClienttakes the server API key — keeping this entry off the client barrel ensures the server key never reaches the client bundle.
// app/journals/page.tsx (server component)
import { QueryClient, HydrationBoundary, dehydrate } from "@tanstack/react-query";
import { JournalsPage } from "@azex/ledger-react";
import {
createServerLedgerClient,
prefetchJournals,
} from "@azex/ledger-react/server";
export default async function Page() {
const queryClient = new QueryClient();
const client = createServerLedgerClient({ baseUrl, apiKey }); // server-side key
await prefetchJournals(queryClient, client, 20);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<JournalsPage linkComponent={YourLink} />
</HydrationBoundary>
);
}Available prefetch* helpers: prefetchJournals, prefetchEntries,
prefetchBalances, prefetchSystemHealth, prefetchSystemBalances,
prefetchReservations, prefetchClassifications, prefetchCurrencies,
prefetchJournalTypes, prefetchTemplates, prefetchSnapshots. The shared
ledgerKeys query-key factory is also exported for advanced cache seeding.
HeroUI skin — @azex/ledger-react/heroui
Building on HeroUI v3 instead of the default shadcn-style components? The same
admin surface (provider, <LedgerAdmin/>, all pages) ships as a second skin
on the ./heroui subpath, backed by the identical headless core:
import { LedgerAdmin, LedgerProvider } from "@azex/ledger-react/heroui";Host contract: @heroui/react + @heroui/styles installed (optional peer —
consumers of the default skin never need it), Tailwind v4 configured, and one
stylesheet import for the skin's layout classes:
/* globals.css */
@import "tailwindcss";
@import "@heroui/styles";
@import "@azex/ledger-react/heroui.css";Theme (light/dark, palette) is owned by the host's HeroUI setup — the skin renders with whatever theme the surrounding app defines.
Headless — @azex/ledger-react/headless
The UI-free core both skins build on (typed client + provider + every TanStack Query hook) is importable directly for hosts that bring their own components:
import { LedgerProvider, useJournals } from "@azex/ledger-react/headless";End-user wallet — @azex/ledger-react/wallet
The holder-scoped wallet surface for YOUR users (not operators): balances
(available / pending / on hold), translated transaction history, refund
markers. Read-only by design — top-up / cash-out flows stay in the host
product and slot in via actions.
import { WalletPanel, WalletProvider } from "@azex/ledger-react/wallet";
<WalletProvider config={{ baseUrl: "/api/v1", getToken: fetchWalletToken }}>
<WalletPanel actions={<TopUpButton />} kindLabels={{ deposit_confirm: "Top up" }} />
</WalletProvider>;getToken returns a holder token your backend mints (HTTP
POST /holder-tokens or in-process server.MintHolderToken); it is called
lazily and once more after a 401. Omit it behind a same-origin BFF.
Components: WalletPanel, WalletBalances, WalletBalanceCard,
TransactionList. HeroUI variant at @azex/ledger-react/wallet/heroui,
UI-free core (client + hooks) at @azex/ledger-react/wallet/headless.
Theming
The Provider (and <LedgerAdmin/>) render a <div className="ledger-root">
wrapper. All design tokens are scoped under .ledger-root so importing the
stylesheet never leaks tokens into your host app — and the stylesheet is
self-contained: it ships its own .ledger-root-scoped preflight (fonts,
resets, table borders), so it renders correctly even in a host with no
Tailwind setup at all.
Default appearance is system (follows the OS via
prefers-color-scheme); pass appearance: "dark" or "light" to force a
variant:
<LedgerProvider config={{ baseUrl, appearance: "dark" }}>Re-theme by overriding the CSS custom properties:
.ledger-root {
--primary: oklch(0.6 0.2 250);
--radius: 0.5rem;
}Or pass per-instance overrides inline via the provider theme prop (applied as
inline style on the .ledger-root div):
<LedgerProvider config={{ baseUrl, theme: { "--primary": "oklch(0.6 0.2 250)" } }}>Reference integration
The web/ app in azex-ai/ledger is the
working reference integration — it consumes this package as its only ledger
UI/data source (dogfood) and demonstrates both the client provider setup and
the /server RSC prefetch pattern with a Next.js linkComponent adapter.
Exports
- Root (
@azex/ledger-react) —LedgerProvider,useLedgerClient,createLedgerClient, all hooks (useJournals,useBalances,useReservations, …),Sidebar,LEDGER_NAV_ITEMS,HealthCards,RecentJournals,StatusBadge, the 11 non-chart*Pagecomponents,LedgerAdmin, andToaster. @azex/ledger-react/charts—DashboardPage,BalancesPage,BalanceTrend(recharts-backed).@azex/ledger-react/server(server-only) —createServerLedgerClient, theprefetch*helpers, andledgerKeys. No"use client"directive; never import from a client component.@azex/ledger-react/styles.css— bundled Tailwind styles.
The complete API reference (per-hook signatures, endpoints, component props, prefetch helpers, query-key factory) lives in the repo at docs/frontend.md.
Releasing
Publishing is tag-driven (CI workflow ledger-react-publish.yml). The tag
version must match package.json — CI fails fast otherwise.
Bump
versioninweb/packages/ledger-react/package.json.Commit the bump.
Tag and push:
git tag ledger-react-v<version> # e.g. ledger-react-v0.1.0 git push --tags
The workflow re-runs the full verify gate (typecheck, test, build, artifact
assertions), asserts the tag matches package.json, then publishes to GitHub
Packages.
