# Reconcile table

Rows matched between two sources, side by side: the ones that agree, the ones in only one source, and the ones that conflict, with the fields that differ marked and both values shown. Each row that needs it offers the resolve actions your app lists (take one side, edit, skip); nothing is resolved for the person, and a row reads as resolved only once your app records it through `resolution`. A summary says how many are left, the list filters by status, and a large set pages.

- Registry name: `reconcile-table` (`@candor/reconcile-table`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/reconcile-table

## Install

```bash
npx candor-ui add reconcile-table
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/reconcile-table`.
It brings in: bidi, button, heading-level, heading-scope, locale, locked, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { ReconcileTable, type ReconcileAction, type ReconcileRow } from "@/components/candor/reconcile-table"

const fields = [
  { id: "supplier", label: "Supplier" },
  { id: "amount", label: "Amount" },
  { id: "due", label: "Due" },
]

const found: ReconcileRow[] = [
  { id: "inv-2041", label: "INV-2041", left: { supplier: "Northwind", amount: "1,200.00", due: "1 Dec 2026" }, right: { supplier: "Northwind", amount: "1,200.00", due: "1 Dec 2026" } },
  { id: "inv-2042", label: "INV-2042", left: { supplier: "Fabrikam", amount: "860.00", due: "3 Dec 2026" }, right: { supplier: "Fabrikam", amount: "680.00", due: "3 Dec 2026" } },
  { id: "inv-2043", label: "INV-2043", left: { supplier: "Tailspin", amount: "95.50", due: "5 Dec 2026" }, right: { supplier: "Tailspin Toys", amount: "95.50", due: null } },
  { id: "inv-2044", label: "INV-2044", left: { supplier: "Contoso", amount: "2,310.00", due: "8 Dec 2026" } },
  { id: "led-7781", label: "LED-7781", right: { supplier: "Litware", amount: "140.00", due: "30 Nov 2026" } },
]

const actions: ReconcileAction[] = [
  { id: "take-left", label: "Take invoice", for: ["conflict"] },
  { id: "take-right", label: "Take ledger", for: ["conflict"] },
  { id: "post", label: "Post to ledger", for: ["left-only"] },
  { id: "skip", label: "Skip" },
]

export default function Example() {
  const [rows, setRows] = React.useState(found)
  const set = (id: string, patch: Partial<ReconcileRow>) => setRows((all) => all.map((r) => (r.id === id ? ({ ...r, ...patch } as ReconcileRow) : r)))
  return (
    <ReconcileTable
      className="max-w-3xl"
      title="November invoices against the ledger"
      sources={{ left: "Invoice", right: "Ledger" }}
      fields={fields}
      rows={rows}
      actions={actions}
      onResolve={(row, action) => {
        // Your server records the choice; the row reads as resolved once it has. An Edit action would open your own form first.
        set(row.id, { cannotResolve: "Saving your choice…" })
        window.setTimeout(() => set(row.id, { cannotResolve: undefined, resolution: action }), 800)
      }}
    />
  )
}
```

## <ReconcileTable>

Props:

- `sources: { left: string; right: string }`: The two sources' names, such as "Invoice" and "Ledger".
- `fields: ReconcileField[]`: The fields to compare, one column each, in order.
- `rows: ReconcileRow[]`: The rows, in the order to show them.
- `actions: ReconcileAction[]`: The ways to resolve a row, in the order to offer them. None is chosen for the person.
- `onResolve: (row: ReconcileRow, action: string) => void`: Called with the row and the action's id when the person picks one. Record it on your server, then pass the id back as the row's `resolution`. For an edit, open your own editor first and record what the person saved.
- `pageSize?: number | undefined` (default `25`): Rows per page. Defaults to 25.
- `title?: string | undefined`: A heading over the table, also its accessible name. Defaults to "Reconciliation" for the name, with no heading shown.
- `headingLevel?: HeadingLevel | undefined`: The heading's level when `title` is set. Defaults to one below the nearest `HeadingScope`, else 2 (`<h2>`).
- `className?: string | undefined`

## Strings (`reconcileTable`)

The fixed words, English by default. Override any of them under `reconcileTable` in `<LocaleProvider strings={{ reconcileTable: { … } }}>`; the table is exported as `reconcileTableStrings`.

- `label`: Reconciliation
- `table`: Rows from {left} and {right}
- `openCount`: {open} to resolve
- `resolvedCount`: {resolved} resolved
- `matchedCount`: {matched} matched
- `show`: Show
- `all`: All
- `open`: To resolve
- `conflict`: Conflicts
- `only`: Only in {source}
- `matched`: Matched
- `option`: {label} {count}
- `record`: Record
- `status`: Status
- `resolve`: Resolve
- `differ`: { one: "{count} field differs", other: "{count} fields differ" }
- `notIn`: Not in {source}
- `notSet`: Not set
- `both`: {left}: {leftValue}; {right}: {rightValue}
- `nothingToResolve`: Nothing to resolve
- `noActions`: No way to resolve this here.
- `resolvedAs`: Resolved: {action}
- `resolved`: Resolved
- `change`: Change
- `changeFor`: Change how {record} is resolved
- `actionFor`: {action}: {record}
- `empty`: No rows to reconcile.
- `noneShown`: No rows of this kind.
- `showing`: Showing {from}–{to} of {total}
- `pages`: Pages
- `previousPage`: Previous page
- `nextPage`: Next page
- `firstPage`: This is the first page.
- `lastPage`: This is the last page.
- `page`: {page} / {pages}

## Types

### ReconcileValues

A record's values in one source, by field id, as text your app formatted. `null` means the source has no value.

```ts
Record<string, string | null | undefined>
```

### ReconcileStatus

How a row stands between the two sources.

```ts
"matched" | "conflict" | "left-only" | "right-only"
```

### ReconcileRow

One row: matched between both sources, or found in only one of them.

```ts
RowBase &
  ({ left: ReconcileValues; right: ReconcileValues } | { left: ReconcileValues; right?: undefined } | { left?: undefined; right: ReconcileValues })
```

### ReconcileField

A column: one field both sources may have.

```ts
{ id: string; label: string }
```

### ReconcileAction

A way to resolve a row that your app lists, such as take the left value, take the right one, edit, or skip. `for` says which rows offer it; it defaults to every row that isn't matched.

```ts
{ id: string; label: string; for?: ReconcileStatus[] | undefined }
```

## Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

### HeadingLevel

Import: `import type { HeadingLevel } from "@/lib/candor/heading-level"` (item `heading-level`, https://candor.themethodic.com/docs/heading-level.md)

A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.

```ts
1 | 2 | 3 | 4 | 5 | 6
```

## Motion

- **tint** (tint): A filter option changes its background when chosen, and a ring fades in on keyboard focus.

Set with `--cd-motion-reconcile-table-<moment>-*` in your brand file; the families are in the design reference.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
