Every entry — main, module, types and both exports conditions — named a .ts file, and `files` shipped src. That works only for a consumer with a bundler in front of it: a Next BFF route, an edge worker or a vitest suite loads the package as JavaScript and parses `(v: unknown): v is number => typeof …` as a syntax error, so the suite fails to collect and reports a broken dependency. Node refuses to strip types under node_modules at all. The console's finance transport test hit this on the root barrel, and `./data` — the seam that exists precisely so a transport module never pulls the component graph — was raw source too. tsc now emits ESM with declarations to dist and the manifest names it. Relative specifiers carry .js in source so the emit is fully specified and resolves without a bundler; src still ships, so the source maps point at something real. prepublishOnly builds rather than typechecks: the artifact a publish uploads is now built, and it should be built fresh. gui.d.ts moves under src as the sibling @hanzo/canvas keeps it, which lets the build config set rootDir to src alone. Co-authored-by: Hanzo Dev <[email protected]>
3.7 KiB
@hanzo/finance-ui
Shared finance data-layer + UI for every Hanzo Finance surface —
finance.hanzo.ai (the tenant shell), the Hanzo Cloud console per-org Finance
module, and the console global-admin finance view. One FinanceClient, one
component set, monochrome, USD cents end to end — so a spend / usage / credits card
looks and behaves identically at every scope. Scope (which org, or all-orgs) is a
property of the injected transport + identity, never a different UI.
Built on the canonical Hanzo 8.x component stack: @hanzo/ui/product (the product
layer — DataTable, Sparkline, LineChart, StatusTag, …) on @hanzo/gui (the
Tamagui primitives). No Tailwind, no shadcn, no Radix, no hand-rolled CSS: every surface
themes off the shared Gui token system, so a finance card reads like any other console
module.
Install
npm i @hanzo/finance-ui @hanzo/ui @hanzo/gui
react / react-dom (≥19) and @hanzo/gui (≥8.0.1) / @hanzo/ui (≥8.0.39)
are peers — the HOST provides the GuiProvider, and it should mount
@hanzo/ui/gui-config (the one canonical scale) rather than a copy of it.
This package ships built dist — ESM with declarations — so a consumer that loads it as
JavaScript rather than through a bundler (a Node BFF route, an edge worker, a vitest
suite) gets a module it can parse. @hanzo/data (a @hanzo/ui peer) still ships TS
source and belongs in your bundler's transpilePackages; as of 8.x @hanzo/ui,
@hanzo/gui and every @hanzogui/* package ship built dist, as does this one.
Use
import { FinanceDashboard, stubFinanceClient, httpFinanceClient } from '@hanzo/finance-ui'
// Preview / landing / local dev — deterministic in-memory data, no backend.
<FinanceDashboard client={stubFinanceClient()} mode="preview" />
// Live — the host injects the transport (its own bearer-scoped fetch to /v1/finance/*).
const client = httpFinanceClient((path, query) => myBearerGet(`finance/${path}`, query))
<FinanceDashboard client={client} />
Swapping stubFinanceClient() ⇄ httpFinanceClient(transport) is a one-line change
with zero UI change — the components never know which one they got.
The /v1/finance/* contract
httpFinanceClient reads, per org (scoped server-side by the caller's bearer):
| Method | Path | Shape |
|---|---|---|
balance() |
GET /v1/finance/balance |
{ availableCents, pendingCents, dueCents, currency, asOf } |
credits() |
GET /v1/finance/credits |
Credit[] |
usage(range) |
GET /v1/finance/usage?range= |
{ totalCents, series[], lines[] } |
invoices() |
GET /v1/finance/invoices |
Invoice[] |
paymentMethods() |
GET /v1/finance/payment-methods |
PaymentMethod[] (masked) |
ledger(range) |
GET /v1/finance/ledger?range= |
LedgerEntry[] (double-entry) |
treasury() |
GET /v1/finance/treasury |
TreasurySummary |
Every payload is run through an optional-safe normalizer: a renamed/absent field
degrades to —, an unconfigured backend yields honest zeros/empty, and a card is
masked to brand + last4 (a PAN can never reach the UI). A rejected read propagates so
the host renders its own honest state — the client never fabricates.
Styling & theming
Monochrome, on the shared @hanzo/gui token system — no CSS import, no hand-rolled
stylesheet. The HOST mounts one GuiProvider (the console + finance.hanzo.ai both do);
the board renders inside it. <FinanceDashboard standalone> (the default) wraps the
board in a Theme scope + page background for a standalone host; pass
standalone={false} inside an app that already mounts its own GuiProvider/Theme
(the console). theme="light" | "dark" pins the scope; otherwise it follows the host.