Files
zeekayandHanzo Dev 8339349a64 Ship @hanzo/finance-ui as compiled JavaScript
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]>
2026-09-04 09:18:22 -07:00

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.