@devfellowship/components 3.0.1 → 3.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.
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/canvas/index.ts
31
+ var canvas_exports = {};
32
+ __export(canvas_exports, {
33
+ DEFAULT_NODE_HEIGHT: () => DEFAULT_NODE_HEIGHT,
34
+ DEFAULT_NODE_SEPARATION: () => DEFAULT_NODE_SEPARATION,
35
+ DEFAULT_NODE_WIDTH: () => DEFAULT_NODE_WIDTH,
36
+ DEFAULT_RANK_SEPARATION: () => DEFAULT_RANK_SEPARATION,
37
+ FlowCanvas: () => FlowCanvas,
38
+ layoutGraph: () => layoutGraph
39
+ });
40
+ module.exports = __toCommonJS(canvas_exports);
41
+
42
+ // src/canvas/FlowCanvas.tsx
43
+ var import_react = require("react");
44
+ var import_react2 = require("@xyflow/react");
45
+
46
+ // src/lib/utils.ts
47
+ var import_clsx = require("clsx");
48
+ var import_tailwind_merge = require("tailwind-merge");
49
+ function cn(...inputs) {
50
+ return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
51
+ }
52
+
53
+ // src/canvas/layout.ts
54
+ var import_dagre = __toESM(require("@dagrejs/dagre"), 1);
55
+ var DEFAULT_NODE_WIDTH = 240;
56
+ var DEFAULT_NODE_HEIGHT = 202;
57
+ var DEFAULT_NODE_SEPARATION = 48;
58
+ var DEFAULT_RANK_SEPARATION = 120;
59
+ function layoutGraph(nodes, edges, options = {}) {
60
+ const {
61
+ direction = "LR",
62
+ nodeWidth = DEFAULT_NODE_WIDTH,
63
+ nodeHeight = DEFAULT_NODE_HEIGHT,
64
+ nodeSeparation = DEFAULT_NODE_SEPARATION,
65
+ rankSeparation = DEFAULT_RANK_SEPARATION
66
+ } = options;
67
+ const known = new Set(nodes.map((n) => n.id));
68
+ const usable = edges.filter((e) => known.has(e.source) && known.has(e.target));
69
+ const g = new import_dagre.default.graphlib.Graph();
70
+ g.setDefaultEdgeLabel(() => ({}));
71
+ g.setGraph({ rankdir: direction, nodesep: nodeSeparation, ranksep: rankSeparation, marginx: 24, marginy: 24 });
72
+ for (const n of nodes) g.setNode(n.id, { width: nodeWidth, height: nodeHeight });
73
+ for (const e of usable) g.setEdge(e.source, e.target);
74
+ import_dagre.default.layout(g);
75
+ const rfNodes = nodes.map((n) => {
76
+ const laid = g.node(n.id);
77
+ return {
78
+ id: n.id,
79
+ type: "flowCanvasCard",
80
+ position: {
81
+ x: laid ? laid.x - nodeWidth / 2 : 0,
82
+ y: laid ? laid.y - nodeHeight / 2 : 0
83
+ },
84
+ data: { node: n },
85
+ // Explicit width/height, not just post-mount measurement: React Flow's
86
+ // MiniMap only draws a node rect once the node "has dimensions"
87
+ // (`measured` OR explicit width/height — see @xyflow/system
88
+ // nodeHasDimensions). Without this the minimap renders zero rects and
89
+ // reads as an empty panel.
90
+ width: nodeWidth,
91
+ height: nodeHeight
92
+ };
93
+ });
94
+ const pairCounts = /* @__PURE__ */ new Map();
95
+ for (const e of usable) {
96
+ const key = pairKey(e);
97
+ pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
98
+ }
99
+ const pairSeen = /* @__PURE__ */ new Map();
100
+ const rfEdges = usable.map((e) => {
101
+ const key = pairKey(e);
102
+ const pairIndex = pairSeen.get(key) ?? 0;
103
+ pairSeen.set(key, pairIndex + 1);
104
+ return {
105
+ id: e.id,
106
+ source: e.source,
107
+ target: e.target,
108
+ label: e.label,
109
+ type: "flowCanvasChip",
110
+ data: { pairIndex, pairCount: pairCounts.get(key) ?? 1 }
111
+ };
112
+ });
113
+ return { nodes: rfNodes, edges: rfEdges };
114
+ }
115
+ function pairKey(e) {
116
+ return [e.source, e.target].sort().join("|");
117
+ }
118
+
119
+ // src/canvas/FlowCanvas.tsx
120
+ var import_jsx_runtime = require("react/jsx-runtime");
121
+ var DEFAULT_CARD_CONTENT_HEIGHT = 130;
122
+ var MINIMAP_NODE_COLOR = "#E07A4A";
123
+ var MINIMAP_MASK_COLOR = "rgba(10,9,8,0.72)";
124
+ var MINIMAP_BG_COLOR = "#141210";
125
+ var FlowCanvasContext = (0, import_react.createContext)(null);
126
+ function useFlowCanvas() {
127
+ const ctx = (0, import_react.useContext)(FlowCanvasContext);
128
+ if (!ctx) throw new Error("FlowCanvas internals rendered outside of <FlowCanvas>");
129
+ return ctx;
130
+ }
131
+ function FlowCanvasCard({ data }) {
132
+ const { renderCard, renderCaption, clickable, nodeWidth, cardContentHeight, horizontal, testIdPrefix } = useFlowCanvas();
133
+ const node = data.node;
134
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
135
+ "div",
136
+ {
137
+ className: cn(
138
+ "overflow-hidden rounded-lg border border-s-brand-ring bg-card shadow-lg shadow-black/40",
139
+ clickable && "cursor-pointer"
140
+ ),
141
+ style: { width: nodeWidth },
142
+ "data-testid": `${testIdPrefix}-node`,
143
+ "data-node-id": node.id,
144
+ children: [
145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
146
+ import_react2.Handle,
147
+ {
148
+ type: "target",
149
+ position: horizontal ? import_react2.Position.Left : import_react2.Position.Top,
150
+ className: "!h-2 !w-2 !border-none !bg-primary"
151
+ }
152
+ ),
153
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
154
+ "div",
155
+ {
156
+ className: "flex w-full items-center justify-center overflow-hidden border-b border-border bg-muted",
157
+ style: { height: cardContentHeight },
158
+ "data-testid": `${testIdPrefix}-node-content`,
159
+ children: renderCard(node)
160
+ }
161
+ ),
162
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "px-3 py-2 text-xs", "data-testid": `${testIdPrefix}-node-caption`, children: renderCaption ? renderCaption(node) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
163
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "truncate font-semibold text-foreground", title: node.title, children: node.title }),
164
+ node.subtitle ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "truncate font-mono text-muted-foreground", title: node.subtitle, children: node.subtitle }) : null
165
+ ] }) }),
166
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
167
+ import_react2.Handle,
168
+ {
169
+ type: "source",
170
+ position: horizontal ? import_react2.Position.Right : import_react2.Position.Bottom,
171
+ className: "!h-2 !w-2 !border-none !bg-primary"
172
+ }
173
+ )
174
+ ]
175
+ }
176
+ );
177
+ }
178
+ function FlowCanvasChipEdge({
179
+ id,
180
+ sourceX,
181
+ sourceY,
182
+ targetX,
183
+ targetY,
184
+ sourcePosition,
185
+ targetPosition,
186
+ label,
187
+ data
188
+ }) {
189
+ const { testIdPrefix } = useFlowCanvas();
190
+ const [edgePath, labelX, labelY] = (0, import_react2.getSmoothStepPath)({
191
+ sourceX,
192
+ sourceY,
193
+ targetX,
194
+ targetY,
195
+ sourcePosition,
196
+ targetPosition
197
+ });
198
+ const pairIndex = data?.pairIndex ?? 0;
199
+ const pairCount = data?.pairCount ?? 1;
200
+ const offsetY = pairCount > 1 ? (pairIndex - (pairCount - 1) / 2) * 22 : 0;
201
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.BaseEdge, { id, path: edgePath, style: { stroke: "var(--s-brand-ring)", strokeWidth: 1.5 } }),
203
+ label ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.EdgeLabelRenderer, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
204
+ "div",
205
+ {
206
+ className: "nodrag nopan pointer-events-none absolute rounded-md border border-s-brand-ring bg-popover px-1.5 py-0.5 text-[10px] font-semibold text-foreground",
207
+ style: { transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY + offsetY}px)` },
208
+ "data-testid": `${testIdPrefix}-edge-label`,
209
+ children: label
210
+ }
211
+ ) }) : null
212
+ ] });
213
+ }
214
+ var nodeTypes = { flowCanvasCard: FlowCanvasCard };
215
+ var edgeTypes = { flowCanvasChip: FlowCanvasChipEdge };
216
+ function FlowCanvas({
217
+ nodes,
218
+ edges,
219
+ renderCard,
220
+ renderCaption,
221
+ onNodeClick,
222
+ direction = "LR",
223
+ nodeWidth = DEFAULT_NODE_WIDTH,
224
+ nodeHeight = DEFAULT_NODE_HEIGHT,
225
+ cardContentHeight = DEFAULT_CARD_CONTENT_HEIGHT,
226
+ nodeSeparation,
227
+ rankSeparation,
228
+ colorMode = "dark",
229
+ miniMap = true,
230
+ controls = true,
231
+ background = true,
232
+ testIdPrefix = "flow",
233
+ className,
234
+ ariaLabel = "Flow canvas"
235
+ }) {
236
+ const graph = (0, import_react.useMemo)(
237
+ () => layoutGraph(nodes, edges, { direction, nodeWidth, nodeHeight, nodeSeparation, rankSeparation }),
238
+ [nodes, edges, direction, nodeWidth, nodeHeight, nodeSeparation, rankSeparation]
239
+ );
240
+ const byId = (0, import_react.useMemo)(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
241
+ const ctx = (0, import_react.useMemo)(
242
+ () => ({
243
+ renderCard,
244
+ renderCaption,
245
+ clickable: Boolean(onNodeClick),
246
+ nodeWidth,
247
+ cardContentHeight,
248
+ horizontal: direction === "LR",
249
+ testIdPrefix
250
+ }),
251
+ [renderCard, renderCaption, onNodeClick, nodeWidth, cardContentHeight, direction, testIdPrefix]
252
+ );
253
+ const miniMapColors = typeof miniMap === "object" ? miniMap : {};
254
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlowCanvasContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
255
+ "div",
256
+ {
257
+ className: cn(
258
+ "h-[70vh] min-h-[520px] w-full overflow-hidden rounded-lg border border-border bg-card",
259
+ className
260
+ ),
261
+ "data-testid": `${testIdPrefix}-canvas`,
262
+ role: "region",
263
+ "aria-label": ariaLabel,
264
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
265
+ import_react2.ReactFlow,
266
+ {
267
+ nodes: graph.nodes,
268
+ edges: graph.edges,
269
+ nodeTypes,
270
+ edgeTypes,
271
+ colorMode,
272
+ fitView: true,
273
+ proOptions: { hideAttribution: true },
274
+ onNodeClick: onNodeClick ? (event, rfNode) => {
275
+ const node = byId.get(rfNode.id);
276
+ if (node) onNodeClick(node, event);
277
+ } : void 0,
278
+ children: [
279
+ background ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.Background, {}) : null,
280
+ controls ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.Controls, { showInteractive: false }) : null,
281
+ miniMap ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
282
+ import_react2.MiniMap,
283
+ {
284
+ pannable: true,
285
+ zoomable: true,
286
+ nodeColor: miniMapColors.nodeColor ?? MINIMAP_NODE_COLOR,
287
+ maskColor: miniMapColors.maskColor ?? MINIMAP_MASK_COLOR,
288
+ bgColor: miniMapColors.bgColor ?? MINIMAP_BG_COLOR
289
+ }
290
+ ) : null
291
+ ]
292
+ }
293
+ )
294
+ }
295
+ ) });
296
+ }
297
+ // Annotate the CommonJS export names for ESM import in node:
298
+ 0 && (module.exports = {
299
+ DEFAULT_NODE_HEIGHT,
300
+ DEFAULT_NODE_SEPARATION,
301
+ DEFAULT_NODE_WIDTH,
302
+ DEFAULT_RANK_SEPARATION,
303
+ FlowCanvas,
304
+ layoutGraph
305
+ });
@@ -0,0 +1,175 @@
1
+ import * as React from 'react';
2
+ import { ReactNode, MouseEvent } from 'react';
3
+ import { Node, Edge } from '@xyflow/react';
4
+
5
+ /**
6
+ * A node the canvas can lay out and draw a card for.
7
+ *
8
+ * The canvas reads exactly three things: `id` (identity, edge endpoints, React
9
+ * keys), `title` and `subtitle` (the caption). Everything else a lens needs
10
+ * travels in `data`, which the canvas NEVER inspects — it only hands it back to
11
+ * `renderCard` and `onNodeClick`. That opacity is the whole point: a card can
12
+ * draw a low-fidelity wireframe, a real screenshot, a coverage heat cell or a
13
+ * diff badge, and the canvas cannot tell the difference.
14
+ */
15
+ interface FlowCanvasNode<TData = unknown> {
16
+ /** Stable identity. Edges point at it and React keys off it. */
17
+ id: string;
18
+ /** First caption line. */
19
+ title: string;
20
+ /** Second caption line, rendered monospaced. Typically a route or a path. */
21
+ subtitle?: string;
22
+ /** Opaque to the canvas. Handed back to `renderCard` and `onNodeClick`. */
23
+ data?: TData;
24
+ }
25
+ /** A directed edge between two `FlowCanvasNode.id`s. */
26
+ interface FlowCanvasEdge {
27
+ /** Stable identity. Two edges between the same pair must still differ here. */
28
+ id: string;
29
+ source: string;
30
+ target: string;
31
+ /** Drawn as a chip on the path midpoint. Omit for an unlabelled edge. */
32
+ label?: string;
33
+ }
34
+ /** Layout direction. `LR` reads as a journey, `TB` as a hierarchy. */
35
+ type FlowCanvasDirection = "LR" | "TB";
36
+ /**
37
+ * MiniMap colours. React Flow's MiniMap takes colour STRINGS (it writes them
38
+ * onto SVG `fill`/`stroke` attributes), so it cannot take a Tailwind class or a
39
+ * CSS custom property reliably. The defaults are the DFL sand/amber values
40
+ * written out literally for that reason — not because the tokens were ignored.
41
+ */
42
+ interface FlowCanvasMiniMapColors {
43
+ nodeColor?: string;
44
+ maskColor?: string;
45
+ bgColor?: string;
46
+ }
47
+ interface FlowCanvasProps<TData = unknown> {
48
+ /** The graph. Positions are computed here — do not pre-position. */
49
+ nodes: FlowCanvasNode<TData>[];
50
+ edges: FlowCanvasEdge[];
51
+ /**
52
+ * THE CARD-CONTENT SLOT. Fills the card's content area, above the caption.
53
+ *
54
+ * This is the seam that keeps the canvas reusable: the consuming lens decides
55
+ * what a card shows. Return a sketch, an `<img>`, a chart, a placeholder —
56
+ * the canvas renders the frame around it and nothing else.
57
+ */
58
+ renderCard: (node: FlowCanvasNode<TData>) => ReactNode;
59
+ /**
60
+ * Optional override for the caption under the card content. The default
61
+ * renders `title` over `subtitle`, which is what makes every lens's canvas
62
+ * read as the same map. Override it when a lens needs a badge or a marker
63
+ * in the caption row.
64
+ */
65
+ renderCaption?: (node: FlowCanvasNode<TData>) => ReactNode;
66
+ /**
67
+ * Click handler for a card. The node comes first because that is what a lens
68
+ * acts on; the DOM event is there for modifier keys.
69
+ *
70
+ * When omitted the cards are not interactive and no pointer affordance is
71
+ * drawn — a canvas whose cards do nothing must not look clickable.
72
+ */
73
+ onNodeClick?: (node: FlowCanvasNode<TData>, event: MouseEvent) => void;
74
+ /** @default "LR" */
75
+ direction?: FlowCanvasDirection;
76
+ /** Card width in px. Also the box dagre lays out around. @default 240 */
77
+ nodeWidth?: number;
78
+ /** Card height in px, caption included. @default 202 */
79
+ nodeHeight?: number;
80
+ /** Height of the card-content area in px. @default 130 */
81
+ cardContentHeight?: number;
82
+ /** Horizontal gap between siblings in a rank. @default 48 */
83
+ nodeSeparation?: number;
84
+ /** Gap between ranks. @default 120 */
85
+ rankSeparation?: number;
86
+ /** @default "dark" */
87
+ colorMode?: "dark" | "light" | "system";
88
+ /** `false` hides the MiniMap. An object overrides its colours. @default true */
89
+ miniMap?: boolean | FlowCanvasMiniMapColors;
90
+ /** @default true */
91
+ controls?: boolean;
92
+ /** @default true */
93
+ background?: boolean;
94
+ /**
95
+ * Prefix for every `data-testid` this component emits:
96
+ * `<prefix>-canvas`, `<prefix>-node`, `<prefix>-node-content`,
97
+ * `<prefix>-node-caption`, `<prefix>-edge-label`.
98
+ *
99
+ * A page with two canvases needs two prefixes, and a consumer whose suite
100
+ * already names the canvas can keep its selectors. @default "flow"
101
+ */
102
+ testIdPrefix?: string;
103
+ /** Applied to the canvas frame (the bordered box), not to the viewport. */
104
+ className?: string;
105
+ /** Accessible name for the canvas region. @default "Flow canvas" */
106
+ ariaLabel?: string;
107
+ }
108
+
109
+ /**
110
+ * FlowCanvas — a spatial, auto-laid-out graph of cards.
111
+ *
112
+ * ONE CANVAS, N LENSES. The canvas owns the map: layout, ranking, edge routing,
113
+ * the card frame, the caption, the minimap and the controls. The consuming lens
114
+ * owns what a card SHOWS (`renderCard`) and what a click DOES (`onNodeClick`).
115
+ * The canvas never inspects `node.data`, so a lens can put a low-fidelity
116
+ * wireframe in one card and a production screenshot in the next without the
117
+ * canvas learning about either. Duplicate the lens; never duplicate the canvas.
118
+ *
119
+ * ## Peer dependencies
120
+ * `@xyflow/react` and `@dagrejs/dagre` are OPTIONAL peer dependencies —
121
+ * install them only if you import this entry point. Consumers of
122
+ * `@devfellowship/components` who never draw a canvas pay nothing.
123
+ *
124
+ * ## Stylesheets the consumer must import
125
+ * ```css
126
+ * @import "@xyflow/react/dist/style.css"; /* React Flow's own base styles *​/
127
+ * @import "@devfellowship/components/canvas.css"; /* DS skin for its chrome *​/
128
+ * ```
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * <FlowCanvas
133
+ * nodes={screens.map((s) => ({ id: s.id, title: s.name, subtitle: s.route, data: s }))}
134
+ * edges={transitions}
135
+ * renderCard={(node) => <WireframePreview screen={node.data} />}
136
+ * onNodeClick={(node) => select(node.id)}
137
+ * />
138
+ * ```
139
+ */
140
+ declare function FlowCanvas<TData = unknown>({ nodes, edges, renderCard, renderCaption, onNodeClick, direction, nodeWidth, nodeHeight, cardContentHeight, nodeSeparation, rankSeparation, colorMode, miniMap, controls, background, testIdPrefix, className, ariaLabel, }: FlowCanvasProps<TData>): React.JSX.Element;
141
+
142
+ declare const DEFAULT_NODE_WIDTH = 240;
143
+ declare const DEFAULT_NODE_HEIGHT = 202;
144
+ declare const DEFAULT_NODE_SEPARATION = 48;
145
+ declare const DEFAULT_RANK_SEPARATION = 120;
146
+ interface LayoutOptions {
147
+ direction?: FlowCanvasDirection;
148
+ nodeWidth?: number;
149
+ nodeHeight?: number;
150
+ nodeSeparation?: number;
151
+ rankSeparation?: number;
152
+ }
153
+ /**
154
+ * Directional auto-layout for the flow canvas.
155
+ *
156
+ * Ranks nodes by dagre's layered algorithm, which computes rank AND in-rank
157
+ * order at once, so it also minimises edge crossings between ranks. The naive
158
+ * alternative — BFS level to column, insertion order to row — overlaps boxes as
159
+ * soon as the number of nodes per level varies.
160
+ *
161
+ * ⚠️ Do NOT inject a synthetic "super-source" edge to pull entry nodes to the
162
+ * front. It pushes genuine entry nodes one rank to the RIGHT of disconnected
163
+ * orphan nodes, which keep dagre's default rank 0 — so an orphan renders as if
164
+ * it came BEFORE the entry point. Plain ranking from the real edges already
165
+ * gives every node with no incoming edge rank 0, which is the correct result.
166
+ *
167
+ * The function is PURE and side-effect free, which is what makes it testable
168
+ * without a DOM: same graph in, same coordinates out.
169
+ */
170
+ declare function layoutGraph<TData>(nodes: FlowCanvasNode<TData>[], edges: FlowCanvasEdge[], options?: LayoutOptions): {
171
+ nodes: Node[];
172
+ edges: Edge[];
173
+ };
174
+
175
+ export { DEFAULT_NODE_HEIGHT, DEFAULT_NODE_SEPARATION, DEFAULT_NODE_WIDTH, DEFAULT_RANK_SEPARATION, FlowCanvas, type FlowCanvasDirection, type FlowCanvasEdge, type FlowCanvasMiniMapColors, type FlowCanvasNode, type FlowCanvasProps, type LayoutOptions, layoutGraph };
@@ -0,0 +1,175 @@
1
+ import * as React from 'react';
2
+ import { ReactNode, MouseEvent } from 'react';
3
+ import { Node, Edge } from '@xyflow/react';
4
+
5
+ /**
6
+ * A node the canvas can lay out and draw a card for.
7
+ *
8
+ * The canvas reads exactly three things: `id` (identity, edge endpoints, React
9
+ * keys), `title` and `subtitle` (the caption). Everything else a lens needs
10
+ * travels in `data`, which the canvas NEVER inspects — it only hands it back to
11
+ * `renderCard` and `onNodeClick`. That opacity is the whole point: a card can
12
+ * draw a low-fidelity wireframe, a real screenshot, a coverage heat cell or a
13
+ * diff badge, and the canvas cannot tell the difference.
14
+ */
15
+ interface FlowCanvasNode<TData = unknown> {
16
+ /** Stable identity. Edges point at it and React keys off it. */
17
+ id: string;
18
+ /** First caption line. */
19
+ title: string;
20
+ /** Second caption line, rendered monospaced. Typically a route or a path. */
21
+ subtitle?: string;
22
+ /** Opaque to the canvas. Handed back to `renderCard` and `onNodeClick`. */
23
+ data?: TData;
24
+ }
25
+ /** A directed edge between two `FlowCanvasNode.id`s. */
26
+ interface FlowCanvasEdge {
27
+ /** Stable identity. Two edges between the same pair must still differ here. */
28
+ id: string;
29
+ source: string;
30
+ target: string;
31
+ /** Drawn as a chip on the path midpoint. Omit for an unlabelled edge. */
32
+ label?: string;
33
+ }
34
+ /** Layout direction. `LR` reads as a journey, `TB` as a hierarchy. */
35
+ type FlowCanvasDirection = "LR" | "TB";
36
+ /**
37
+ * MiniMap colours. React Flow's MiniMap takes colour STRINGS (it writes them
38
+ * onto SVG `fill`/`stroke` attributes), so it cannot take a Tailwind class or a
39
+ * CSS custom property reliably. The defaults are the DFL sand/amber values
40
+ * written out literally for that reason — not because the tokens were ignored.
41
+ */
42
+ interface FlowCanvasMiniMapColors {
43
+ nodeColor?: string;
44
+ maskColor?: string;
45
+ bgColor?: string;
46
+ }
47
+ interface FlowCanvasProps<TData = unknown> {
48
+ /** The graph. Positions are computed here — do not pre-position. */
49
+ nodes: FlowCanvasNode<TData>[];
50
+ edges: FlowCanvasEdge[];
51
+ /**
52
+ * THE CARD-CONTENT SLOT. Fills the card's content area, above the caption.
53
+ *
54
+ * This is the seam that keeps the canvas reusable: the consuming lens decides
55
+ * what a card shows. Return a sketch, an `<img>`, a chart, a placeholder —
56
+ * the canvas renders the frame around it and nothing else.
57
+ */
58
+ renderCard: (node: FlowCanvasNode<TData>) => ReactNode;
59
+ /**
60
+ * Optional override for the caption under the card content. The default
61
+ * renders `title` over `subtitle`, which is what makes every lens's canvas
62
+ * read as the same map. Override it when a lens needs a badge or a marker
63
+ * in the caption row.
64
+ */
65
+ renderCaption?: (node: FlowCanvasNode<TData>) => ReactNode;
66
+ /**
67
+ * Click handler for a card. The node comes first because that is what a lens
68
+ * acts on; the DOM event is there for modifier keys.
69
+ *
70
+ * When omitted the cards are not interactive and no pointer affordance is
71
+ * drawn — a canvas whose cards do nothing must not look clickable.
72
+ */
73
+ onNodeClick?: (node: FlowCanvasNode<TData>, event: MouseEvent) => void;
74
+ /** @default "LR" */
75
+ direction?: FlowCanvasDirection;
76
+ /** Card width in px. Also the box dagre lays out around. @default 240 */
77
+ nodeWidth?: number;
78
+ /** Card height in px, caption included. @default 202 */
79
+ nodeHeight?: number;
80
+ /** Height of the card-content area in px. @default 130 */
81
+ cardContentHeight?: number;
82
+ /** Horizontal gap between siblings in a rank. @default 48 */
83
+ nodeSeparation?: number;
84
+ /** Gap between ranks. @default 120 */
85
+ rankSeparation?: number;
86
+ /** @default "dark" */
87
+ colorMode?: "dark" | "light" | "system";
88
+ /** `false` hides the MiniMap. An object overrides its colours. @default true */
89
+ miniMap?: boolean | FlowCanvasMiniMapColors;
90
+ /** @default true */
91
+ controls?: boolean;
92
+ /** @default true */
93
+ background?: boolean;
94
+ /**
95
+ * Prefix for every `data-testid` this component emits:
96
+ * `<prefix>-canvas`, `<prefix>-node`, `<prefix>-node-content`,
97
+ * `<prefix>-node-caption`, `<prefix>-edge-label`.
98
+ *
99
+ * A page with two canvases needs two prefixes, and a consumer whose suite
100
+ * already names the canvas can keep its selectors. @default "flow"
101
+ */
102
+ testIdPrefix?: string;
103
+ /** Applied to the canvas frame (the bordered box), not to the viewport. */
104
+ className?: string;
105
+ /** Accessible name for the canvas region. @default "Flow canvas" */
106
+ ariaLabel?: string;
107
+ }
108
+
109
+ /**
110
+ * FlowCanvas — a spatial, auto-laid-out graph of cards.
111
+ *
112
+ * ONE CANVAS, N LENSES. The canvas owns the map: layout, ranking, edge routing,
113
+ * the card frame, the caption, the minimap and the controls. The consuming lens
114
+ * owns what a card SHOWS (`renderCard`) and what a click DOES (`onNodeClick`).
115
+ * The canvas never inspects `node.data`, so a lens can put a low-fidelity
116
+ * wireframe in one card and a production screenshot in the next without the
117
+ * canvas learning about either. Duplicate the lens; never duplicate the canvas.
118
+ *
119
+ * ## Peer dependencies
120
+ * `@xyflow/react` and `@dagrejs/dagre` are OPTIONAL peer dependencies —
121
+ * install them only if you import this entry point. Consumers of
122
+ * `@devfellowship/components` who never draw a canvas pay nothing.
123
+ *
124
+ * ## Stylesheets the consumer must import
125
+ * ```css
126
+ * @import "@xyflow/react/dist/style.css"; /* React Flow's own base styles *​/
127
+ * @import "@devfellowship/components/canvas.css"; /* DS skin for its chrome *​/
128
+ * ```
129
+ *
130
+ * @example
131
+ * ```tsx
132
+ * <FlowCanvas
133
+ * nodes={screens.map((s) => ({ id: s.id, title: s.name, subtitle: s.route, data: s }))}
134
+ * edges={transitions}
135
+ * renderCard={(node) => <WireframePreview screen={node.data} />}
136
+ * onNodeClick={(node) => select(node.id)}
137
+ * />
138
+ * ```
139
+ */
140
+ declare function FlowCanvas<TData = unknown>({ nodes, edges, renderCard, renderCaption, onNodeClick, direction, nodeWidth, nodeHeight, cardContentHeight, nodeSeparation, rankSeparation, colorMode, miniMap, controls, background, testIdPrefix, className, ariaLabel, }: FlowCanvasProps<TData>): React.JSX.Element;
141
+
142
+ declare const DEFAULT_NODE_WIDTH = 240;
143
+ declare const DEFAULT_NODE_HEIGHT = 202;
144
+ declare const DEFAULT_NODE_SEPARATION = 48;
145
+ declare const DEFAULT_RANK_SEPARATION = 120;
146
+ interface LayoutOptions {
147
+ direction?: FlowCanvasDirection;
148
+ nodeWidth?: number;
149
+ nodeHeight?: number;
150
+ nodeSeparation?: number;
151
+ rankSeparation?: number;
152
+ }
153
+ /**
154
+ * Directional auto-layout for the flow canvas.
155
+ *
156
+ * Ranks nodes by dagre's layered algorithm, which computes rank AND in-rank
157
+ * order at once, so it also minimises edge crossings between ranks. The naive
158
+ * alternative — BFS level to column, insertion order to row — overlaps boxes as
159
+ * soon as the number of nodes per level varies.
160
+ *
161
+ * ⚠️ Do NOT inject a synthetic "super-source" edge to pull entry nodes to the
162
+ * front. It pushes genuine entry nodes one rank to the RIGHT of disconnected
163
+ * orphan nodes, which keep dagre's default rank 0 — so an orphan renders as if
164
+ * it came BEFORE the entry point. Plain ranking from the real edges already
165
+ * gives every node with no incoming edge rank 0, which is the correct result.
166
+ *
167
+ * The function is PURE and side-effect free, which is what makes it testable
168
+ * without a DOM: same graph in, same coordinates out.
169
+ */
170
+ declare function layoutGraph<TData>(nodes: FlowCanvasNode<TData>[], edges: FlowCanvasEdge[], options?: LayoutOptions): {
171
+ nodes: Node[];
172
+ edges: Edge[];
173
+ };
174
+
175
+ export { DEFAULT_NODE_HEIGHT, DEFAULT_NODE_SEPARATION, DEFAULT_NODE_WIDTH, DEFAULT_RANK_SEPARATION, FlowCanvas, type FlowCanvasDirection, type FlowCanvasEdge, type FlowCanvasMiniMapColors, type FlowCanvasNode, type FlowCanvasProps, type LayoutOptions, layoutGraph };