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

With `places`, nodes can be moved to the places your app names for them (between two nodes, into a branch), and the layout stays automatic: your app moves the node in its data and the graph is laid out again. With a mouse or a pen, a node is dragged: its places are drawn, a line and the place's label mark the one nearest the pointer, letting go calls `onPlace`, and Escape puts it back. From the keyboard your app gives the control (a Move button that picks the node up, and arrow keys that walk its places) and draws it through `moving`, as WorkflowEditor does. On a touch screen a press on a node scrolls the graph as before; while your app draws a move through `moving`, each place is a button named by its label, out of the tab order, and a tap on one calls `onPlace`, so the same Move control works there. Give it a way to cancel too, since there is no Escape on a touch screen.

- 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: bidi, button, graph-layout, layer, locale, motion, 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/candor/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.
- `places?: ((id: string) => GraphViewPlace[]) | undefined`: The places a node can be moved to, asked for with its id; an empty list for a node that stays where it is. Given, a node with places can be dragged to one with a mouse or a pen. Left out, nothing moves, as before 4.5.
- `onPlace?: ((id: string, place: string) => void) | undefined`: Called when a dragged node is let go over one of its places, or a place is tapped or clicked while `moving`, with the node's id and the place's. End the move (clear `moving`) when it is called.
- `moving?: { id: string; place: string } | undefined`: A node your app is moving from the keyboard, and the id of the place it is at now: the node is drawn lifted, its places are drawn, and a line and the place's label mark that one, scrolled into view. Clear it when the move ends.
- `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
- `places`: Places to move it to

## 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 }
```

### GraphViewPlace

A place a node can be moved to, in the layout's own units, as a node's `layer` and `slot`: a fraction is between two, so layer 1.5 is midway between the second layer and the third. It is drawn where a node there would have its middle.

```ts
{
  id: string;
  /** What the place is, in words ("2 of 3 in Then of If Priority: High"): drawn by its line while it is chosen. */
  label: string;
  /** The layer it is in, as a node's; a fraction is between two layers. */
  layer: number;
  /** Where it is across its layer, as a node's slot; a fraction is between two. */
  slot: number;
}
```

## Motion

- **settle** (move): After a node is dropped on one of its places, the nodes that changed place slide from where they were drawn to the new layout; the arrows are drawn there at once. Only after a drop: a graph your app changes is drawn in its new layout. Under reduced motion the nodes are simply in their new places.
- **tint** (tint): A selectable node's background changes when the pointer is over it, and a ring fades in when it takes keyboard focus. Hovering moves nothing: 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`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
