@elabs-ai/components-flow 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/index.d.ts +738 -20
- package/dist/index.js +985 -182
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/__contract__/inspector-panel.contract.test.tsx +49 -0
- package/src/__contract__/legend.contract.test.tsx +49 -0
- package/src/canvas-shell/canvas-shell.tsx +116 -1
- package/src/canvas-shell/use-measured-nodes.ts +101 -0
- package/src/flow-button-edge/flow-button-edge.stories.tsx +13 -0
- package/src/flow-button-edge/flow-button-edge.tsx +8 -10
- package/src/flow-edge/flow-edge.stories.tsx +20 -0
- package/src/flow-edge/flow-edge.tsx +10 -3
- package/src/flow-edge-path/flow-edge-path.tsx +149 -0
- package/src/flow-edge-path/index.ts +1 -0
- package/src/flow-edge-path/no-raw-base-edge.test.ts +45 -0
- package/src/flow-floating-edge/flow-floating-edge.tsx +7 -3
- package/src/flow-group-node/flow-group-node.stories.tsx +1 -1
- package/src/flow-group-node/flow-group-node.tsx +24 -8
- package/src/flow-handle/flow-handle-anchor.test.tsx +97 -0
- package/src/flow-handle/flow-handle-anchor.ts +36 -0
- package/src/flow-handle/index.ts +1 -0
- package/src/flow-layout/flow-layout.stories.tsx +2 -2
- package/src/flow-layout/flow-layout.test.tsx +91 -0
- package/src/flow-layout/flow-layout.ts +77 -1
- package/src/flow-layout/layout-graph.test.ts +83 -2
- package/src/flow-layout/layout-graph.ts +23 -15
- package/src/flow-mini-map/flow-mini-map.stories.tsx +103 -0
- package/src/flow-node/flow-node.stories.tsx +151 -0
- package/src/flow-node/flow-node.tsx +56 -1
- package/src/flow-placeholder-node/flow-placeholder-node.tsx +5 -2
- package/src/flow-self-loop-edge/flow-self-loop-edge.stories.tsx +275 -0
- package/src/flow-self-loop-edge/flow-self-loop-edge.test.tsx +196 -0
- package/src/flow-self-loop-edge/flow-self-loop-edge.tsx +172 -0
- package/src/flow-self-loop-edge/index.ts +13 -0
- package/src/flow-self-loop-edge/self-loop-geometry.test.ts +128 -0
- package/src/flow-self-loop-edge/self-loop-geometry.ts +165 -0
- package/src/flow-smart-edge/flow-smart-edge.stories.tsx +55 -8
- package/src/flow-smart-edge/flow-smart-edge.tsx +125 -35
- package/src/flow-smart-edge/index.ts +5 -1
- package/src/flow-smart-edge/smart-edge-geometry.test.ts +88 -47
- package/src/flow-smart-edge/smart-edge-geometry.ts +69 -33
- package/src/flow-weighted-edge/back-edge-geometry.test.ts +54 -0
- package/src/flow-weighted-edge/back-edge-geometry.ts +60 -0
- package/src/flow-weighted-edge/edge-aria.test.ts +108 -0
- package/src/flow-weighted-edge/edge-aria.ts +117 -0
- package/src/flow-weighted-edge/edge-label-pill.test.tsx +65 -0
- package/src/flow-weighted-edge/edge-label-pill.tsx +82 -0
- package/src/flow-weighted-edge/flow-weighted-edge.stories.tsx +691 -0
- package/src/flow-weighted-edge/flow-weighted-edge.test.tsx +405 -0
- package/src/flow-weighted-edge/flow-weighted-edge.tsx +308 -0
- package/src/flow-weighted-edge/index.ts +18 -0
- package/src/flow-weighted-edge/weight-scale.test.ts +92 -0
- package/src/flow-weighted-edge/weight-scale.ts +86 -0
- package/src/index.ts +9 -0
- package/src/inspector-panel/inspector-panel.stories.tsx +1 -1
- package/src/inspector-panel/inspector-panel.test.tsx +20 -0
- package/src/inspector-panel/inspector-panel.tsx +22 -9
- package/src/legend/index.ts +7 -1
- package/src/legend/legend.stories.tsx +126 -0
- package/src/legend/legend.test.tsx +180 -0
- package/src/legend/legend.tsx +222 -3
- package/src/templates-flow-workspace.stories.tsx +1 -1
- package/src/testing/canvas-framing.test.ts +107 -0
- package/src/testing/canvas-framing.ts +396 -0
- package/src/testing/edge-anchors.ts +107 -0
- package/src/testing/index.ts +36 -0
- package/src/zoom-controls/zoom-controls.tsx +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, SVGAttributes } from 'react';
|
|
3
|
-
import { Node, Edge, ReactFlowProps, NodeProps, EdgeProps, Position, NodeChange, MiniMapProps, PanelPosition } from '@xyflow/react';
|
|
2
|
+
import { ReactNode, SVGProps, CSSProperties, SVGAttributes, ComponentPropsWithoutRef } from 'react';
|
|
3
|
+
import { Node, Edge, ReactFlowProps, FitViewOptions, NodeProps, EdgeProps, Position, NodeChange, MiniMapProps, PanelPosition } from '@xyflow/react';
|
|
4
4
|
export { Background, Connection, Controls, Edge, EdgeProps, MiniMap, Node, NodeProps, Panel, Position, ReactFlow, ReactFlowProvider, addEdge, useEdgesState, useNodesState, useReactFlow } from '@xyflow/react';
|
|
5
5
|
|
|
6
6
|
interface CanvasShellProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> extends ReactFlowProps<NodeType, EdgeType> {
|
|
@@ -15,6 +15,26 @@ interface CanvasShellProps<NodeType extends Node = Node, EdgeType extends Edge =
|
|
|
15
15
|
* use the exported `useHelperLines` + `<HelperLines>` directly.
|
|
16
16
|
*/
|
|
17
17
|
helperLines?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Re-fit the viewport whenever this value changes.
|
|
20
|
+
*
|
|
21
|
+
* React Flow's own `fitView` prop fits ONCE, as soon as the nodes are first measured,
|
|
22
|
+
* and then never again. A canvas whose positions arrive AFTER that first paint —
|
|
23
|
+
* anything laid out asynchronously, e.g. a debounced `layoutFlow` pass — therefore gets
|
|
24
|
+
* fitted to the pre-layout positions (typically every node still stacked at the origin)
|
|
25
|
+
* and stays there. Measured on the process map: the viewport pinned at React Flow's
|
|
26
|
+
* `maxZoom` of 2 with 2 of 11 nodes on screen, while the layout underneath was correct
|
|
27
|
+
* the whole time.
|
|
28
|
+
*
|
|
29
|
+
* Pass the key the layout is cached on (structure + direction), NOT something that
|
|
30
|
+
* changes on every render and not a metric: a re-fit is a viewport jump, so it must
|
|
31
|
+
* happen when the picture genuinely moved and never while the reader is reading. The
|
|
32
|
+
* fit waits for `useNodesInitialized`, so it measures real node boxes rather than
|
|
33
|
+
* fitting a degenerate bounding box a second time.
|
|
34
|
+
*/
|
|
35
|
+
fitViewKey?: string | number;
|
|
36
|
+
/** Options for the {@link fitViewKey} re-fit. Ignored without one. */
|
|
37
|
+
fitViewKeyOptions?: FitViewOptions;
|
|
18
38
|
/** Overlays rendered inside the flow (ZoomControls, Legend, Panels). */
|
|
19
39
|
children?: ReactNode;
|
|
20
40
|
className?: string;
|
|
@@ -31,6 +51,43 @@ interface CanvasShellProps<NodeType extends Node = Node, EdgeType extends Edge =
|
|
|
31
51
|
*/
|
|
32
52
|
declare function CanvasShell<NodeType extends Node = Node, EdgeType extends Edge = Edge>({ helperLines, ...props }: CanvasShellProps<NodeType, EdgeType>): react.JSX.Element;
|
|
33
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The class every React Flow `<Handle>` in this package carries — and the one a
|
|
56
|
+
* consumer's own custom node must carry too.
|
|
57
|
+
*
|
|
58
|
+
* A connector dot is a MEASUREMENT ANCHOR, not a moving part. React Flow measures
|
|
59
|
+
* `handleBounds` from the DOM exactly once per layout change (`updateNodeInternals`,
|
|
60
|
+
* fired from the effect that sees `sourcePosition`/`targetPosition` change) and then
|
|
61
|
+
* draws every edge endpoint from that stored number until something invalidates it.
|
|
62
|
+
* So a dot that is still ON ITS WAY to its new side when that measurement is taken is
|
|
63
|
+
* measured at the wrong place — permanently, because nothing measures again.
|
|
64
|
+
*
|
|
65
|
+
* That is not hypothetical, and it is not only about author-written transitions:
|
|
66
|
+
*
|
|
67
|
+
* - `@elabs-ai/components-tokens`' reduced-motion backstop (`themes.css`, MOTION GATE)
|
|
68
|
+
* sets `transition-duration: 0.01ms !important` on `*` so third-party engines that
|
|
69
|
+
* ignore the `--t-*` tokens (Monaco, `@xyflow/react`, Streamdown) cannot animate.
|
|
70
|
+
* `transition-property` is left at its initial value, `all` — so under
|
|
71
|
+
* `prefers-reduced-motion: reduce` that rule does not remove a transition from a
|
|
72
|
+
* handle, it CREATES one: every geometric property of every element becomes
|
|
73
|
+
* transitioned, for one frame.
|
|
74
|
+
* - One frame is all it takes. Measured on `ProcessMap direction="LR"`: at the commit
|
|
75
|
+
* that flips the handles from top/bottom to left/right, the dot's computed box was
|
|
76
|
+
* still the OLD one (`getAnimations()` on it returned live `left`+`top`+`transform`
|
|
77
|
+
* transitions), React Flow measured `{ x: 164, y: 45.5 }` where the settled DOM has
|
|
78
|
+
* `{ x: 172, y: 37.5 }`, and every edge on the map then terminated up to 67 px away
|
|
79
|
+
* from the dot it points at — for reduced-motion readers only, forever.
|
|
80
|
+
*
|
|
81
|
+
* `transition-property: none` is the fix at the right layer: it costs nothing under
|
|
82
|
+
* normal motion (no rule animates a handle there anyway) and it makes the reposition
|
|
83
|
+
* synchronous, so whenever React Flow measures, it measures the settled dot.
|
|
84
|
+
*
|
|
85
|
+
* The literal is written out here rather than assembled, because Tailwind extracts
|
|
86
|
+
* candidates from source TEXT: this file is scanned, so the utility is emitted, and
|
|
87
|
+
* components may interpolate the constant freely.
|
|
88
|
+
*/
|
|
89
|
+
declare const FLOW_HANDLE_ANCHOR_CLASS = "transition-none";
|
|
90
|
+
|
|
34
91
|
/** A node side that can carry a handle. Doubles as the handle's stable id. */
|
|
35
92
|
type FlowHandleSide = "top" | "right" | "bottom" | "left";
|
|
36
93
|
/**
|
|
@@ -66,28 +123,166 @@ interface FlowNodeData extends Record<string, unknown> {
|
|
|
66
123
|
* bottom-source (unchanged, backward-compatible).
|
|
67
124
|
*/
|
|
68
125
|
handles?: FlowNodeHandles;
|
|
126
|
+
/**
|
|
127
|
+
* An extra row rendered INSIDE the card, below the text block — a meter bar, a
|
|
128
|
+
* sparkline, a chip row.
|
|
129
|
+
*
|
|
130
|
+
* It exists because content a composing package renders BESIDE `FlowNode` (as a sibling
|
|
131
|
+
* inside React Flow's node element) silently breaks the canvas's geometry: React Flow
|
|
132
|
+
* positions every `<Handle>` against the nearest positioned ancestor and measures the
|
|
133
|
+
* node box from its own wrapper, so a sibling row makes the node box taller than the
|
|
134
|
+
* visible card and the handles drift off the card's border by exactly that difference.
|
|
135
|
+
* Measured on the process map's activity node, whose 6px meter and 4px gap put every
|
|
136
|
+
* bottom dot 10px below the card it was supposed to sit on, and every left/right dot
|
|
137
|
+
* 5px below the card's own mid-line.
|
|
138
|
+
*
|
|
139
|
+
* Put the row here instead and the card IS the node box again, so the dots land on the
|
|
140
|
+
* card edge for free. Nothing is rendered when it is absent — existing nodes are
|
|
141
|
+
* byte-identical.
|
|
142
|
+
*/
|
|
143
|
+
footer?: ReactNode;
|
|
69
144
|
}
|
|
70
145
|
type BrandFlowNode = Node<FlowNodeData, "brand">;
|
|
71
146
|
/**
|
|
72
147
|
* Branded custom node. Register it in `nodeTypes={{ brand: FlowNode }}` and
|
|
73
148
|
* create nodes with `type: "brand"` and `data: FlowNodeData`.
|
|
149
|
+
*
|
|
150
|
+
* ## Focus vs selection (#312)
|
|
151
|
+
*
|
|
152
|
+
* `selected && "ring-2 ring-ring"` below is a SELECTION marker, not a focus
|
|
153
|
+
* indicator — it is React Flow's own click-driven `selected` state and is the
|
|
154
|
+
* genuine-selection carve-out `.claude/rules/theming.md` names explicitly.
|
|
155
|
+
* Keyboard focus is a separate, independent signal this component used to omit
|
|
156
|
+
* entirely (issue #312): React Flow puts `tabIndex`/`role="group"` and the real
|
|
157
|
+
* `:focus-visible` state on **its own wrapper** (`.react-flow__node`, which
|
|
158
|
+
* also always carries a `data-id` attribute), one level ABOVE the `<div>` this
|
|
159
|
+
* component returns — so neither `focus-ring` (`:focus-visible` on self) nor
|
|
160
|
+
* `focus-ring-within` (`:focus-within`, a focused descendant) can ever fire
|
|
161
|
+
* here; focus is PROXIED to an ancestor this component doesn't render.
|
|
162
|
+
* `focus-ring-static` is the flavour built for that exact shape (ADR 0027),
|
|
163
|
+
* gated by an ancestor-selector arbitrary variant — the same idiom
|
|
164
|
+
* `FlowEdgePath` uses for `.react-flow__edge:focus-visible`, keyed on
|
|
165
|
+
* `[data-id]` here (rather than the escaped `.react-flow\_\_node` class) so
|
|
166
|
+
* the selector needs no backslash escaping inside a plain JS string — a
|
|
167
|
+
* literal `\_` in a `cn()` argument is a real JS string escape and would be
|
|
168
|
+
* silently stripped at runtime (unlike in a bare, unbraced JSX attribute,
|
|
169
|
+
* where backslashes are never processed — the reason `FlowEdgePath` can use
|
|
170
|
+
* the class form safely and this component, composing through `cn()`, cannot).
|
|
171
|
+
* The two signals compose without merging into one ring: `selected` alone
|
|
172
|
+
* paints the ring layer only, while a focused node additionally gets the
|
|
173
|
+
* `--ring-contour` outline drawn outside it, so "selected AND focused" reads
|
|
174
|
+
* as two visible layers, not the single ring "selected alone" paints.
|
|
74
175
|
*/
|
|
75
176
|
declare function FlowNode({ data, selected, sourcePosition, targetPosition, }: NodeProps<BrandFlowNode>): react.JSX.Element;
|
|
76
177
|
|
|
77
178
|
/**
|
|
78
179
|
* Branded bezier edge using the `--flow-edge` token. Register it in
|
|
79
180
|
* `edgeTypes={{ brand: FlowEdge }}` and create edges with `type: "brand"`.
|
|
181
|
+
*
|
|
182
|
+
* Drawn through `FlowEdgePath`, so it carries the shared keyboard focus
|
|
183
|
+
* indicator (#286) — never reach for React Flow's `BaseEdge` directly.
|
|
80
184
|
*/
|
|
81
185
|
declare function FlowEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, style, }: EdgeProps): react.JSX.Element;
|
|
82
186
|
|
|
83
187
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
188
|
+
* Extra stroke width, in px, of the `--ring` band drawn around a focused edge.
|
|
189
|
+
* The band sits *outside* the edge's own stroke, so it is visible whatever the
|
|
190
|
+
* edge's resting width is.
|
|
191
|
+
*/
|
|
192
|
+
declare const FLOW_EDGE_FOCUS_RING_WIDTH = 3;
|
|
193
|
+
/**
|
|
194
|
+
* Extra stroke width, in px, of the neutral contour drawn outside the `--ring`
|
|
195
|
+
* band. This is the layer that carries the WCAG 1.4.11 bar: `--foreground`
|
|
196
|
+
* measures 12.50:1 against `--canvas` in `light` and 16.25:1 in `dark`, where
|
|
197
|
+
* `--ring` alone measures 1.30:1 in `light` (issue #286).
|
|
198
|
+
*/
|
|
199
|
+
declare const FLOW_EDGE_FOCUS_CONTOUR_WIDTH = 6;
|
|
200
|
+
interface FlowEdgePathProps extends Omit<SVGProps<SVGPathElement>, "path" | "stroke" | "strokeWidth" | "style" | "ref"> {
|
|
201
|
+
/** SVG path `d` for this edge, from `getBezierPath`/`getSmoothStepPath`/… */
|
|
202
|
+
path: string;
|
|
203
|
+
/** Resting stroke paint — a `var(--token)` reference or a resolved colour. */
|
|
204
|
+
stroke: string;
|
|
205
|
+
/** Resting stroke width in px. The focus layers are drawn wider than this. */
|
|
206
|
+
strokeWidth: number;
|
|
207
|
+
/** Dash pattern, applied to the edge AND to both focus layers so a dashed edge keeps its shape when focused. */
|
|
208
|
+
strokeDasharray?: string;
|
|
209
|
+
/** Stroke opacity for the edge itself. Never applied to the focus layers. */
|
|
210
|
+
strokeOpacity?: number;
|
|
211
|
+
/** React Flow marker url, e.g. from `EdgeProps.markerEnd`. */
|
|
212
|
+
markerEnd?: string;
|
|
213
|
+
/** React Flow marker url, e.g. from `EdgeProps.markerStart`. */
|
|
214
|
+
markerStart?: string;
|
|
215
|
+
/** Width of the invisible pointer-target path React Flow draws over the edge. */
|
|
216
|
+
interactionWidth?: number;
|
|
217
|
+
/** Merged onto the edge path, last — so a consumer's `style.stroke` still wins. */
|
|
218
|
+
style?: CSSProperties;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The edge path every brand edge type draws, with the keyboard focus indicator
|
|
222
|
+
* built in. **Use this instead of React Flow's `BaseEdge`** — a custom edge that
|
|
223
|
+
* reaches for `BaseEdge` directly ships with no focus indicator at all, which is
|
|
224
|
+
* the defect issue #286 records (and `no-raw-base-edge.test.ts` fails on).
|
|
225
|
+
*
|
|
226
|
+
* ## Why the indicator is drawn rather than restyled
|
|
227
|
+
*
|
|
228
|
+
* React Flow zeroes the native outline on a focused edge
|
|
229
|
+
* (`.react-flow__edge:focus-visible { outline: none }`) and substitutes a stroke
|
|
230
|
+
* recolour on `.react-flow__edge-path`. Every brand edge passes its stroke as an
|
|
231
|
+
* **inline style** on that exact path — `BaseEdge` spreads `style` onto it — and
|
|
232
|
+
* an inline declaration beats any stylesheet rule, so the substitute never
|
|
233
|
+
* painted. Focusing an edge changed nothing on screen: WCAG 2.4.7 failed on
|
|
234
|
+
* every edge of every canvas, in both themes.
|
|
235
|
+
*
|
|
236
|
+
* The obvious repair — move the stroke into a custom property so a stylesheet
|
|
237
|
+
* rule can reach it — does **not** work here, and that was measured rather than
|
|
238
|
+
* assumed: React Flow's own `.react-flow__edge-path { stroke: … }` ships
|
|
239
|
+
* **unlayered**, and unlayered CSS outranks anything in `@layer utilities`,
|
|
240
|
+
* where Tailwind puts every utility. A `stroke-[var(--flow-edge-stroke)]` class
|
|
241
|
+
* would therefore lose to React Flow's `#b1b1b7` default and repaint every edge
|
|
242
|
+
* in the library. `!important` and setting `--xy-edge-stroke-selected` were both
|
|
243
|
+
* rejected in the issue.
|
|
244
|
+
*
|
|
245
|
+
* So the indicator is drawn on two paths this component owns, under the edge and
|
|
246
|
+
* on the same geometry. Nothing can shadow them: not React Flow's stylesheet,
|
|
247
|
+
* not a consumer's inline `style.stroke`, not a future edge type's own painting.
|
|
248
|
+
* They are hidden (`opacity-0`) until the ancestor `g.react-flow__edge` matches
|
|
249
|
+
* `:focus-visible`, which is a plain descendant selector on classes only this
|
|
250
|
+
* component uses.
|
|
251
|
+
*
|
|
252
|
+
* ## Compound, because one colour is not enough
|
|
253
|
+
*
|
|
254
|
+
* The indicator is two layers, per the compound-indicator recipe in
|
|
255
|
+
* `.claude/rules/theming.md`: a neutral `--foreground` contour at
|
|
256
|
+
* `strokeWidth + 6`, and the `--ring` band at `strokeWidth + 3` over it. The
|
|
257
|
+
* contour is the layer that clears WCAG 1.4.11's 3:1 bar against `--canvas` in
|
|
258
|
+
* both reference themes (12.50:1 `light`, 16.25:1 `dark`); a bare `--ring`
|
|
259
|
+
* stroke would not (1.30:1 in `light`). It is opacity + stroke only — no
|
|
260
|
+
* shadow — so it survives `data-decoration="8|9|10"`, which goes shadowless.
|
|
261
|
+
*/
|
|
262
|
+
declare function FlowEdgePath({ path, stroke, strokeWidth, strokeDasharray, strokeOpacity, markerEnd, markerStart, interactionWidth, className, style, ...props }: FlowEdgePathProps): react.JSX.Element;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Branded edge that picks the closest source/target **handle** pair and routes a
|
|
266
|
+
* bezier between them. Anchors are recomputed every render, so they flip as
|
|
267
|
+
* nodes are dragged. Register it in `edgeTypes={{ smart: FlowSmartEdge }}`; give
|
|
268
|
+
* the connected nodes a `data.handles` config (e.g. `FLOW_ALL_SIDE_HANDLES`) to
|
|
269
|
+
* offer it more than the default two anchors. Uses the `--flow-edge` token,
|
|
270
|
+
* matching `FlowEdge`.
|
|
271
|
+
*
|
|
272
|
+
* ## The path terminates ON the handle dot
|
|
273
|
+
*
|
|
274
|
+
* The endpoints come from React Flow's **measured `handleBounds`** — the centre
|
|
275
|
+
* of the rendered dot — not from a point derived from the node's rectangle.
|
|
276
|
+
* That is a correctness property, not a refinement: an earlier version slid each
|
|
277
|
+
* anchor along the chosen side toward the other node (to fan out edges sharing a
|
|
278
|
+
* side) and picked sides from a four-way fallback list, so a line could meet the
|
|
279
|
+
* node up to half a side away from any dot — measured at ~22px on multi-side
|
|
280
|
+
* nodes and ~124px on nodes carrying only the default top/bottom handles, where
|
|
281
|
+
* a left/right side with no handle at all could be chosen. Two edges leaving the
|
|
282
|
+
* same handle now leave from the same point and diverge, exactly as React Flow's
|
|
283
|
+
* own edges do.
|
|
89
284
|
*/
|
|
90
|
-
declare function FlowSmartEdge({ id, source, target, markerEnd, style }: EdgeProps): react.JSX.Element | null;
|
|
285
|
+
declare function FlowSmartEdge({ id, source, target, sourceHandleId, targetHandleId, markerEnd, style, }: EdgeProps): react.JSX.Element | null;
|
|
91
286
|
|
|
92
287
|
/** Axis-aligned node rectangle in absolute flow coordinates. */
|
|
93
288
|
interface NodeRect {
|
|
@@ -100,12 +295,48 @@ interface NodeRect {
|
|
|
100
295
|
declare const HANDLE_SIDES: FlowHandleSide[];
|
|
101
296
|
/** Maps a handle side to the React Flow `Position` used for bezier control. */
|
|
102
297
|
declare const sideToPosition: Record<FlowHandleSide, Position>;
|
|
298
|
+
/** Maps a React Flow `Position` back to the handle side it names. */
|
|
299
|
+
declare const positionToSide: Record<Position, FlowHandleSide>;
|
|
300
|
+
/**
|
|
301
|
+
* One candidate connection point: the **measured centre of a rendered handle
|
|
302
|
+
* dot**, in absolute flow coordinates, plus the side it sits on.
|
|
303
|
+
*
|
|
304
|
+
* This is the unit `FlowSmartEdge` routes between. Anchoring on a measured
|
|
305
|
+
* handle — rather than on a point derived from the node's rectangle — is what
|
|
306
|
+
* guarantees the drawn path terminates exactly on the dot the user sees,
|
|
307
|
+
* whatever the handle's size, offset or CSS. See {@link pickClosestAnchors}.
|
|
308
|
+
*/
|
|
309
|
+
interface HandleAnchor {
|
|
310
|
+
/** The handle's `id`, when it has one (`FlowNode` uses the side name). */
|
|
311
|
+
id: string | null;
|
|
312
|
+
/** Absolute x of the handle dot's centre. */
|
|
313
|
+
x: number;
|
|
314
|
+
/** Absolute y of the handle dot's centre. */
|
|
315
|
+
y: number;
|
|
316
|
+
/** The node side the handle sits on — the bezier's control direction. */
|
|
317
|
+
side: FlowHandleSide;
|
|
318
|
+
}
|
|
319
|
+
/** The chosen source/target anchor pair. */
|
|
320
|
+
interface ClosestAnchors {
|
|
321
|
+
source: HandleAnchor;
|
|
322
|
+
target: HandleAnchor;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Picks the source/target pair of **rendered handles** with the shortest
|
|
326
|
+
* straight-line distance between them.
|
|
327
|
+
*
|
|
328
|
+
* Returns `undefined` when either side has no candidates, so the caller can
|
|
329
|
+
* fall back to rectangle geometry for a node whose handles have not been
|
|
330
|
+
* measured yet (React Flow populates `handleBounds` on its first measurement
|
|
331
|
+
* pass; before that there is nothing to anchor to).
|
|
332
|
+
*/
|
|
333
|
+
declare function pickClosestAnchors(sources: HandleAnchor[], targets: HandleAnchor[]): ClosestAnchors | undefined;
|
|
103
334
|
/** Absolute coordinate of a handle on the given side (the side's midpoint). */
|
|
104
335
|
declare function handlePoint(rect: NodeRect, side: FlowHandleSide): {
|
|
105
336
|
x: number;
|
|
106
337
|
y: number;
|
|
107
338
|
};
|
|
108
|
-
/** The
|
|
339
|
+
/** The side pair picked by {@link pickClosestHandles}, with both anchor points. */
|
|
109
340
|
interface ClosestHandles {
|
|
110
341
|
sourceSide: FlowHandleSide;
|
|
111
342
|
targetSide: FlowHandleSide;
|
|
@@ -115,9 +346,14 @@ interface ClosestHandles {
|
|
|
115
346
|
ty: number;
|
|
116
347
|
}
|
|
117
348
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
349
|
+
* Rectangle-only fallback: picks the closest pair of **side midpoints** from the
|
|
350
|
+
* candidate sides of each node.
|
|
351
|
+
*
|
|
352
|
+
* `FlowSmartEdge` prefers {@link pickClosestAnchors} (measured handle centres)
|
|
353
|
+
* and only reaches for this before React Flow has measured the nodes. Pass the
|
|
354
|
+
* sides each node genuinely renders a handle on — passing sides that carry no
|
|
355
|
+
* handle produces an anchor floating on a bare border, which is the defect this
|
|
356
|
+
* module exists to avoid.
|
|
121
357
|
*/
|
|
122
358
|
declare function pickClosestHandles(source: NodeRect, sourceSides: FlowHandleSide[], target: NodeRect, targetSides: FlowHandleSide[]): ClosestHandles;
|
|
123
359
|
|
|
@@ -229,6 +465,22 @@ interface FlowLayoutOptions {
|
|
|
229
465
|
interface FlowLayoutResult<NodeType extends Node = Node, EdgeType extends Edge = Edge> {
|
|
230
466
|
nodes: NodeType[];
|
|
231
467
|
edges: EdgeType[];
|
|
468
|
+
/**
|
|
469
|
+
* Ids of edges that run **against** the layout direction — a rework / retry
|
|
470
|
+
* loop in a process graph. dagre breaks cycles by reversing such edges
|
|
471
|
+
* internally and never surfaces which ones it reversed, so this is derived
|
|
472
|
+
* from the ranks dagre stamps on the laid-out graph: an edge whose source
|
|
473
|
+
* ranks at or after its target went backwards. Render these with
|
|
474
|
+
* `FlowWeightedEdge`'s `variant="back"`.
|
|
475
|
+
*/
|
|
476
|
+
backEdges: string[];
|
|
477
|
+
/**
|
|
478
|
+
* Ids of edges whose `source === target`. dagre does not lay out self-loops,
|
|
479
|
+
* so they are withheld from the graph entirely (never `setEdge`-ed) and are
|
|
480
|
+
* returned unchanged in `edges` — they take part in no rank computation and
|
|
481
|
+
* cannot distort the layout. Render these with `FlowSelfLoopEdge`.
|
|
482
|
+
*/
|
|
483
|
+
selfLoops: string[];
|
|
232
484
|
}
|
|
233
485
|
/**
|
|
234
486
|
* Pure dagre-powered auto layout. Computes new `position`s for `nodes` given
|
|
@@ -240,6 +492,11 @@ interface FlowLayoutResult<NodeType extends Node = Node, EdgeType extends Edge =
|
|
|
240
492
|
* `node.height`, then a sensible default. Node identity and `data` are left
|
|
241
493
|
* untouched — only `position` changes.
|
|
242
494
|
*
|
|
495
|
+
* Also reports the graph's two structural signals — `backEdges` (edges that run
|
|
496
|
+
* against the layout direction) and `selfLoops` (`source === target`). Both are
|
|
497
|
+
* additive fields on the result; a caller that only destructures
|
|
498
|
+
* `{ nodes, edges }` is unaffected.
|
|
499
|
+
*
|
|
243
500
|
* Pair with `useFlowLayout` to apply the result to a live canvas.
|
|
244
501
|
*/
|
|
245
502
|
declare function layoutFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(nodes: NodeType[], edges: EdgeType[], options?: FlowLayoutOptions): FlowLayoutResult<NodeType, EdgeType>;
|
|
@@ -268,8 +525,8 @@ interface UseFlowLayoutResult {
|
|
|
268
525
|
declare function useFlowLayout<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): UseFlowLayoutResult;
|
|
269
526
|
|
|
270
527
|
/** Which generic graph-geometry algorithm `layoutGraph` should run. */
|
|
271
|
-
type LayoutAlgorithm = "concentric" | "force" | "layered-lr" | "grid";
|
|
272
|
-
/** Horizontal/vertical gap used by the `"grid"` and `"layered
|
|
528
|
+
type LayoutAlgorithm = "concentric" | "force" | "layered-lr" | "layered-tb" | "grid";
|
|
529
|
+
/** Horizontal/vertical gap used by the `"grid"` and `"layered-*"` algorithms. */
|
|
273
530
|
interface LayoutSpacing {
|
|
274
531
|
x: number;
|
|
275
532
|
y: number;
|
|
@@ -286,7 +543,7 @@ interface LayoutOptions {
|
|
|
286
543
|
ringRadius?: number;
|
|
287
544
|
/** `"force"` only — number of synchronous simulation ticks to run. @default 300 */
|
|
288
545
|
iterations?: number;
|
|
289
|
-
/** `"grid"` / `"layered-lr"` — gap between nodes. @default {x:200,y:120} for grid. */
|
|
546
|
+
/** `"grid"` / `"layered-lr"` / `"layered-tb"` — gap between nodes. @default {x:200,y:120} for grid. */
|
|
290
547
|
spacing?: LayoutSpacing;
|
|
291
548
|
/** Resolve a node's size by id. Falls back to `measured`/`width`/`height`/a sensible default. */
|
|
292
549
|
nodeSize?: (id: string) => {
|
|
@@ -303,7 +560,10 @@ interface LayoutOptions {
|
|
|
303
560
|
* Node identity and `data` are left untouched — only `position` changes.
|
|
304
561
|
* Pair with `useAutoLayout` for a memoized hook form.
|
|
305
562
|
*
|
|
306
|
-
* - `"layered-lr"`
|
|
563
|
+
* - `"layered-lr"` / `"layered-tb"` delegate to the dagre-powered `layoutFlow`
|
|
564
|
+
* (direction `"LR"` / `"TB"`). `spacing.x` is always the horizontal gap and
|
|
565
|
+
* `spacing.y` the vertical one, so the two differ only in which axis carries
|
|
566
|
+
* the ranks.
|
|
307
567
|
* - `"concentric"` places `centerId` (or the highest-degree node) at the
|
|
308
568
|
* origin, with BFS shells at `ring × ringRadius`; disconnected nodes land in
|
|
309
569
|
* one extra outer ring so positions are never `NaN`.
|
|
@@ -477,13 +737,57 @@ interface LegendItem {
|
|
|
477
737
|
/** Any CSS color or token reference, e.g. "var(--chart-1)". */
|
|
478
738
|
color: string;
|
|
479
739
|
}
|
|
480
|
-
|
|
740
|
+
/**
|
|
741
|
+
* Categorical legend props — a swatch-and-label list. This is the shape
|
|
742
|
+
* `Legend` renders when `variant` is omitted, byte-for-byte unchanged from
|
|
743
|
+
* before the `"scale"` variant existed.
|
|
744
|
+
*/
|
|
745
|
+
interface LegendCategoricalProps {
|
|
746
|
+
variant?: "categorical";
|
|
481
747
|
items: LegendItem[];
|
|
482
748
|
title?: string;
|
|
483
749
|
className?: string;
|
|
484
750
|
}
|
|
485
|
-
/**
|
|
486
|
-
|
|
751
|
+
/**
|
|
752
|
+
* Continuous scale legend — a reading key that explains a WIDTH or COLOR
|
|
753
|
+
* encoding as a *range*, not a set of discrete categories (e.g.
|
|
754
|
+
* `FlowWeightedEdge`'s `data.weight` → stroke width, `data.value` → stroke
|
|
755
|
+
* color). `kind: "width"` reuses `computeEdgeWeightScale` — the exact pure
|
|
756
|
+
* scale `FlowWeightedEdge` calls — so the sample strokes drawn here can never
|
|
757
|
+
* drift from the widths a real flow's edges render for the same weights.
|
|
758
|
+
*/
|
|
759
|
+
interface LegendScaleProps {
|
|
760
|
+
variant: "scale";
|
|
761
|
+
/** Which continuous encoding this legend explains. */
|
|
762
|
+
kind: "width" | "color";
|
|
763
|
+
/** `[min, max]` of the underlying value the ramp represents. */
|
|
764
|
+
domain: [number, number];
|
|
765
|
+
/**
|
|
766
|
+
* Formats a domain value for display at a tick.
|
|
767
|
+
* @default the active `LocaleProvider` locale's `formatNumber` (host locale
|
|
768
|
+
* when no provider is mounted) — never a hardcoded `toLocaleString()`.
|
|
769
|
+
*/
|
|
770
|
+
format?: (value: number) => string;
|
|
771
|
+
/**
|
|
772
|
+
* Sample count for `kind: "width"`: `"minmax"` draws a min/max pair of
|
|
773
|
+
* sample strokes, `"minmedmax"` adds the domain midpoint as a third
|
|
774
|
+
* sample. `kind: "color"` always renders a fixed 5-stop gradient
|
|
775
|
+
* regardless of this prop — five stops is what makes a 2-endpoint color
|
|
776
|
+
* ramp legible as an ordered scale (see the color-ramp a11y note below),
|
|
777
|
+
* independent of how many width samples are shown.
|
|
778
|
+
* @default "minmax"
|
|
779
|
+
*/
|
|
780
|
+
ticks?: "minmax" | "minmedmax";
|
|
781
|
+
title?: string;
|
|
782
|
+
className?: string;
|
|
783
|
+
}
|
|
784
|
+
type LegendProps = LegendCategoricalProps | LegendScaleProps;
|
|
785
|
+
/**
|
|
786
|
+
* Small legend mapping colors/types to labels for a canvas or chart
|
|
787
|
+
* (`variant: "categorical"`, the default), or — via `variant="scale"` — a
|
|
788
|
+
* continuous width/color ramp with a domain, tick marks and formatting.
|
|
789
|
+
*/
|
|
790
|
+
declare function Legend(props: LegendProps): react.JSX.Element;
|
|
487
791
|
|
|
488
792
|
interface ZoomControlsProps {
|
|
489
793
|
position?: PanelPosition;
|
|
@@ -631,4 +935,418 @@ interface UseFlowGroupsResult {
|
|
|
631
935
|
*/
|
|
632
936
|
declare function useFlowGroups<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): UseFlowGroupsResult;
|
|
633
937
|
|
|
634
|
-
|
|
938
|
+
interface EdgeLabelPillProps extends ComponentPropsWithoutRef<"button"> {
|
|
939
|
+
/** Primary label, e.g. a frequency count ("128×"). */
|
|
940
|
+
label?: string;
|
|
941
|
+
/** Secondary label rendered alongside the primary, e.g. a duration ("3.4d avg"). */
|
|
942
|
+
secondaryLabel?: string;
|
|
943
|
+
/** Label anchor, from `getBezierPath`/`getSmoothStepPath`'s `labelX`/`labelY`. */
|
|
944
|
+
x: number;
|
|
945
|
+
y: number;
|
|
946
|
+
/** Matches the parent edge's `selected` state. */
|
|
947
|
+
selected?: boolean;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* A small HTML pill (via `EdgeLabelRenderer`, not SVG `<text>`) anchored at an
|
|
951
|
+
* edge's label point — so it can theme, wrap, and carry two values, unlike a
|
|
952
|
+
* bare SVG text node. Renders nothing when neither label is set. Real
|
|
953
|
+
* `<button>` so it is a genuine keyboard tab stop with a visible focus ring;
|
|
954
|
+
* `pointer-events: auto` on an otherwise `nodrag nopan` wrapper so it doesn't
|
|
955
|
+
* drag/pan the canvas, and doesn't block hovering the edge underneath it (the
|
|
956
|
+
* wrapper is sized to the pill itself, not the whole edge).
|
|
957
|
+
*
|
|
958
|
+
* `className`/`...props` spread onto the root `<button>` (`data-slot="edge-label-pill"`)
|
|
959
|
+
* so a consumer that composes this pill from outside `@elabs-ai/components-flow` —
|
|
960
|
+
* `@elabs-ai/components-process`'s `ProcessTransitionEdge` is the reference caller —
|
|
961
|
+
* can reach it directly (a dashed frame, a `data-selection` attribute) without a new
|
|
962
|
+
* semantic prop on this component. `className` merges LAST via `cn()`, so a caller can
|
|
963
|
+
* override any of the pill's own utility classes; omitting both leaves every existing
|
|
964
|
+
* caller's rendered markup unchanged.
|
|
965
|
+
*/
|
|
966
|
+
declare function EdgeLabelPill({ label, secondaryLabel, x, y, selected, className, ...props }: EdgeLabelPillProps): react.JSX.Element | null;
|
|
967
|
+
|
|
968
|
+
interface FlowWeightedEdgeData extends Record<string, unknown> {
|
|
969
|
+
/** Frequency/volume this edge carries. Scaled into stroke width — see `computeEdgeWeightScale`. */
|
|
970
|
+
weight?: number;
|
|
971
|
+
/** Edges sharing a `scaleGroup` share one min-max width domain. @default all edges in the flow */
|
|
972
|
+
scaleGroup?: string;
|
|
973
|
+
/** A second, continuous measure (e.g. average duration). Colours the stroke — needs `valueDomain` too. */
|
|
974
|
+
value?: number;
|
|
975
|
+
/** `[min, max]` domain `value` is interpolated across, from `--flow-edge-weak` to `--flow-edge-strong`. */
|
|
976
|
+
valueDomain?: [number, number];
|
|
977
|
+
/** Primary edge-label-pill text, e.g. a frequency count. */
|
|
978
|
+
label?: string;
|
|
979
|
+
/** Secondary edge-label-pill text, e.g. a duration. */
|
|
980
|
+
secondaryLabel?: string;
|
|
981
|
+
/** Path geometry. Ignored when `variant` is `"back"`, which always routes smoothstep. @default "bezier" */
|
|
982
|
+
path?: "bezier" | "smoothstep";
|
|
983
|
+
/**
|
|
984
|
+
* Whether this edge advances the process (`"forward"`) or runs against the
|
|
985
|
+
* layout direction (`"back"` — a rework/retry edge, as reported by
|
|
986
|
+
* `layoutFlow`'s `backEdges`).
|
|
987
|
+
*
|
|
988
|
+
* `"back"` is distinguished by SHAPE, not colour: a dashed stroke, and a
|
|
989
|
+
* smoothstep route pushed clear of the forward edge between the same pair of
|
|
990
|
+
* nodes so the two never overlap. It also carries a real accessible name, so
|
|
991
|
+
* the direction reaches assistive technology as text rather than only as a
|
|
992
|
+
* `data-variant` attribute.
|
|
993
|
+
*
|
|
994
|
+
* @default "forward"
|
|
995
|
+
*/
|
|
996
|
+
variant?: "forward" | "back";
|
|
997
|
+
/**
|
|
998
|
+
* Overrides the accessible name given to a `"back"` edge's graphic. Defaults
|
|
999
|
+
* to "Back edge — runs against the process direction".
|
|
1000
|
+
*/
|
|
1001
|
+
variantLabel?: string;
|
|
1002
|
+
/**
|
|
1003
|
+
* Passed straight through to the rendered `EdgeLabelPill`'s `className`/`...props`
|
|
1004
|
+
* (see `EdgeLabelPillProps`) — the seam a composing package (e.g.
|
|
1005
|
+
* `@elabs-ai/components-process`'s `ProcessTransitionEdge`) uses to reach the pill's
|
|
1006
|
+
* own root button from outside this component, without a new semantic prop here.
|
|
1007
|
+
*/
|
|
1008
|
+
labelProps?: Omit<EdgeLabelPillProps, "label" | "secondaryLabel" | "x" | "y" | "selected">;
|
|
1009
|
+
}
|
|
1010
|
+
type BrandFlowWeightedEdge = Edge<FlowWeightedEdgeData, "weighted">;
|
|
1011
|
+
/**
|
|
1012
|
+
* Branded weighted edge: `data.weight` scales stroke width (min-maxed per
|
|
1013
|
+
* `data.scaleGroup` across every edge in the flow — see `computeEdgeWeightScale`,
|
|
1014
|
+
* exported so a sibling like a continuous `Legend` renders the same ramp);
|
|
1015
|
+
* `data.value` + `data.valueDomain` interpolate stroke colour between
|
|
1016
|
+
* `--flow-edge-weak` and `--flow-edge-strong`; `data.label`/`data.secondaryLabel`
|
|
1017
|
+
* render as an `EdgeLabelPill`. An edge with none of this data renders exactly
|
|
1018
|
+
* like `FlowEdge` (fixed 1.5px, `--flow-edge` token) — fully backward-compatible.
|
|
1019
|
+
* Register it in `edgeTypes={{ weighted: FlowWeightedEdge }}` and create edges
|
|
1020
|
+
* with `type: "weighted"` and `data: FlowWeightedEdgeData`.
|
|
1021
|
+
*
|
|
1022
|
+
* `weight`/`value` are VISUAL ONLY — stroke width and colour reach no screen
|
|
1023
|
+
* reader. This component cannot set its own accessible name: React Flow's
|
|
1024
|
+
* `EdgeWrapper` sources it from `edge.ariaLabel` on the edge OBJECT, one level
|
|
1025
|
+
* above the component it renders as a child (issue #285). Run your `edges`
|
|
1026
|
+
* through `withWeightedEdgeAria` (from `./edge-aria`, or set `edge.ariaLabel`
|
|
1027
|
+
* yourself) before handing them to `<CanvasShell>`/`<ReactFlow>` — otherwise
|
|
1028
|
+
* the measure this component exists to show reaches no assistive technology,
|
|
1029
|
+
* and the edge announces only React Flow's generic "Edge from n1 to n2".
|
|
1030
|
+
* `withWeightedEdgeAria` returns a new array each call — memoize it
|
|
1031
|
+
* (`useMemo(() => withWeightedEdgeAria(edges), [edges])`) if you call it
|
|
1032
|
+
* inline in render.
|
|
1033
|
+
*
|
|
1034
|
+
* `data-weight`/`data-value` are also stamped onto the rendered `<path>` (raw
|
|
1035
|
+
* `data.weight`/`data.value`, not the scaled stroke width) — a stable
|
|
1036
|
+
* selector for tests/consumers, independent of the naming seam above.
|
|
1037
|
+
*
|
|
1038
|
+
* `data.variant: "back"` marks an edge that runs against the process direction
|
|
1039
|
+
* (dagre's reversed edges — see `layoutFlow`'s `backEdges`). It is dashed and
|
|
1040
|
+
* routed clear of the forward edge between the same two nodes, and carries a
|
|
1041
|
+
* real accessible name; the default `"forward"` renders exactly as before.
|
|
1042
|
+
*
|
|
1043
|
+
* Selected state uses `--ring` (matching the `ring-ring` treatment `FlowNode`/
|
|
1044
|
+
* `FlowGroupNode` use), overriding weight/value-derived width and colour so a
|
|
1045
|
+
* selected edge always reads clearly. No stroke-dasharray animation — reduced
|
|
1046
|
+
* motion is respected because there is no motion to reduce.
|
|
1047
|
+
*
|
|
1048
|
+
* KEYBOARD FOCUS is a separate state, drawn by `FlowEdgePath` (#286): selection
|
|
1049
|
+
* needs a consumer's `onEdgesChange` to ever become true, so it can never be the
|
|
1050
|
+
* indicator a tab stop owes its user.
|
|
1051
|
+
*/
|
|
1052
|
+
declare function FlowWeightedEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, style, selected, data, }: EdgeProps<BrandFlowWeightedEdge>): react.JSX.Element;
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Where a back edge's return leg runs, so it stays VISIBLE.
|
|
1056
|
+
*
|
|
1057
|
+
* A back edge is drawn as a smoothstep with one long cross-segment running against the
|
|
1058
|
+
* layout direction. React Flow puts that segment halfway between the two handles by
|
|
1059
|
+
* default, and a fixed nudge off the midpoint does not help: in a top-down layout the
|
|
1060
|
+
* midpoint of a rework edge sits between two ranks, i.e. squarely inside the column of
|
|
1061
|
+
* cards it is supposed to run past. Edges paint UNDER nodes, so the segment vanishes
|
|
1062
|
+
* behind the cards and all the reader is left with is a dashed stub below one node and
|
|
1063
|
+
* another above the other — which reads as a stray rectangle, not as a loop back.
|
|
1064
|
+
*
|
|
1065
|
+
* So the leg is placed OUTSIDE every card it would otherwise pass behind: past the far
|
|
1066
|
+
* side of every node whose extent overlaps the span the edge crosses. That is how a
|
|
1067
|
+
* process-mining tool draws a rework loop — out, around the rank, back in — and it is
|
|
1068
|
+
* the only placement that is correct for a graph rather than for a pair of nodes.
|
|
1069
|
+
*/
|
|
1070
|
+
/** One node's laid-out box, in flow coordinates. */
|
|
1071
|
+
interface BackEdgeNodeRect {
|
|
1072
|
+
x: number;
|
|
1073
|
+
y: number;
|
|
1074
|
+
width: number;
|
|
1075
|
+
height: number;
|
|
1076
|
+
}
|
|
1077
|
+
/** Which axis the layout's ranks advance along. */
|
|
1078
|
+
type BackEdgeAxis = "vertical" | "horizontal";
|
|
1079
|
+
/**
|
|
1080
|
+
* The `centerX` (vertical layout) or `centerY` (horizontal layout) to hand
|
|
1081
|
+
* `getSmoothStepPath`, or `null` when nothing is measured yet and React Flow's own
|
|
1082
|
+
* midpoint should stand.
|
|
1083
|
+
*
|
|
1084
|
+
* @param rects Every laid-out node on the canvas. Nodes that cannot be hit are filtered
|
|
1085
|
+
* out here rather than by the caller, so the caller stays a plain map.
|
|
1086
|
+
* @param span The interval the edge crosses on the RANK axis (`[min, max]` of the two
|
|
1087
|
+
* handle coordinates) — a node outside it is never behind this edge.
|
|
1088
|
+
* @param clearance Gap left between the leg and the outermost card it clears.
|
|
1089
|
+
*/
|
|
1090
|
+
declare function backEdgeDetour(rects: readonly BackEdgeNodeRect[], axis: BackEdgeAxis, span: readonly [number, number], clearance: number): number | null;
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* weight-scale — pure, framework-free "edge weight → stroke width" scale.
|
|
1094
|
+
*
|
|
1095
|
+
* `FlowWeightedEdge` calls this with every edge in the flow (via `useEdges()`,
|
|
1096
|
+
* memoized) so a group of edges can be min-maxed against ONE shared domain
|
|
1097
|
+
* instead of each edge picking its own arbitrary width. No React, no DOM: a
|
|
1098
|
+
* sibling (e.g. RM-045's continuous `Legend`) can call it directly to render
|
|
1099
|
+
* the same ramp the edges use, and it is trivially unit-testable.
|
|
1100
|
+
*/
|
|
1101
|
+
/** Minimal edge shape the scale needs — a subset of `Edge<FlowWeightedEdgeData>`. */
|
|
1102
|
+
interface WeightedEdgeLike {
|
|
1103
|
+
id: string;
|
|
1104
|
+
data?: {
|
|
1105
|
+
weight?: number;
|
|
1106
|
+
scaleGroup?: string;
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
interface EdgeWeightScaleOptions {
|
|
1110
|
+
/** Output stroke-width range, in px. @default [1.5, 8] */
|
|
1111
|
+
widthRange?: [number, number];
|
|
1112
|
+
/**
|
|
1113
|
+
* Restrict the domain calculation (and the returned map) to edges whose
|
|
1114
|
+
* `data.scaleGroup` equals this value — edges in a different group are
|
|
1115
|
+
* skipped entirely. Omit to compute one scale per distinct `scaleGroup`
|
|
1116
|
+
* present in `edges` (edges with no `scaleGroup` share one implicit
|
|
1117
|
+
* default group, so a flow that never sets it still gets one shared
|
|
1118
|
+
* domain — "all edges in the same `<ReactFlow>`").
|
|
1119
|
+
*/
|
|
1120
|
+
scaleGroup?: string;
|
|
1121
|
+
}
|
|
1122
|
+
/** Matches today's fixed 1.5px `FlowEdge` stroke, so an unweighted edge is unchanged. */
|
|
1123
|
+
declare const DEFAULT_EDGE_WIDTH_RANGE: [number, number];
|
|
1124
|
+
/**
|
|
1125
|
+
* Resolve every edge's `data.weight` into a stroke-width, linearly min-maxed
|
|
1126
|
+
* into `widthRange` per `scaleGroup`. An edge with no `data.weight` gets the
|
|
1127
|
+
* range floor — the existing fixed 1.5px `FlowEdge` already draws, so a plain
|
|
1128
|
+
* edge renders unchanged. An edge that is the only member of its group (or
|
|
1129
|
+
* whose group has zero weight variance) gets the midpoint of the range —
|
|
1130
|
+
* there is no domain to compare it against.
|
|
1131
|
+
*/
|
|
1132
|
+
declare function computeEdgeWeightScale(edges: WeightedEdgeLike[], opts?: EdgeWeightScaleOptions): Map<string, number>;
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* edge-aria — pure, framework-free "edge data → accessible name" naming seam
|
|
1136
|
+
* for `FlowWeightedEdge`, mirroring `weight-scale.ts`'s shape (no React, no
|
|
1137
|
+
* DOM, trivially unit-testable).
|
|
1138
|
+
*
|
|
1139
|
+
* `FlowWeightedEdge` encodes `data.weight` as stroke width and, optionally,
|
|
1140
|
+
* `data.value` as stroke colour — both purely visual. React Flow's own
|
|
1141
|
+
* `EdgeWrapper` sources an edge's accessible name from `edge.ariaLabel`
|
|
1142
|
+
* (`edge` object, not the edge COMPONENT — see issue #285), so the component
|
|
1143
|
+
* has no channel to set it. This module is the seam a caller runs their
|
|
1144
|
+
* `edges` array through instead.
|
|
1145
|
+
*
|
|
1146
|
+
* Naming contract (a decision, not an accident):
|
|
1147
|
+
* 1. An explicit `edge.ariaLabel` always wins — never overwritten.
|
|
1148
|
+
* 2. Otherwise compose `"Edge from <source> to <target>, weight <n>"`,
|
|
1149
|
+
* appending `", <valueLabel> <n>"` when `data.value` is set and the pill
|
|
1150
|
+
* text (`data.label`/`data.secondaryLabel`, space-joined) when either is
|
|
1151
|
+
* present.
|
|
1152
|
+
* 3. When an edge carries none of `weight`/`value`/`label`/`secondaryLabel`,
|
|
1153
|
+
* return `undefined` so React Flow's own default
|
|
1154
|
+
* ("Edge from <source> to <target>") survives untouched. That quoted
|
|
1155
|
+
* string is `@xyflow/react`'s output, not ours — the `e-bare` edge in
|
|
1156
|
+
* `flow-weighted-edge.stories.tsx`'s `NamingContractEdges` story (#327)
|
|
1157
|
+
* is the browser lock for it: it renders a measure-less edge against a
|
|
1158
|
+
* real `CanvasShell` and asserts the default name via
|
|
1159
|
+
* `toHaveAccessibleName`. If that story goes red after a dependency
|
|
1160
|
+
* bump, read it as a signal that React Flow's default changed, not as a
|
|
1161
|
+
* flake to silence.
|
|
1162
|
+
*/
|
|
1163
|
+
|
|
1164
|
+
interface WeightedEdgeAriaOptions {
|
|
1165
|
+
/** Word used for the width measure in the composed name. @default "weight" */
|
|
1166
|
+
weightLabel?: string;
|
|
1167
|
+
/** Word used for the colour measure in the composed name. @default "value" */
|
|
1168
|
+
valueLabel?: string;
|
|
1169
|
+
/**
|
|
1170
|
+
* Map a node id to its display name, so the composed name says
|
|
1171
|
+
* "Edge from Order placed to Picked" rather than "Edge from n1 to n2". The
|
|
1172
|
+
* caller owns node display names (this module never reaches into a `nodes`
|
|
1173
|
+
* array), so it is a hook, not an automatic lookup.
|
|
1174
|
+
*/
|
|
1175
|
+
nameOf?: (nodeId: string) => string;
|
|
1176
|
+
/** Format a numeric measure for display. @default String(n) */
|
|
1177
|
+
formatNumber?: (n: number) => string;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Derive one edge's accessible name from its weight/value/labels. Pure — no
|
|
1181
|
+
* React, no DOM. Returns:
|
|
1182
|
+
* - the edge's own `ariaLabel`, unchanged, when the caller already set one;
|
|
1183
|
+
* - a composed string when the edge carries `weight`, `value`, `label` or
|
|
1184
|
+
* `secondaryLabel`;
|
|
1185
|
+
* - `undefined` when there is nothing to add (so the default announcement
|
|
1186
|
+
* from React Flow's `EdgeWrapper` survives untouched).
|
|
1187
|
+
*/
|
|
1188
|
+
declare function buildWeightedEdgeAriaLabel(edge: BrandFlowWeightedEdge, opts?: WeightedEdgeAriaOptions): string | undefined;
|
|
1189
|
+
/**
|
|
1190
|
+
* Stamp `ariaLabel` onto every weighted edge that has none, per
|
|
1191
|
+
* `buildWeightedEdgeAriaLabel`'s naming contract. An edge that already
|
|
1192
|
+
* carries an `ariaLabel` is returned unchanged (never overwritten); an edge
|
|
1193
|
+
* with nothing to add is also returned unchanged, so an unrelated edge type
|
|
1194
|
+
* sharing this array keeps its identity.
|
|
1195
|
+
*
|
|
1196
|
+
* Returns a NEW array — memoize the call when used inline in render (e.g.
|
|
1197
|
+
* `useMemo(() => withWeightedEdgeAria(edges), [edges])`), or a consumer hands
|
|
1198
|
+
* React Flow a new `edges` identity every frame.
|
|
1199
|
+
*/
|
|
1200
|
+
declare function withWeightedEdgeAria<E extends BrandFlowWeightedEdge>(edges: E[], opts?: WeightedEdgeAriaOptions): E[];
|
|
1201
|
+
|
|
1202
|
+
interface FlowSelfLoopEdgeData extends Record<string, unknown> {
|
|
1203
|
+
/**
|
|
1204
|
+
* Frequency/volume this loop carries. Scaled into stroke width by the SAME
|
|
1205
|
+
* `computeEdgeWeightScale` domain as `FlowWeightedEdge`, so a loop's weight
|
|
1206
|
+
* is directly comparable with the forward edges around it.
|
|
1207
|
+
*/
|
|
1208
|
+
weight?: number;
|
|
1209
|
+
/** Edges sharing a `scaleGroup` share one min-max width domain. @default all edges in the flow */
|
|
1210
|
+
scaleGroup?: string;
|
|
1211
|
+
/** Primary edge-label-pill text, e.g. a repeat count. */
|
|
1212
|
+
label?: string;
|
|
1213
|
+
/** Secondary edge-label-pill text, e.g. an average duration. */
|
|
1214
|
+
secondaryLabel?: string;
|
|
1215
|
+
/** Radius of the arc, in px. @default 28 */
|
|
1216
|
+
loopRadius?: number;
|
|
1217
|
+
/**
|
|
1218
|
+
* Overrides the accessible name given to the loop's graphic. Defaults to
|
|
1219
|
+
* "Self-loop on <node> — this step repeats".
|
|
1220
|
+
*/
|
|
1221
|
+
loopLabel?: string;
|
|
1222
|
+
/**
|
|
1223
|
+
* Passed straight through to the rendered `EdgeLabelPill`'s `className`/`...props`
|
|
1224
|
+
* (see `EdgeLabelPillProps`) — the seam a composing package (e.g.
|
|
1225
|
+
* `@elabs-ai/components-process`'s `ProcessTransitionEdge`) uses to reach the pill's
|
|
1226
|
+
* own root button from outside this component, without a new semantic prop here.
|
|
1227
|
+
*/
|
|
1228
|
+
labelProps?: Omit<EdgeLabelPillProps, "label" | "secondaryLabel" | "x" | "y" | "selected">;
|
|
1229
|
+
}
|
|
1230
|
+
type BrandFlowSelfLoopEdge = Edge<FlowSelfLoopEdgeData, "self-loop">;
|
|
1231
|
+
/**
|
|
1232
|
+
* Branded self-loop edge: an edge whose `source === target` — the "this step
|
|
1233
|
+
* repeated" signal of a process map. Register it in
|
|
1234
|
+
* `edgeTypes={{ "self-loop": FlowSelfLoopEdge }}` and create edges with
|
|
1235
|
+
* `type: "self-loop"` and `data: FlowSelfLoopEdgeData`.
|
|
1236
|
+
*
|
|
1237
|
+
* dagre cannot lay a self-loop out, so `layoutFlow` withholds them from the
|
|
1238
|
+
* graph entirely and reports their ids in `selfLoops` — this component draws
|
|
1239
|
+
* the arc itself, from the node's own live geometry (`useInternalNode`).
|
|
1240
|
+
*
|
|
1241
|
+
* ## It terminates ON the handle dots, like every other brand edge
|
|
1242
|
+
*
|
|
1243
|
+
* The two handle points of a self-loop sit on OPPOSITE sides of one node, so they
|
|
1244
|
+
* describe no useful straight line — which is why this edge used to ignore them and
|
|
1245
|
+
* arc over the node's top edge instead, with its feet on bare border a loop-radius
|
|
1246
|
+
* away from the nearest dot. Measured on the process map, that read as a detached
|
|
1247
|
+
* arc floating above the card: 24 px clear of any dot in a top-to-bottom layout, and
|
|
1248
|
+
* 69 px in a left-to-right one, where the loop stayed stubbornly on TOP while the
|
|
1249
|
+
* flow (and the handles) had moved to the sides. Both are the "an edge terminates on
|
|
1250
|
+
* a handle dot" rule being broken, just by a component that had declared itself
|
|
1251
|
+
* exempt.
|
|
1252
|
+
*
|
|
1253
|
+
* `selfLoopHandleArc` keeps the loop a SHAPE — it just bulges clear of the node on
|
|
1254
|
+
* the side a quarter turn from the source handle's own normal, so it lassos down the
|
|
1255
|
+
* right in a top-to-bottom layout and arcs over the top in a left-to-right one,
|
|
1256
|
+
* without this component knowing which direction is in force. Before the node is
|
|
1257
|
+
* measured there is nothing to clear, so it falls back to the node-box arc
|
|
1258
|
+
* (`selfLoopPath`) rather than to `NaN`.
|
|
1259
|
+
*
|
|
1260
|
+
* The loop is distinguished from a forward edge by its SHAPE, not by colour —
|
|
1261
|
+
* a closed arc above the node, legible in greyscale and in every theme — and
|
|
1262
|
+
* publishes that meaning as a real accessible name, because a `data-slot` is
|
|
1263
|
+
* invisible to assistive technology. Its label is an `EdgeLabelPill` at the
|
|
1264
|
+
* arc's apex, a genuine keyboard tab stop with a visible focus ring.
|
|
1265
|
+
*
|
|
1266
|
+
* Stroke width comes from the same `computeEdgeWeightScale` domain
|
|
1267
|
+
* `FlowWeightedEdge` uses, so a loop weighted 8 reads as thick as a forward
|
|
1268
|
+
* edge weighted 8. Nothing animates, so there is no motion to reduce.
|
|
1269
|
+
*
|
|
1270
|
+
* The arc is drawn through `FlowEdgePath`, so it inherits the shared keyboard
|
|
1271
|
+
* focus indicator (#286) rather than having to opt into it.
|
|
1272
|
+
*/
|
|
1273
|
+
declare function FlowSelfLoopEdge({ id, source, sourceX, sourceY, targetX, targetY, markerEnd, style, selected, data, }: EdgeProps<BrandFlowSelfLoopEdge>): react.JSX.Element;
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* self-loop-geometry — pure, framework-free arc math for a self-referencing edge.
|
|
1277
|
+
*
|
|
1278
|
+
* Kept out of the component (and out of React) so the loop's shape can be
|
|
1279
|
+
* unit-tested without a canvas, and so a sibling that needs the same apex
|
|
1280
|
+
* point (a legend, an overlay, a screenshot harness) can compute it directly.
|
|
1281
|
+
*/
|
|
1282
|
+
/** Default arc radius, in px. A loop this size clears `FlowNode`'s header without dominating it. */
|
|
1283
|
+
declare const DEFAULT_LOOP_RADIUS = 28;
|
|
1284
|
+
/** The box a self-loop is drawn above. Matches a React Flow `InternalNode` structurally. */
|
|
1285
|
+
interface SelfLoopAnchor {
|
|
1286
|
+
/** Horizontal centre of the node the loop belongs to. */
|
|
1287
|
+
centerX: number;
|
|
1288
|
+
/** Top edge of that node — the loop is drawn above this line. */
|
|
1289
|
+
topY: number;
|
|
1290
|
+
}
|
|
1291
|
+
interface SelfLoopPath {
|
|
1292
|
+
/** SVG `d` for the arc. */
|
|
1293
|
+
path: string;
|
|
1294
|
+
/** Where a label belongs: the arc's apex. */
|
|
1295
|
+
labelX: number;
|
|
1296
|
+
labelY: number;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* A cubic arc that leaves the node's top-RIGHT, bulges up over the node, and
|
|
1300
|
+
* re-enters at its top-LEFT — the conventional "this step repeated" mark in a
|
|
1301
|
+
* process map, and a SHAPE rather than a colour, so it survives greyscale.
|
|
1302
|
+
*
|
|
1303
|
+
* The curve is symmetric about `centerX`, which puts its `t = 0.5` midpoint
|
|
1304
|
+
* exactly at `(centerX, topY - APEX_FACTOR × loopRadius)` — the apex the label
|
|
1305
|
+
* is anchored to. Never returns `NaN`: a non-finite input falls back to `0`
|
|
1306
|
+
* and a non-positive radius falls back to the default.
|
|
1307
|
+
*/
|
|
1308
|
+
declare function selfLoopPath(anchor: SelfLoopAnchor, loopRadius: number): SelfLoopPath;
|
|
1309
|
+
/**
|
|
1310
|
+
* The two handle anchors a self-loop actually joins, plus the box it has to clear.
|
|
1311
|
+
*
|
|
1312
|
+
* A self-loop's `sourceX/sourceY` and `targetX/targetY` are the SAME node's two handle
|
|
1313
|
+
* points, which is why they describe no useful straight line — but they are still the
|
|
1314
|
+
* only two points on the canvas the reader recognises as connectors, so the arc has to
|
|
1315
|
+
* start and end exactly on them. {@link selfLoopHandleArc} is the geometry that does
|
|
1316
|
+
* that; {@link selfLoopPath} remains the node-box arc used before a node is measured.
|
|
1317
|
+
*/
|
|
1318
|
+
interface SelfLoopHandleAnchor {
|
|
1319
|
+
/** Where the loop leaves the node — React Flow's own SOURCE handle anchor. */
|
|
1320
|
+
sourceX: number;
|
|
1321
|
+
sourceY: number;
|
|
1322
|
+
/** Where it re-enters — React Flow's own TARGET handle anchor. */
|
|
1323
|
+
targetX: number;
|
|
1324
|
+
targetY: number;
|
|
1325
|
+
/** The node's centre. Decides which way is "out of the card". */
|
|
1326
|
+
centerX: number;
|
|
1327
|
+
centerY: number;
|
|
1328
|
+
/** The node's rendered size, so the arc clears the card instead of crossing it. */
|
|
1329
|
+
width: number;
|
|
1330
|
+
height: number;
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* A cubic that leaves the SOURCE handle dot, bulges clear of the node on one side, and
|
|
1334
|
+
* re-enters at the TARGET handle dot.
|
|
1335
|
+
*
|
|
1336
|
+
* It is direction-agnostic by construction: the bulge is the source normal rotated a
|
|
1337
|
+
* quarter turn, so a top-to-bottom layout (handles on the bottom and top) gets a lasso
|
|
1338
|
+
* down the node's right-hand side, and a left-to-right layout (handles on the right and
|
|
1339
|
+
* left) gets an arc over the top — with no `direction` prop, and no list of cases to keep
|
|
1340
|
+
* in step with `layoutFlow`'s `HANDLE_BY_DIRECTION`.
|
|
1341
|
+
*
|
|
1342
|
+
* `loopRadius` is how far the curve shoots straight out of each dot before it turns, and
|
|
1343
|
+
* also the gap the arc's widest point keeps from the card's edge. The label sits at that
|
|
1344
|
+
* widest point — the cubic's `t = 0.5` midpoint — so it is off the node by construction.
|
|
1345
|
+
*
|
|
1346
|
+
* Never returns `NaN`: non-finite inputs fall back to `0`, a non-positive radius to
|
|
1347
|
+
* {@link DEFAULT_LOOP_RADIUS}, and a handle point sitting on the node's centre (nothing
|
|
1348
|
+
* measured yet) to a downward source normal.
|
|
1349
|
+
*/
|
|
1350
|
+
declare function selfLoopHandleArc(anchor: SelfLoopHandleAnchor, loopRadius: number): SelfLoopPath;
|
|
1351
|
+
|
|
1352
|
+
export { type BackEdgeAxis, type BackEdgeNodeRect, type BrandFlowButtonEdge, type BrandFlowGroupNode, type BrandFlowNode, type BrandFlowPlaceholderNode, type BrandFlowSelfLoopEdge, type BrandFlowWeightedEdge, CanvasShell, type CanvasShellProps, type ClosestAnchors, type ClosestHandles, DEFAULT_EDGE_WIDTH_RANGE, DEFAULT_LOOP_RADIUS, EdgeLabelPill, type EdgeLabelPillProps, type EdgeParams, type EdgeWeightScaleOptions, FLOW_ALL_SIDE_HANDLES, FLOW_EDGE_FOCUS_CONTOUR_WIDTH, FLOW_EDGE_FOCUS_RING_WIDTH, FLOW_GROUP_NODE_TYPE, FLOW_HANDLE_ANCHOR_CLASS, type FloatingEdgeData, type FloatingNodeGeometry, FlowButtonEdge, type FlowButtonEdgeData, FlowEdge, FlowEdgePath, type FlowEdgePathProps, FlowFloatingEdge, FlowGroupNode, type FlowGroupNodeData, type FlowGroupOperationResult, type FlowGroupProxyEdgeData, type FlowGroupTone, type FlowHandleSide, type FlowLayoutDirection, type FlowLayoutOptions, type FlowLayoutResult, FlowMiniMap, type FlowMiniMapProps, FlowNode, type FlowNodeData, type FlowNodeHandles, FlowPlaceholderNode, type FlowPlaceholderNodeData, FlowSelfLoopEdge, type FlowSelfLoopEdgeData, FlowSmartEdge, FlowWeightedEdge, type FlowWeightedEdgeData, type GroupNodesOptions, type GroupOptions, HANDLE_SIDES, type HandleAnchor, type HelperLineRect, HelperLines, type HelperLinesProps, type HelperLinesResult, InspectorPanel, type InspectorPanelProps, type LayoutAlgorithm, type LayoutOptions, type LayoutSpacing, Legend, type LegendCategoricalProps, type LegendItem, type LegendProps, type LegendScaleProps, type NodeRect, type SelfLoopAnchor, type SelfLoopHandleAnchor, type SelfLoopPath, type UseFlowGroupsResult, type UseFlowLayoutResult, type UseHelperLinesOptions, type UseHelperLinesResult, type WeightedEdgeAriaOptions, type WeightedEdgeLike, ZoomControls, type ZoomControlsProps, backEdgeDetour, buildWeightedEdgeAriaLabel, collapseGroup, computeEdgeWeightScale, expandGroup, getEdgeParams, getHelperLines, groupNodes, handlePoint, isFlowGroupProxyEdge, layoutFlow, layoutGraph, pickClosestAnchors, pickClosestHandles, positionToSide, selfLoopHandleArc, selfLoopPath, sideToPosition, toggleGroupCollapsed, ungroup, useAutoLayout, useFlowGroups, useFlowLayout, useHelperLines, withWeightedEdgeAria };
|