Repo & conventions
finance-blocks/
├── README.md
├── DESIGN.md ← machine-readable token contract
├── registry.json
├── docs/ ← this documentation set
│
├── apps/
│ └── docs/ Next.js 15 App Router · MDX · the docs site
│ ├── app/
│ ├── content/ one MDX file per component
│ └── registry/ live examples, also used for visual regression
│
├── packages/
│ ├── theme/
│ │ ├── tokens.css the single source of token truth
│ │ ├── preset.ts Tailwind v3 preset
│ │ └── theme.css Tailwind v4 @theme
│ │
│ └── ledger/ the components, as they'll be copied
│ ├── lib/
│ │ ├── money.types.ts
│ │ ├── format-money.ts
│ │ ├── allocate.ts
│ │ ├── fiscal-period.ts
│ │ └── cn.ts
│ ├── hooks/
│ │ ├── use-density.ts
│ │ ├── use-live-value.ts
│ │ └── use-column-priority.ts
│ └── components/
│ ├── money.tsx
│ ├── money.README.md ← the four-layer spec
│ ├── money.test.tsx
│ └── …
│
└── scripts/
├── build-registry.ts
├── check-tokens.ts contrast + grid conformance, fails the build
└── check-registry.ts every item installs and typechecks
Naming
| Thing | Convention | Example |
|---|---|---|
| Files | kebab-case | balance-card.tsx |
| Components | PascalCase | BalanceCard |
| Hooks | use-{name}.ts |
use-density.ts |
| Props types | {Component}Props |
BalanceCardProps |
| Constants | UPPER_SNAKE | DEFAULT_DENSITY |
| CSS vars | kebab, semantic-first | --text-secondary, not --gray-600 |
| Registry items | kebab, matching the file | balance-card |
Tokens are named by role, never by value. --text-secondary, not --gray-600; --positive, not --green. A value-named token can't be re-themed and can't be renamed without a breaking change.
File conventions
- Named exports only. No default exports. No barrel files — direct imports keep the registry's file graph honest and keep tree-shaking predictable.
- One component per file, with its sub-parts co-located when they're never used independently (
Card,CardHeader,CardTitleincard.tsx). "use client"only when interaction demands it.Money,Figure,StatementRow,Ruleare server-safe and must stay that way — they're the ones that render in bulk.- Every component has a sibling
.README.mdcontaining the four-layer spec from component anatomy. The spec ships with the code, so a consumer who copies the component also gets the reasoning. - Imports:
@/*→./src/*. Consumers get@/components/ledger/*.
Component skeleton
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/ledger/cn";
const balanceCardVariants = cva(
"relative rounded-xl border border-[--border] bg-[--surface] p-6",
{
variants: {
size: { sm: "p-4", md: "p-6", lg: "p-6" },
interactive: {
true: "transition-transform duration-1 ease-out active:scale-[0.97] hover:-translate-y-px hover:shadow-[--elevation-1]",
false: "",
},
},
defaultVariants: { size: "md", interactive: false },
},
);
export interface BalanceCardProps
extends React.ComponentPropsWithoutRef<"div">,
VariantProps<typeof balanceCardVariants> {
value: Money | null;
polarity?: Polarity;
action?: React.ReactNode;
}
export const BalanceCard = React.forwardRef<HTMLDivElement, BalanceCardProps>(
({ className, size, interactive, value, polarity = "standard", action, ...props }, ref) => (
<div ref={ref} className={cn(balanceCardVariants({ size, interactive }), className)} {...props}>
{/* … */}
</div>
),
);
BalanceCard.displayName = "BalanceCard";
Every primitive: cva for variants, forwardRef, displayName, cn() merging a className, props spread to the root, extends the native element's props.
Testing
| Layer | Tool | What |
|---|---|---|
| Unit | Vitest | Formatters, allocation, fiscal periods. Property-based tests on money math — fast-check over random amounts and currencies. |
| Component | Vitest + Testing Library | Every state in the edge-case checklist |
| A11y | axe-core per story |
Blocking in CI |
| Visual | Playwright screenshots | Every component × {light, dark} × {default, compact} × {320, 768, 1280} |
| Motion | Playwright traces | Assert compositor-only: no layout/paint entries during an animation |
| Tokens | scripts/check-tokens.ts |
Contrast ratios computed from tokens; grid conformance on every spacing value |
| Registry | scripts/check-registry.ts |
Every item installs into a clean app and typechecks |
The money math gets property-based tests. formatMoney(parseMoney(x)) === x for all valid inputs, and allocate(total, weights).sum() === total for all inputs. These are the invariants the whole library rests on, and example-based tests won't find the edge that breaks them.
Commands
bun run dev # docs site
bun run build # docs + registry
bun run registry:build # emit /r/*.json
bun run registry:check # install every item into a scratch app, typecheck
bun run test # vitest
bun run test:visual # playwright
bun run test:a11y # axe across all stories
bun run check:tokens # contrast + grid conformance
bun x ultracite fix # lint + format
bun run typecheck
Pre-push, once, at the end of a change:
bun x ultracite fix && bun run typecheck && bun run build
Contribution rules
- Spec before code. The four-layer
.README.mdis reviewed and approved before implementation starts. A PR with code and no spec is closed. - No new token without a decision record. Adding a color, spacing value, or duration requires a note in the relevant
foundations/doc explaining what existing token was insufficient. - No
biome-ignore. Fix the code, or changebiome.jsoncwith a justification. - No committed
console.logordebugger. - No hardcoded credentials.
process.env, always. - Every PR runs the quality bar. The checklist is a PR template.
- A component is
stableonly after the next-day review. Merged ≠ stable. The status in its README says which.