# Graph view

A directed graph drawn from nodes that name what they come after, laid out on its own (layers, then an order that crosses few arrows; `lib/graph-layout`). Nodes are in an ordered list in the order given, so a screen reader reads them as a list, and the arrows are drawn only: say what they mean in each node's `content`. One node is in the tab order at a time; arrow keys move along the arrows and across a layer, mirrored right to left, and Home and End go to the first and last. With `onSelect` each node is a button.

Plan and RunTree use it for their graph view, and Diagram for diagrams given as nodes and edges. `GraphViewSwitch` is the List / Graph control they show when the work branches.

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

## Install

```bash
npx candor-ui add graph-view
```

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

## Example

```tsx
"use client"

import * as React from "react"
import { GraphView, GraphViewSwitch, type GraphViewNode } from "@/components/graph-view"

const step = (title: string, who: string, after?: string) => (
  <span className="flex min-w-0 flex-col">
    <span className="line-clamp-2 text-sm font-medium leading-snug">{title}</span>
    <span className="text-xs text-muted-foreground">{who}</span>
    {/* The arrows are drawn only, so each node says what it comes after. */}
    {after && <span className="sr-only">. After {after}</span>}
  </span>
)

const nodes: GraphViewNode[] = [
  { id: "a", content: step("Add the region column", "Dev agent") },
  { id: "b", after: ["a"], content: step("Show it on the customer page", "Dev agent", "Add the region column") },
  { id: "c", after: ["a"], content: step("Backfill from billing country", "Dev agent", "Add the region column") },
  { id: "d", after: ["b", "c"], content: step("Test on Staging", "QA agent", "Show it on the customer page and Backfill from billing country") },
]

export default function Example() {
  const [view, setView] = React.useState<"list" | "graph">("graph")
  const [active, setActive] = React.useState<string>()
  return (
    <div className="flex max-w-xl flex-col gap-2 rounded-xl border bg-card p-3">
      <GraphViewSwitch view={view} onViewChange={setView} className="self-end" />
      {view === "graph" ? (
        <GraphView nodes={nodes} label="Plan steps as a graph" description="The same steps as the list, in the same order." onSelect={setActive} activeId={active} />
      ) : (
        <ol className="list-decimal ps-6 text-sm">
          {nodes.map((n) => (
            <li key={n.id}>{n.content}</li>
          ))}
        </ol>
      )}
    </div>
  )
}
```

## <GraphView>

Props:

- `nodes: GraphViewNode[]`: The nodes, in reading order: the order a screen reader and Home and End follow.
- `label: string`: The graph's accessible name.
- `description?: string | undefined`: A sentence read with the graph before the keys, e.g. "The same steps as the list, in the same order."
- `edgeLabels?: GraphViewEdgeLabel[] | undefined`: Words drawn on arrows.
- `direction?: "down" | "across" | undefined` (default `"down"`): "down" (the default): layers are rows, top to bottom. "across": layers are columns, from the start side.
- `nodeWidth?: number | undefined` (default `13`): Each node's width, in rem. Defaults to 13.
- `nodeHeight?: number | undefined` (default `4.5`): Each node's height, in rem; longer content is clipped, so keep it short. Defaults to 4.5.
- `onSelect?: ((id: string) => void) | undefined`: Makes each node a button; called with its id.
- `activeId?: string | undefined`: The node shown as selected.
- `className?: string | undefined`

## <GraphViewSwitch>

The List / Graph control a component shows when its work branches: two buttons, the one in use pressed.

Props:

- `view: "list" | "graph"`: The view in use.
- `onViewChange: (view: "list" | "graph") => void`: Called with the view the person picks.
- `className?: string | undefined`

## Strings (`graphView`)

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

- `keys`: Arrow keys follow the arrows and move across each row; Home and End go to the first and the last.
- `view`: View
- `list`: List
- `graph`: Graph

## Types

### GraphViewNode

A node in the graph: what it shows, and the nodes it comes after.

```ts
{
  id: string;
  /** What the node shows. Put what the arrows say in words here too (`sr-only`), since the arrows are only drawn. */
  content: React.ReactNode;
  /** The nodes this one comes after: an arrow from each to it. */
  after?: string[] | undefined;
  /** A fixed layer: the row going down, the column going across. Left out, it's laid out from `after`. */
  layer?: number | undefined;
  /** A fixed place across its layer. Left out, it's laid out to cross as few arrows as it can. */
  slot?: number | undefined;
  /** "muted" for a node that's out of the work (skipped, say). */
  tone?: "default" | "muted" | undefined;
}
```

### GraphViewEdgeLabel

Words drawn on an arrow. Drawn only: say them in the target's or source's `content` as well.

```ts
{ from: string; to: string; label: string }
```

## Motion

- **tint** (tint): A selectable node's background changes when the pointer is over it, and a ring fades in when it takes keyboard focus. The arrows and the nodes stay where they are drawn.

Set with `--cd-motion-graph-view-<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` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
