{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "balance-card", "type": "registry:component", "title": "Balance card", "description": "The available/reserved composition: available is the only figure above the rule; reserved demoted by position and scale, padlocked when unspendable.", "registryDependencies": [ "@venlyfinance/venly-tokens", "@venlyfinance/money" ], "files": [ { "path": "registry/components/balance-card.tsx", "type": "registry:component", "target": "~/components/venly/components/balance-card.tsx", "content": "import type { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport { Money } from \"../lib/money.js\";\n\n/**\n * Balance card – the available/reserved composition.\n *\n * Design contract encoded by this component:\n * - Available is the terminal, emphasised figure: ~2× everything else and\n * the ONLY figure above the hairline rule. The Send button spends it.\n * - Total is demoted below the rule beside the reserved buckets. Reserved\n * is marked by position and scale only – no colour, no weight change.\n * - A padlock glyph marks \"present but not yours to spend\"; it survives\n * greyscale and needs no legend.\n * - Bucket labels name the mechanism (\"Reserved against card authorisation,\n * releases 14 Mar\"), never a bare \"Reserved\".\n * - Masking is a labelled text control (never an ambiguous icon) and covers\n * the hero AND the demoted buckets – a masked hero beside visible deltas\n * leaks what masking promised to hide.\n * - Every reserved aggregate drills through to the records causing it:\n * pass onDrill on a bucket and it becomes a real control.\n * - No shadow: the card sits in the base layer, separated by a hairline.\n */\nexport interface BalanceBucket {\n /** Mechanism-naming label, e.g. \"Reserved in\" / \"Reserved out\". */\n label: string;\n amount: number | null | undefined;\n /** Marks unspendable money with the padlock glyph. */\n locked?: boolean;\n /** Drill-through to the records causing this bucket. */\n onDrill?: () => void;\n}\n\nexport interface BalanceCardProps {\n /** Caption above the hero figure. Defaults to \"Available\". */\n label?: string;\n available: number | null | undefined;\n currency?: string;\n /** Demoted figures below the rule: Total first, then reserved buckets. */\n buckets?: BalanceBucket[];\n /** Secondary line under the hero: a qualifier naming the figure. */\n qualifier?: ReactNode;\n /** Masks every figure on the card (hero and buckets alike). */\n masked?: boolean;\n /** When provided, renders the labelled Hide/Show control on the label row. */\n onToggleMasked?: () => void;\n style?: CSSProperties;\n className?: string;\n}\n\nexport function BalanceCard({\n label = \"Available\",\n available,\n currency,\n buckets = [],\n qualifier,\n masked = false,\n onToggleMasked,\n style,\n className,\n}: BalanceCardProps): ReactElement {\n return (\n \n \n {label}\n {onToggleMasked ? (\n \n {masked ? \"Show\" : \"Hide\"}\n \n ) : null}\n \n
\n \n
\n {qualifier !== undefined ? (\n \n {qualifier}\n \n ) : null}\n {buckets.length > 0 ? (\n \n {buckets.map((bucket) => {\n const body = (\n <>\n \n {bucket.locked ? (\n \n 🔒\n \n ) : null}\n {bucket.label}\n \n \n \n );\n return bucket.onDrill ? (\n \n {body}\n \n ) : (\n
\n {body}\n
\n );\n })}\n \n ) : null}\n \n );\n}\n" } ] }