import { ProcessMap } from "@elabs-ai/components-process";npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-processPaste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.
Add the brand-ui ProcessMap component (from @elabs-ai/components-process) to this project. 1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`. 2. Look ProcessMap up before writing code: `npx -y @elabs-ai/components-cli docs ProcessMap` prints its real props, variants, what it pairs with and what to avoid. 3. If @elabs-ai/components-process is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-process` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-process`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`. 4. Use it here: [the screen or file, and what it should show] 5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop. 6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.
Avoid
- Encoding the metric in colour alone — an edge carries its reading as stroke WIDTH and a printed label pill before it carries it as hue, and a node prints its value beside the meter bar. A greyscale render must still be readable (WCAG 1.4.1).
- Reaching for raw `ReactFlow`/`Panel`/`Handle` to add canvas furniture — the map is a composition of flow's CanvasShell; an overlay is an absolutely-positioned sibling, and a missing primitive is a `@elabs-ai/components-flow` change.
- Re-deriving the numbers for a table, an export or a tooltip — `tableView` and the canvas render from the SAME model, which is what stops the two from disagreeing.
- Passing a raw event log on every render — pass `graph` from `discoverGraph` (or memoize the log) so a metric switch never re-runs discovery or the dagre layout.
Works with
Examples
Performance
The same graph read as time: median activity duration on the nodes, median flow time on the edges. The value is always printed — a duration ramp is never asked to carry the reading on its own.
Rework
Rework tallies from `detectRework`, badged on the activities that repeat. The badge is a count and an `sr-only` sentence, not a colour.
With Activity Colors
With a shared `colorScale` (RM-054): each activity shows a small identity swatch beside its meter — the same colour `VariantExplorer` paints that activity's chips with when it gets the same scale. The swatch is a mark, never the card fill, so it does not compete with the metric reading.
Abstracted
Half the activities and a third of the paths kept — `/core`'s `abstractGraph`.
Selection
A controlled selection, plus the keyboard path, asserted by a REAL `Tab` walk rather than a synthetic `focus()` call: `Tab` reaches every activity in layout order, `Enter` selects the one it is standing on, `F` opens the filter-intent menu for it, and `Escape` puts focus back on the activity instead of on the Filter button. Wired through `useProcessExplorer` (RM-052 round 2, #227) rather than a bare `useState`, so `onFilterIntent` is the hook's real `applyIntent` — choosing an intent from the menu this story already opens does not shrink the canvas. The tail of the play function proves it: applying a real filter dims the activities it drops but leaves every one of them in the DOM (Invariant F).
Left To Right
Left-to-right layout. The same cached model; only the layout key changes. It repeats the whole framing check rather than trusting the top-to-bottom pass, because the two directions put the handles on different sides of the card and every one of the defects {@link expectWellFramedCanvas} locks behaved DIFFERENTLY here: the connector drift measured 69 px against top-to-bottom's 24 px, and it was the direction FLIP that triggered it — the node's motion transition was reaching React Flow's handle elements as well as the node body, so a dot animated to its new side while React Flow measured it.
Elk Layout
The same graph laid out by ELK (`layoutEngine="elk"`) instead of dagre. elkjs is loaded lazily on first layout; the map, its framing and its keyboard order behave exactly as the dagre default does.
Table View
The accessible twin required for a graph that cannot be read as a picture. It renders from the same model as the canvas, so the numbers are identical by construction.
Table View Selection
The accessible twin WITH an active selection AND an active filter exclusion (#373) — the cell of the story matrix that was missing before this fix. No shipped story combined `tableView` with a real `selection`/`selectionStates`, so the twin's state gap never reached the blocking interaction + axe job or autodocs. Locks that a selected row and an excluded row are each named for their real, perceivable state.
Loading
No graph yet — a whole-region loading panel, not an empty canvas.
Empty
An abstraction that hid everything, or a log with no events.
Large Graph
Sixty activities with skip paths, self-loops and back-edges — the layout budget case, and the one where the overview thumbnail earns its place: at this size the reader navigates by the minimap, so a blank one is not a cosmetic loss. Only the thumbnail is asserted here. Label crowding at sixty activities is a real property of the graph rather than a layout defect, so the pill/card check belongs on the two eleven-activity stories, where a collision means the spacing is wrong.
API
ProcessMap
| Prop | Type | Description |
|---|---|---|
graph | ProcessGraph | A discovered graph. Takes precedence over {@link log} when both are given. |
log | EventLog | An event log to discover from, for a surface with no explorer around it. Runs `/core`'s `discoverGraph` and `detectRework` internally, memoized on the log. |
abstraction | Pick<AbstractionOptions, "activities" | "paths"> | Fractions of activities / paths to KEEP, as `/core`'s `abstractGraph` reads them. |
metricrequired | ProcessMetricSpec | Which readings the nodes and edges paint. |
rework | ReworkStats | Rework tallies for the node badge. Derived from {@link log} when that is given. |
selection | ProcessSelection | null | Controlled selection. Omit for an uncontrolled map that owns its own. |
selectionStates | ProcessSelectionStates | Per-element states an active filter contributes (RM-052 round 2, #227) — e.g. from `useProcessExplorer`'s own `selectionStates`. Applying or clearing a filter never adds or removes a node or an edge (Invariant F): it only re-inks the elements a filter dropped as `"excluded"` (dimmed, never `aria-disabled`). Omit for a map with no filtering, which reproduces today's selection-only behaviour exactly. |
onSelect | (target: ProcessSelection | null) => void | Fires with the new selection, or `null` when the reader deselects. |
onFilterIntent | (intent: ProcessFilterIntent) => void | Fires with an intent from the filter menu — hand it straight to `filterLog`. |
direction | FlowLayoutDirection | @default "TB" |
showMiniMap | boolean | @default true |
showLegend | boolean | @default true |
tableView | boolean | Render the accessible table twin instead of the canvas. @default false |
loading | boolean | No graph yet. Renders the loading panel rather than an empty canvas. |
colorScale | ActivityColorScale | A shared activity colour scale (RM-054, `/core`'s `activityColorScale`). When given, each activity node shows its identity swatch — the same colour `VariantExplorer` paints that activity with. Build it once from the FULL graph and hand the same instance to both views. Omit for today's uncoloured nodes. |
conformance | ConformanceResult | A replay result (RM-061's `tokenReplay`) to overlay (RM-062). When given, every node and edge carries `data-conformance="both" | "logOnly" | "modelOnly"` — additive to `data-selection` — painted as a status tone PLUS a glyph and a line style, with the state's word in the accessible name and a Conformance column in the table twin. Omit for today's map, unchanged. Usually reached through `ConformanceOverlay`. |
objectCentric | ObjectCentricGraph | Object-centric — RM-066. An object-centric graph (`discoverObjectCentricGraph`, e.g. over `fromOcel(…).logs`) to draw instead of `graph`/`log` — mutually exclusive with both, and it wins when given together (with a dev-mode console warning). Shared activities merge into one node carrying a chip per object type; edges are drawn once per object type in that type's chart colour, side by side. `abstraction` applies to every type alike; for per-type abstraction pass `abstractObjectCentricGraph(…)`'s result here instead. |
objectCentricLabels | ObjectCentricMapLabels | Object-centric — RM-066. Strings the object-centric mode composes. |
label | string | Accessible name for the canvas region. Defaults to the localized `process.map.label` message. |
fitMinZoom | number | The smallest zoom the OPENING fit may use. The default keeps activity names legible and, when the process does not fit at that size, opens on its start. A workspace that would rather open on the whole picture — a wide canvas, a reader who zooms in on what matters — lowers it. The reader can always zoom out further by hand. @default PROCESS_MAP_LEGIBLE_ZOOM |
fitPadding | number | The fraction of the pane the opening fit keeps clear on every side. The default is generous because transition pills are drawn outside the fitted bounds; a pane that is short on the axis the process runs along can trade some of it for a larger picture. @default 0.15 |
refitKey | string | number | Re-frames the map whenever this value changes. The map already re-fits when its own structure or direction changes; it cannot know that the pane around it just changed shape — a dock opened, a side panel closed. Pass anything that changes when that happens. Unset: no extra re-fits, the reader's viewport is left alone. |
layoutEngine | "dagre" | "elk" | The layout engine. `"elk"` lays the map out with `@elabs-ai/components-flow`'s `layoutFlowElk` — elkjs, an optional peer, loaded lazily on first use; when it is not installed the map falls back to dagre with a development-only warning. @default "dagre" |
ZoomControls
| Prop | Type | Description |
|---|---|---|
position | PanelPosition | |
className | string |