<!-- GENERATED by scripts/build-llms.mjs from llms/data.md — do not edit this file. -->

# `lr-flow-canvas`

- **Import** `import '@aceshooting/lyra-ui/components/lr-flow-canvas.js';` (stable tag alias; registers the tag)
- **Class** `LyraFlowCanvas`, also available unregistered from `@aceshooting/lyra-ui/components/data/flow-canvas/flow-canvas.class.js`
- **Family** `components/data/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 29 parts, 15 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-flow-canvas`

A pannable/zoomable DAG workflow canvas: positions HTML node cards, draws SVG edges between their
handles, runs a shared layered auto-layout for unpositioned nodes, and owns all selection/drag/
connect interaction. It is readonly by default; opt into editor gestures with `nodes-draggable`,
`connectable`, and `droppable`. The component snapshots model inputs instead of retaining mutable
caller aliases, and reports edit intent for the host to apply.

Input/output handle IDs are reserved only after their complete handle record is admitted; unreadable
optional labels cannot suppress a later valid handle with the same ID.

Horizontal RTL reflects the coordinate plane while authored cards, generated `lr-flow-node` cards,
native portable fallback cards and SVG edge labels retain readable content. Explicit physical model
coordinates remain unchanged.

Flow records and companion payloads are readonly public contracts. Consumers that need the types
without registering a component can import them from the side-effect-free module:

```ts
import type {
  FlowEdge,
  FlowHandle,
  FlowLayoutChangeDetail,
  FlowNode,
  FlowRunDecoration,
  FlowRunDecorations,
  FlowStructureSnapshot,
} from "@aceshooting/lyra-ui/components/data/flow-canvas/flow-types.js";
```

**Properties:**

- `nodes: readonly FlowNode[] = []` (attribute: false) — each record has readonly `id`, optional
  `type`, `position`, `data`, `accessibleLabel`, `inputs`, `outputs`, and `disabled`. A missing
  `position` opts into layered layout. String `data.label` and `data.description` feed the
  declarative fallback card. Assignment takes a detached, deeply frozen snapshot of plain
  arrays/records, omitting blank ids and later duplicates first-wins before layout, focus,
  selection, gestures, companion snapshots, and events. At most the first 10,000 source nodes are
  retained, with finite nested depth/entry budgets; reassign `nodes` after changes. Replacing the
  model cancels node-drag and connect gestures whose ids belonged to the old model and silently
  prunes selected ids that no longer exist. `disabled` marks a node non-actionable: it keeps its
  position and card content but cannot be selected/activated by click or keyboard, roving-tabindex
  navigation (arrow keys, Home/End) steps past it, it cannot be dragged even while
  `nodes-draggable`, and it is excluded from starting or receiving a new connection while
  `connectable` (an edge already touching it is left alone). Omitted or `false` renders the node
  exactly as before this field existed.
- `edges: readonly FlowEdge[] = []` (attribute: false) — readonly `id`, `source`, `target`, optional
  handle ids, optional drawn `label`, and optional `tone: LyraVariant`. The canonical brand value is
  `brand`; the former `accent` value and `FlowEdgeTone` alias are not part of this contract. Blank
  ids and later duplicates are omitted first-wins before render, focus, selection, gestures,
  companion snapshots, and events. At most the first 10,000 source edges are retained, with finite
  nested depth/entry budgets; reassign `edges` after changes. Dangling endpoint references remain
  visible through the component's documented fail-closed edge-list/stub paths.
- `orientation: 'horizontal' | 'vertical' = 'horizontal'` (reflected) — downstream layout/handle axis
- `nodesDraggable: boolean = false` (attribute `nodes-draggable`)
- `connectable: boolean = false`
- `droppable: boolean = false` — accepts drops carrying the `FLOW_PALETTE_MIME_TYPE` payload a
  `lr-node-palette` drag sets, emitting `lr-node-add`. The decoded payload must be a plain record
  with a non-empty string `type`; text fields and total payload size are bounded.
- `locked: boolean = false` (reflected) — freezes pan/zoom/drag/connect without touching the other
  gesture flags. Enabling it during a pan, node drag, pointer/keyboard connection, or palette drop
  cancels the active preview, rolls pan/node geometry back, clears transient state, and retires the
  window pointer listeners so a later release cannot commit.
- `selectedNodeIds: readonly string[] = []`, `selectedEdgeIds: readonly string[] = []` (attribute:
  false) — seed or replace selection. Each assignment snapshots at most the first 10,000 ids,
  omits blank/later duplicates first-wins, and prunes identities absent from the current canonical
  node/edge model. Reassign after changes. Activation and clear-selection gestures update frozen
  arrays before emitting `lr-selection-change`; model shrinkage prunes stale ids without claiming
  a user selection gesture occurred.
- `minZoom: number = 0.25` (attribute `min-zoom`), `maxZoom: number = 2` (attribute `max-zoom`)
- `grid: number = 8` — snap step in content px for drags/nudges/drop positions (`0` disables
  snapping); also the dotted background's base spacing
- `layerGap: number = 64` (attribute `layer-gap`), `nodeGap: number = 24` (attribute `node-gap`) —
  auto-layout layer/sibling spacing. The canvas measures rendered cards in layout space before its
  first pass; live changes to orientation, gaps, or card size trigger a new pass.
- `decorations: FlowRunDecorations | null = null` (attribute: false) —
  `Record<nodeOrEdgeId, FlowRunDecoration>`, where `FlowRunDecoration` has `status` plus optional
  `progress`, `durationMs`, and `detail`; assignment is detached, deeply frozen, bounded to 10,000
  keys plus finite nested depth/entry budgets, and invalid statuses and records with unreadable `status`, `progress`, `durationMs` or `detail` fields are omitted independently, retaining valid neighbors. Reassign the
  record after changes. Usually supplied by `lr-flow-run-status`.
- `accessibleLabel: string | null = null` (attribute `aria-label`)
- `viewport` (readonly getter) — a frozen `{ x, y, zoom }` snapshot

**Methods:** `setViewport({ x, y, zoom })`, `zoomIn()`, `zoomOut()`, `resetZoom()`,
`fit(options?: { padding?: number })` (frames every node), `focusNode(id, options?: { zoom? })`
(pans/zooms to one node and moves roving focus onto it), `toContentPoint(clientX, clientY)` (maps a
pointer position to content coordinates, RTL-aware), `registerCompanion(cb: (snapshot:
FlowStructureSnapshot) => void): () => void` — the subscription `lr-flow-minimap` uses to read
live node/edge/viewport geometry without this canvas ever importing the minimap.
All viewport-mutating methods, including `focusNode()`, are inert while `locked`; coordinate mapping
and companion subscription remain available because neither mutates viewport or edit state.
Each companion observer receives its own deeply frozen `FlowStructureSnapshot`: readonly node and
edge geometry/status arrays, viewport `{ x, y, zoom, width, height, minZoom, maxZoom }`, and the
effective `locked`, `orientation`, `layerGap`, and `nodeGap`. Zoom bounds are finite, positive, and
sorted even when public inputs are invalid or reversed.

**Events:** `lr-node-activate` (`detail: { nodeId }`), `lr-edge-activate` (`detail: { edgeId, source, target
}`), `lr-selection-change` (`detail: { nodeIds, edgeIds }`), `lr-node-move` (`detail: { nodeId,
position, previous }`), `lr-connect` (`detail: { source, target, sourceHandle, targetHandle }`),
`lr-node-add` (`detail: { type, position }`, from a palette drop), `lr-selection-delete`
(`detail: { nodeIds, edgeIds }`), `lr-viewport-change` (`detail: { x, y, zoom }`),
`lr-layout-change` (`detail: { positions, truncated }`, fired after an auto-layout pass places
previously unpositioned nodes). Every detail and nested coordinate/array is readonly and frozen.

**Slots:** default (consumer-authored cards matched by `node-id` and assigned to generated
`node-{id}` slots), `top-start`, `top-end`, `bottom-start`, `bottom-end`. Each node always has a
declarative shadow-DOM fallback card, so SSR and hydration do not depend on imperative light-DOM
card creation. An authored card replaces only its matching fallback; unmatched cards are unslotted
with a warning. Opposite-side companion slots share wrapping rails in narrow allocations.

**CSS parts:** `base`, `viewport`, `background`, `edges`, `edge`, `edge-label`, `edge-hit-area`,
`arrowhead`, `stub`
(a dangling-edge stub line), `connection-line` (in-progress connect gesture), `node`, `empty`,
`node-control` (the native per-node roving/activation control), `live-region`, `edge-list` (a
visually hidden list only for dangling/unrenderable edges), `layout-limit` (a visible notice whose
announcement uses the shared light-DOM polite sink), `overlay-rail`, and the
fallback-card parts `node-card`, `node-card-base`, `node-card-surface`, `node-card-header`,
`node-card-heading`, `node-card-status`, `node-card-progress`, `node-card-body`, `node-card-toolbar`,
`node-card-handle`, `node-card-handle-input`, and `node-card-handle-output`. A node whose `type`
normalizes to a safe part token also exposes `node-type-{value}`. A selected node wrapper carries
`data-selected`; its hidden `node-control` exposes the state as `aria-pressed`.

`live-region` is a visually hidden, `aria-hidden` mirror of the latest item/gesture message. The
actual messages are flushed to the document's shared light-DOM polite sink; mount is silent, and
identical repeated messages are appended as separate announcements.

**Themeable custom properties:** `--lr-canvas-reserved-height` (default
`var(--lr-size-24rem)`) controls the host's default block size and matches the pre-upgrade
reservation stylesheet; an explicit outer `block-size` still wins. `--lr-flow-canvas-grid-size`
(default: the finite `grid` property, or `8px`; dotted background spacing) can be set on the canvas
or a theme ancestor to override that property-derived fallback. Each edge tone colors its stroke
and the arrowhead marker it references:
`--lr-flow-canvas-edge-neutral-color` (default `var(--lr-color-border)`),
`--lr-flow-canvas-edge-brand-color` (default `var(--lr-color-brand)`),
`--lr-flow-canvas-edge-success-color` (default `var(--lr-color-success)`),
`--lr-flow-canvas-edge-warning-color` (default `var(--lr-color-warning)`), and
`--lr-flow-canvas-edge-danger-color` (default `var(--lr-color-danger)`).
`--lr-flow-canvas-connection-line-color` (default `var(--lr-color-brand)`) colors
`[part="connection-line"]`, the in-progress ghost path drawn while dragging a new connection from a
node's output handle, independent of a finished edge's own `-edge-*-color` tone above.
`--lr-flow-canvas-march-duration` (default
`var(--lr-duration-ambient)`, running-edge march animation duration; this is a time-only value, not
the `--lr-transition-ambient` duration/easing shorthand, because the animation supplies its own
`linear` timing function), and
`--lr-flow-canvas-node-selected-outline-color` (default `var(--lr-color-brand)`) controls the
selected-node outline. It is an inherited inline fallback, so it can be set on the canvas or a theme
ancestor without retinting the library-wide brand token. The same pattern applies to:
`--lr-flow-canvas-node-connect-invalid-outline-color` (default `var(--lr-color-danger)`) — outline of
a node that is an invalid connect-gesture drop target; `--lr-flow-canvas-node-connect-target-outline-color`
(default `var(--lr-color-brand)`) — outline of a node that is a valid connect-gesture drop target; and
`--lr-flow-canvas-drop-active-outline-color` (default `var(--lr-color-brand)`) — outline of the
viewport itself while a palette item is dragged over it (`droppable`). A fifth,
`--lr-flow-canvas-node-hover-outline-color` (default `var(--lr-color-border-strong)`) — the
mouse-hover preview of a node's own `:focus-visible` ring — exists for a different reason than the
four above. Set it to `transparent` to opt out of the hover treatment.

**Optional peer deps:** none.

```html
<lr-flow-canvas
  id="canvas"
  nodes-draggable
  connectable
  droppable
  style="height:480px"
>
  <lr-flow-controls slot="bottom-start" for="canvas"></lr-flow-controls>
  <lr-flow-minimap slot="bottom-end" for="canvas"></lr-flow-minimap>
  <lr-flow-run-status slot="top-start" for="canvas"></lr-flow-run-status>
</lr-flow-canvas>
<script>
  const canvas = document.getElementById("canvas");
  canvas.nodes = [
    { id: "a", data: { label: "Fetch" } },
    { id: "b", data: { label: "Transform" } },
  ];
  canvas.edges = [{ id: "a-b", source: "a", target: "b" }];
  canvas.addEventListener("lr-node-move", (e) => {
    canvas.nodes = canvas.nodes.map((n) =>
      n.id === e.detail.nodeId ? { ...n, position: e.detail.position } : n
    );
  });
</script>
```

**Known gotchas:**

- `nodes` and `edges` are controlled inputs: move, connect, add, and delete events are requests the
  host applies back. Selection is hybrid state: `selectedNodeIds`/`selectedEdgeIds` accept external
  replacement, while node/edge activation and clear-selection gestures update them internally and
  emit `lr-selection-change`.
- Auto-layout (via the dependency-free `layeredLayout()` util) only ever positions nodes that are
  missing an explicit `position`; a node the host has already positioned is left exactly where it is
  and, when its resolved center is nonnegative and within the safe-integer range, is used as a fixed
  anchor for the rest of the layout pass. Negative and larger finite coordinates remain rendered
  and caller-controlled but are omitted from the bounded utility's fixed-anchor input. The utility
  bounds virtual ordering work. If that ceiling is reached, the canvas renders a localized `layout-limit` status and sets
  `lr-layout-change.detail.truncated` to `true`; positions remain usable. Call the public utility
  directly with `maxVirtualWaypoints` when an application needs a different work ceiling.
- `droppable` only accepts drags carrying the exact `FLOW_PALETTE_MIME_TYPE` MIME type a
  `lr-node-palette` drag sets — the two components can never disagree on the payload shape because
  they share one exported constant.
- Pan/drag/zoom track the pointer's physical direction; under an RTL ancestor the pan and node-drag
  deltas are mirrored so content still visually follows the cursor, matching every other
  RTL-mirrored surface in this library.

**Additional API surface:**

- `part="edge-hit-area"` — The transparent wide pointer target behind an edge.
- `part="node-control"` — The visually hidden, roving selection button for a node.
- `--lr-flow-canvas-node-selected-outline-color` — Outline color of a selected node. Default: `var(--lr-color-brand)`.
- `--lr-flow-canvas-node-disabled-opacity` — Opacity of a node whose `FlowNode` entry sets
  `disabled`. Default: `var(--lr-opacity-disabled)`.

---
