Skip to content

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, CardTitle in card.tsx).
  • "use client" only when interaction demands it. Money, Figure, StatementRow, Rule are server-safe and must stay that way — they're the ones that render in bulk.
  • Every component has a sibling .README.md containing 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

  1. Spec before code. The four-layer .README.md is reviewed and approved before implementation starts. A PR with code and no spec is closed.
  2. 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.
  3. No biome-ignore. Fix the code, or change biome.jsonc with a justification.
  4. No committed console.log or debugger.
  5. No hardcoded credentials. process.env, always.
  6. Every PR runs the quality bar. The checklist is a PR template.
  7. A component is stable only after the next-day review. Merged ≠ stable. The status in its README says which.

Told when this spec changes

The docs are the contract the components are built against. When one changes, the component changes with it — and the copy in your repo does not. One email when that happens.

One address, no list rental, unsubscribe in one click.

Works in your QuickBooks Online or Xero file

Get your books closed every month

Lopifi is building AI bookkeeping that keeps your books closed on US GAAP as transactions happen, with a CPA review every quarter, for $200 a month. We’re not open yet.

$200/month when we open. No card now.