# Repo & conventions

> 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.

Source: https://lopifi.com/docs/architecture/02-repo-and-conventions
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

# 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](../craft/01-component-anatomy.md). 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

```tsx
"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

```bash
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:**
```bash
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](../craft/03-quality-bar.md).** 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.
