# Narrative block

Report prose as claims, each followed by what it rests on: a source as a `Citation` marker, a section of the report as a mark that opens it. A claim that states a figure and gives no evidence is never shown as plain fact: it is muted, underlined with a dashed line and followed by "No evidence given for this figure". A claim with no figure and no evidence reads as ordinary prose. The claims are model output, drawn as text.

- Registry name: `narrative-block` (`@candor/narrative-block`)
- 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/narrative-block

## Install

```bash
npx candor-ui add narrative-block
```

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

## Example

```tsx
"use client"

import { NarrativeBlock } from "@/components/candor/narrative-block"

// The second claim states a figure with nothing behind it, so it is shown as unsupported rather than as fact.
export default function Example() {
  return (
    <NarrativeBlock
      className="max-w-xl"
      heading="Summary"
      onOpenSection={() => {}}
      claims={[
        {
          id: "growth",
          text: "Pipeline grew 12% against last quarter, mostly in the mid-market segment.",
          evidence: [{ id: "q1", marker: 1, kind: "query", label: "Pipeline by segment", origin: "Query on opportunities" }, { section: 2, title: "Pipeline" }],
        },
        { id: "win", text: "Win rate fell to 18%." },
        { id: "q3", text: "Q3 deals are concentrated in late stages.", numeric: false, evidence: [{ section: 3, title: "Stages" }] },
      ]}
    />
  )
}
```

## <NarrativeBlock>

Props:

- `claims: NarrativeClaim[]`: The prose, as claims in reading order.
- `onOpenSection?: ((number: number) => void) | undefined`: Opens a section of the report by its number. Without it a section is named but isn't a button.
- `heading?: string | undefined`: A heading over the prose, "Summary".
- `headingLevel?: HeadingLevel | undefined`: The heading's level in the page's outline. Defaults to one below the nearest `HeadingScope`, else 2 (`<h2>`).
- `className?: string | undefined`: Classes for the outer box.

## Functions

### claimStatesNumber

```ts
claimStatesNumber(text: string)
```

Whether `text` holds a decimal digit in any script (`3`, `٣`, `३`): a claim that does states a figure.

## Strings (`narrativeBlock`)

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

- `unsupported`: No evidence given for this figure
- `sectionMarker`: §{number}
- `sectionLabel`: Section {number}: {title}

## Types

### NarrativeSection

A section of the report a claim rests on, opened through `onOpenSection`.

```ts
{
  /** The section's number, as `ReportSection` shows it. */
  section: number;
  /** The section's title. */
  title: string;
}
```

### NarrativeEvidence

What a claim rests on: a source (the free `Citation`'s shape, a document, a query, a web page) or a section of the report.

```ts
MessageSource | NarrativeSection
```

### NarrativeClaim

One claim in the prose and what it rests on.

```ts
{
  /** Stable across renders. */
  id: string;
  /** The claim's words, from the model: drawn as text. */
  text: string;
  /** The sources and sections the claim rests on. A claim that states a figure and has none is shown as unsupported. */
  evidence?: NarrativeEvidence[] | undefined;
  /**
   * Whether the claim states a figure. Left out, a claim states one when its text holds a digit (in any script). Set it
   * `false` for digits that aren't a figure ("Q3", a year) and `true` for a figure in words ("twelve accounts").
   */
  numeric?: boolean | 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
```

### MessageSource

Import: `import type { MessageSource } from "@/lib/candor/chat"` (item `chat`, https://candor.themethodic.com/docs/chat.md)

```ts
{
  id: string;
  /** The [n] shown inline. Assigned by the service, so it is stable across renders. */
  marker: number;
  kind: SourceKind;
  label: string;
  /** An inert excerpt shown in the popover. Text only. */
  snippet?: string | undefined;
  /** Where it came from in a human sense: "Docs · Sales process v3", "Query on orders". */
  origin?: string | undefined;
  provisional?: boolean | undefined;
}
```

### SourceKind

Import: `import type { SourceKind } from "@/lib/candor/chat"` (item `chat`, https://candor.themethodic.com/docs/chat.md)

"document", "web", "query", "code", "data", "workflow", "knowledge", or the app's own; unknown kinds read as "Source".

```ts
string
```

## Motion

- **tint** (tint): A section mark's border and text colour change when the pointer is over it, and a ring fades in when it takes keyboard focus.

Set with `--cd-motion-narrative-block-<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`.
