@gntik-ai/flow 0.0.0-stage → 0.5.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/dist/index.d.ts +217 -0
- package/dist/index.js +466 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -3
- package/src/styles.css +138 -0
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
import { Connection, DefaultEdgeOptions, Edge, EdgeMarker, Node, NodeProps, ReactFlowProps } from "@xyflow/react";
|
|
4
|
+
import { Theme } from "@gntik-ai/tokens/runtime";
|
|
5
|
+
//#region src/tones.d.ts
|
|
6
|
+
/** Node status → brand colour. Green = active/healthy (never severity); amber = degraded;
|
|
7
|
+
* red = failed; violet/rose = needs a human (handoff / review). */
|
|
8
|
+
type FlowTone = 'running' | 'degraded' | 'failed' | 'paused' | 'done' | 'handoff' | 'review';
|
|
9
|
+
/** The role of a node, which decides its handles and its icon chip. */
|
|
10
|
+
type FlowNodeKind = 'trigger' | 'step' | 'router' | 'output';
|
|
11
|
+
interface FlowToneStyle {
|
|
12
|
+
label: string;
|
|
13
|
+
/** Pill background + text (contrast-safe text aliases). */
|
|
14
|
+
pill: string;
|
|
15
|
+
/** Status dot. */
|
|
16
|
+
dot: string;
|
|
17
|
+
/** Thick card border, or null for the neutral border. */
|
|
18
|
+
border: string | null;
|
|
19
|
+
/** Brand token used for the minimap swatch. */
|
|
20
|
+
token: string;
|
|
21
|
+
/** Draws the marching segment around the card. */
|
|
22
|
+
march?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare const FLOW_TONES: Record<FlowTone, FlowToneStyle>;
|
|
25
|
+
/** Icon chip per node kind. */
|
|
26
|
+
export declare const KIND_CHIP: Record<FlowNodeKind, string>;
|
|
27
|
+
export declare function isFlowTone(v: unknown): v is FlowTone;
|
|
28
|
+
/** Token for a node's minimap swatch: its tone, else muted for routers, else primary. */
|
|
29
|
+
export declare function nodeToken(tone: unknown, kind: unknown): string;
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/decorations.d.ts
|
|
32
|
+
interface FlowNodeDecoration {
|
|
33
|
+
status?: FlowTone;
|
|
34
|
+
issues?: readonly {
|
|
35
|
+
code: string;
|
|
36
|
+
message: string;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
/** Memoized per node: unrelated status changes do not render this badge again. */
|
|
40
|
+
export declare const FlowStatusBadge: import("react").MemoExoticComponent<({ status }: {
|
|
41
|
+
status: FlowTone;
|
|
42
|
+
}) => import("react").JSX.Element>;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/nodes.d.ts
|
|
45
|
+
/** Generic node content. No domain data: title, subtitle, meta, icon and a status tone. */
|
|
46
|
+
type FlowNodeData = {
|
|
47
|
+
decoration?: FlowNodeDecoration;
|
|
48
|
+
title: string;
|
|
49
|
+
subtitle?: string;
|
|
50
|
+
meta?: string;
|
|
51
|
+
/** Decorative icon element (e.g. a lucide icon); defaults to the kind's glyph. */
|
|
52
|
+
icon?: ReactNode;
|
|
53
|
+
tone?: FlowTone;
|
|
54
|
+
/** Override the kind's default handles. */
|
|
55
|
+
handles?: {
|
|
56
|
+
target?: boolean;
|
|
57
|
+
source?: boolean;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** Built-in kinds by default; consumers can opt into custom type identifiers. */
|
|
61
|
+
type FlowNode<Kind extends string = FlowNodeKind> = Omit<Node<FlowNodeData, FlowNodeKind>, 'type'> & {
|
|
62
|
+
type: Kind;
|
|
63
|
+
};
|
|
64
|
+
/** Entry point: source handle only. */
|
|
65
|
+
export declare const TriggerNode: {
|
|
66
|
+
({ data, selected, sourcePosition, targetPosition, isConnectable }: NodeProps<FlowNode>): import("react").JSX.Element;
|
|
67
|
+
displayName: string;
|
|
68
|
+
};
|
|
69
|
+
/** Processing step: target + source handles, optional meta and status. */
|
|
70
|
+
export declare const StepNode: {
|
|
71
|
+
({ data, selected, sourcePosition, targetPosition, isConnectable }: NodeProps<FlowNode>): import("react").JSX.Element;
|
|
72
|
+
displayName: string;
|
|
73
|
+
};
|
|
74
|
+
/** Conditional branch: target + source handles. */
|
|
75
|
+
export declare const RouterNode: {
|
|
76
|
+
({ data, selected, sourcePosition, targetPosition, isConnectable }: NodeProps<FlowNode>): import("react").JSX.Element;
|
|
77
|
+
displayName: string;
|
|
78
|
+
};
|
|
79
|
+
/** Terminal node: target handle only. */
|
|
80
|
+
export declare const OutputNode: {
|
|
81
|
+
({ data, selected, sourcePosition, targetPosition, isConnectable }: NodeProps<FlowNode>): import("react").JSX.Element;
|
|
82
|
+
displayName: string;
|
|
83
|
+
};
|
|
84
|
+
/** `nodeTypes` map for `<FlowCanvas>` / `<ReactFlow>` (define it once, outside render). */
|
|
85
|
+
export declare const flowNodeTypes: {
|
|
86
|
+
trigger: {
|
|
87
|
+
({ data, selected, sourcePosition, targetPosition, isConnectable }: NodeProps<FlowNode>): import("react").JSX.Element;
|
|
88
|
+
displayName: string;
|
|
89
|
+
};
|
|
90
|
+
step: {
|
|
91
|
+
({ data, selected, sourcePosition, targetPosition, isConnectable }: NodeProps<FlowNode>): import("react").JSX.Element;
|
|
92
|
+
displayName: string;
|
|
93
|
+
};
|
|
94
|
+
router: {
|
|
95
|
+
({ data, selected, sourcePosition, targetPosition, isConnectable }: NodeProps<FlowNode>): import("react").JSX.Element;
|
|
96
|
+
displayName: string;
|
|
97
|
+
};
|
|
98
|
+
output: {
|
|
99
|
+
({ data, selected, sourcePosition, targetPosition, isConnectable }: NodeProps<FlowNode>): import("react").JSX.Element;
|
|
100
|
+
displayName: string;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/renderers.d.ts
|
|
105
|
+
/** Named connection point. Positions default to left for targets and right for sources. */
|
|
106
|
+
interface FlowHandle {
|
|
107
|
+
id: string;
|
|
108
|
+
kind: 'source' | 'target';
|
|
109
|
+
position?: 'left' | 'right' | 'top' | 'bottom';
|
|
110
|
+
label?: string;
|
|
111
|
+
}
|
|
112
|
+
/** Kit-owned renderer input: no React Flow component types are required by consumers. */
|
|
113
|
+
interface FlowNodeRenderProps {
|
|
114
|
+
id: string;
|
|
115
|
+
type: string;
|
|
116
|
+
data: FlowNodeData;
|
|
117
|
+
selected: boolean;
|
|
118
|
+
connectable: boolean;
|
|
119
|
+
}
|
|
120
|
+
interface FlowNodeRenderer {
|
|
121
|
+
type: string;
|
|
122
|
+
handles: readonly FlowHandle[];
|
|
123
|
+
/** Defaults to kit overlays. Consumer renderers may own accessible status and issue decorations. */
|
|
124
|
+
decorations?: 'kit' | 'consumer';
|
|
125
|
+
render: (props: FlowNodeRenderProps) => ReactNode;
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/FlowCanvas.d.ts
|
|
129
|
+
interface FlowCanvasProps<N extends Node = FlowNode, E extends Edge = Edge> extends Omit<ReactFlowProps<N, E>, 'aria-label' | 'height' | 'width'> {
|
|
130
|
+
/** Accessible name of the canvas region (required). */
|
|
131
|
+
'aria-label': string;
|
|
132
|
+
/** Canvas height (px or CSS length). */
|
|
133
|
+
height?: number | string;
|
|
134
|
+
showControls?: boolean;
|
|
135
|
+
/** Hide the control that toggles graph editing, while retaining zoom and fit controls. */
|
|
136
|
+
showInteractiveControls?: boolean;
|
|
137
|
+
showMiniMap?: boolean;
|
|
138
|
+
showBackground?: boolean;
|
|
139
|
+
/** false = static diagram: no pan/zoom/connect/select (nodes still render). */
|
|
140
|
+
interactive?: boolean;
|
|
141
|
+
/** Class for the inner `.react-flow` element (`className` goes on the wrapper). */
|
|
142
|
+
canvasClassName?: string;
|
|
143
|
+
/** Kit-owned custom node renderers with named handles. */
|
|
144
|
+
nodeRenderers?: readonly FlowNodeRenderer[];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* React Flow canvas themed from the brand tokens: dotted background, controls and minimap,
|
|
148
|
+
* brand node types, smoothstep edges with arrows (primary when animated). Drag, connect and
|
|
149
|
+
* zoom/pan are on by default. Import `@gntik-ai/flow/styles.css` once.
|
|
150
|
+
*/
|
|
151
|
+
export declare function FlowCanvas<N extends Node = FlowNode, E extends Edge = Edge>({ 'aria-label': label, height, showControls, showInteractiveControls, showMiniMap, showBackground, interactive, className, canvasClassName, nodeTypes, nodeRenderers, edges, defaultEdgeOptions, proOptions, children, ...rest }: FlowCanvasProps<N, E>): import("react").JSX.Element;
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/NodeCard.d.ts
|
|
154
|
+
interface NodeCardProps {
|
|
155
|
+
kind?: FlowNodeKind;
|
|
156
|
+
title: string;
|
|
157
|
+
subtitle?: string;
|
|
158
|
+
/** Footer text (left), e.g. a version or an owner. */
|
|
159
|
+
meta?: string;
|
|
160
|
+
/** Icon element (decorative); defaults to the kind's glyph. */
|
|
161
|
+
icon?: ReactNode;
|
|
162
|
+
tone?: FlowTone;
|
|
163
|
+
selected?: boolean;
|
|
164
|
+
className?: string;
|
|
165
|
+
}
|
|
166
|
+
/** The brand node card, without handles (reused by the node types and static galleries). */
|
|
167
|
+
export declare function NodeCard({ kind, icon, title, subtitle, meta, tone, selected, className }: NodeCardProps): import("react").JSX.Element;
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/glyphs.d.ts
|
|
170
|
+
/** Default decorative icon for a node kind (stroke = currentColor). */
|
|
171
|
+
export declare function KindGlyph({ kind, size }: {
|
|
172
|
+
kind: FlowNodeKind;
|
|
173
|
+
size?: number;
|
|
174
|
+
}): import("react").JSX.Element;
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/theme.d.ts
|
|
177
|
+
interface FlowTheme {
|
|
178
|
+
theme: Theme;
|
|
179
|
+
/** Idle edge / arrow colour. */
|
|
180
|
+
edge: string;
|
|
181
|
+
/** Active (animated) edge / arrow colour. */
|
|
182
|
+
edgeActive: string;
|
|
183
|
+
/** Background dot pattern. */
|
|
184
|
+
pattern: string;
|
|
185
|
+
/** Resolves any brand token (e.g. "--warning") to a concrete colour. */
|
|
186
|
+
color: (token: string, alpha?: number) => string;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Brand colours for the parts React Flow paints from props (arrow markers, minimap nodes,
|
|
190
|
+
* background pattern), resolved through `@gntik-ai/tokens/runtime` so they also survive
|
|
191
|
+
* SVG/PNG export. Re-read on every theme class change.
|
|
192
|
+
*/
|
|
193
|
+
export declare function useFlowTheme(): FlowTheme;
|
|
194
|
+
/** Closed arrow marker in a given colour. */
|
|
195
|
+
export declare function flowMarker(color: string): EdgeMarker;
|
|
196
|
+
/** Brand edge defaults: orthogonal (smoothstep) routing. */
|
|
197
|
+
export declare const brandEdgeOptions: DefaultEdgeOptions;
|
|
198
|
+
/** Adds an arrow to edges that have none: primary when animated (active), muted otherwise. */
|
|
199
|
+
export declare function withBrandMarkers<E extends Edge>(edges: readonly E[], t: FlowTheme): E[];
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/useFlowState.d.ts
|
|
202
|
+
/**
|
|
203
|
+
* Local nodes/edges state wired for drag, select and connect. New connections become
|
|
204
|
+
* active (animated) smoothstep edges. Spread the result into `<FlowCanvas>`.
|
|
205
|
+
*/
|
|
206
|
+
export declare function useFlowState<N extends Node = FlowNode, E extends Edge = Edge>(initialNodes: N[], initialEdges: E[]): {
|
|
207
|
+
nodes: N[];
|
|
208
|
+
edges: E[];
|
|
209
|
+
setNodes: import("react").Dispatch<import("react").SetStateAction<N[]>>;
|
|
210
|
+
setEdges: import("react").Dispatch<import("react").SetStateAction<E[]>>;
|
|
211
|
+
onNodesChange: import("@xyflow/react").OnNodesChange<N>;
|
|
212
|
+
onEdgesChange: import("@xyflow/react").OnEdgesChange<E>;
|
|
213
|
+
onConnect: (c: Connection) => void;
|
|
214
|
+
};
|
|
215
|
+
//#endregion
|
|
216
|
+
export type { FlowCanvasProps, FlowHandle, FlowNode, FlowNodeData, FlowNodeDecoration, FlowNodeKind, FlowNodeRenderProps, FlowNodeRenderer, FlowTheme, FlowTone, FlowToneStyle, NodeCardProps };
|
|
217
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Background, BackgroundVariant, Controls, Handle, MarkerType, MiniMap, Position, ReactFlow, addEdge, useEdgesState, useNodesState } from "@xyflow/react";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { currentTheme, observeTheme, tokenColor } from "@gntik-ai/tokens/runtime";
|
|
6
|
+
//#region src/tones.ts
|
|
7
|
+
const FLOW_TONES = {
|
|
8
|
+
running: {
|
|
9
|
+
label: "Running",
|
|
10
|
+
pill: "bg-primary/14 text-primary-text",
|
|
11
|
+
dot: "bg-primary",
|
|
12
|
+
border: "border-primary/60",
|
|
13
|
+
token: "--primary",
|
|
14
|
+
march: true
|
|
15
|
+
},
|
|
16
|
+
degraded: {
|
|
17
|
+
label: "Degraded",
|
|
18
|
+
pill: "bg-warning/16 text-warning-text",
|
|
19
|
+
dot: "bg-warning",
|
|
20
|
+
border: "border-warning/65",
|
|
21
|
+
token: "--warning"
|
|
22
|
+
},
|
|
23
|
+
failed: {
|
|
24
|
+
label: "Failed",
|
|
25
|
+
pill: "bg-destructive/15 text-destructive-text",
|
|
26
|
+
dot: "bg-destructive",
|
|
27
|
+
border: "border-destructive/65",
|
|
28
|
+
token: "--destructive"
|
|
29
|
+
},
|
|
30
|
+
paused: {
|
|
31
|
+
label: "Paused",
|
|
32
|
+
pill: "bg-muted-foreground/16 text-muted-foreground",
|
|
33
|
+
dot: "bg-muted-foreground",
|
|
34
|
+
border: null,
|
|
35
|
+
token: "--muted-foreground"
|
|
36
|
+
},
|
|
37
|
+
done: {
|
|
38
|
+
label: "Done",
|
|
39
|
+
pill: "bg-primary/14 text-primary-text",
|
|
40
|
+
dot: "bg-primary",
|
|
41
|
+
border: "border-primary/70",
|
|
42
|
+
token: "--primary"
|
|
43
|
+
},
|
|
44
|
+
handoff: {
|
|
45
|
+
label: "Handoff",
|
|
46
|
+
pill: "bg-category-violet/16 text-foreground",
|
|
47
|
+
dot: "bg-category-violet",
|
|
48
|
+
border: "border-category-violet",
|
|
49
|
+
token: "--category-violet"
|
|
50
|
+
},
|
|
51
|
+
review: {
|
|
52
|
+
label: "Review",
|
|
53
|
+
pill: "bg-category-rose/16 text-foreground",
|
|
54
|
+
dot: "bg-category-rose",
|
|
55
|
+
border: "border-category-rose",
|
|
56
|
+
token: "--category-rose"
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
/** Icon chip per node kind. */
|
|
60
|
+
const KIND_CHIP = {
|
|
61
|
+
trigger: "bg-primary text-primary-foreground",
|
|
62
|
+
step: "bg-primary/14 text-primary-text",
|
|
63
|
+
router: "border border-border bg-secondary text-foreground",
|
|
64
|
+
output: "bg-accent text-accent-foreground"
|
|
65
|
+
};
|
|
66
|
+
function isFlowTone(v) {
|
|
67
|
+
return typeof v === "string" && v in FLOW_TONES;
|
|
68
|
+
}
|
|
69
|
+
/** Token for a node's minimap swatch: its tone, else muted for routers, else primary. */
|
|
70
|
+
function nodeToken(tone, kind) {
|
|
71
|
+
if (isFlowTone(tone)) return FLOW_TONES[tone].token;
|
|
72
|
+
return kind === "router" ? "--muted-foreground" : "--primary";
|
|
73
|
+
}
|
|
74
|
+
function cx(...parts) {
|
|
75
|
+
return parts.filter(Boolean).join(" ");
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/decorations.tsx
|
|
79
|
+
/** Memoized per node: unrelated status changes do not render this badge again. */
|
|
80
|
+
const FlowStatusBadge = memo(function FlowStatusBadge({ status }) {
|
|
81
|
+
const tone = FLOW_TONES[status];
|
|
82
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
83
|
+
className: cx("inline-flex h-[18px] shrink-0 items-center gap-1 rounded px-1.5 font-mono text-[9.5px] font-semibold", tone.pill),
|
|
84
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
85
|
+
"aria-hidden": true,
|
|
86
|
+
className: cx("h-1.5 w-1.5 rounded-full", tone.dot, tone.march && "animate-pulse motion-reduce:animate-none")
|
|
87
|
+
}), tone.label]
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
function FlowNodeIssues({ issues }) {
|
|
91
|
+
if (!issues?.length) return null;
|
|
92
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
93
|
+
className: "absolute top-full left-0 flex w-full items-start gap-1 rounded border border-destructive/65 bg-card p-1 text-[11px] text-destructive-text",
|
|
94
|
+
children: [/* @__PURE__ */ jsx("svg", {
|
|
95
|
+
"aria-hidden": "true",
|
|
96
|
+
width: "14",
|
|
97
|
+
height: "14",
|
|
98
|
+
viewBox: "0 0 24 24",
|
|
99
|
+
className: "shrink-0 fill-none stroke-current",
|
|
100
|
+
strokeWidth: "2",
|
|
101
|
+
children: /* @__PURE__ */ jsx("path", { d: "m12 3 10 18H2Z M12 9v5 M12 17v1" })
|
|
102
|
+
}), /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("span", {
|
|
103
|
+
className: "sr-only",
|
|
104
|
+
children: "Validation issue: "
|
|
105
|
+
}), issues.map((issue) => issue.message).join("; ")] })]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/glyphs.tsx
|
|
110
|
+
const PATHS = {
|
|
111
|
+
trigger: ["M13 2 3 14h9l-1 8 10-12h-9l1-8z"],
|
|
112
|
+
step: ["M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z", "M9 9h6v6H9z"],
|
|
113
|
+
router: [
|
|
114
|
+
"M16 3h5v5",
|
|
115
|
+
"M8 3H3v5",
|
|
116
|
+
"M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3",
|
|
117
|
+
"m15 9 6-6"
|
|
118
|
+
],
|
|
119
|
+
output: ["M20 6 9 17l-5-5"]
|
|
120
|
+
};
|
|
121
|
+
/** Default decorative icon for a node kind (stroke = currentColor). */
|
|
122
|
+
function KindGlyph({ kind, size = 16 }) {
|
|
123
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
124
|
+
"aria-hidden": "true",
|
|
125
|
+
focusable: "false",
|
|
126
|
+
width: size,
|
|
127
|
+
height: size,
|
|
128
|
+
viewBox: "0 0 24 24",
|
|
129
|
+
fill: "none",
|
|
130
|
+
stroke: "currentColor",
|
|
131
|
+
strokeWidth: 2,
|
|
132
|
+
strokeLinecap: "round",
|
|
133
|
+
strokeLinejoin: "round",
|
|
134
|
+
children: PATHS[kind].map((d) => /* @__PURE__ */ jsx("path", { d }, d))
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/NodeCard.tsx
|
|
139
|
+
/** The brand node card, without handles (reused by the node types and static galleries). */
|
|
140
|
+
function NodeCard({ kind = "step", icon, title, subtitle, meta, tone, selected, className }) {
|
|
141
|
+
const t = tone ? FLOW_TONES[tone] : void 0;
|
|
142
|
+
const running = Boolean(t?.march);
|
|
143
|
+
const border = selected ? "border-2 border-primary/60 shadow-md ring-2 ring-primary/35" : t?.border ? cx("border-2 shadow-sm", t.border) : "border border-border shadow-sm";
|
|
144
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
145
|
+
className: cx("relative h-full w-full min-w-52", className),
|
|
146
|
+
"data-tone": tone,
|
|
147
|
+
children: [running && /* @__PURE__ */ jsx("svg", {
|
|
148
|
+
"aria-hidden": "true",
|
|
149
|
+
width: "100%",
|
|
150
|
+
height: "100%",
|
|
151
|
+
className: "gu-flow-run-frame pointer-events-none absolute inset-0 z-10",
|
|
152
|
+
children: /* @__PURE__ */ jsx("rect", {
|
|
153
|
+
className: "gu-flow-run fill-none stroke-primary",
|
|
154
|
+
strokeWidth: 2,
|
|
155
|
+
strokeLinecap: "round",
|
|
156
|
+
pathLength: 100,
|
|
157
|
+
strokeDasharray: "22 78"
|
|
158
|
+
})
|
|
159
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
160
|
+
className: cx("flex h-full w-full flex-col overflow-hidden rounded-lg bg-card transition-shadow", border),
|
|
161
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
162
|
+
className: "flex flex-1 items-center gap-2.5 px-3 py-2.5",
|
|
163
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
164
|
+
"aria-hidden": true,
|
|
165
|
+
className: cx("grid h-8 w-8 shrink-0 place-items-center rounded-lg", KIND_CHIP[kind]),
|
|
166
|
+
children: icon ?? /* @__PURE__ */ jsx(KindGlyph, { kind })
|
|
167
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
168
|
+
className: "min-w-0 flex-1",
|
|
169
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
170
|
+
className: "truncate font-sans text-[12.5px] leading-tight font-semibold text-foreground",
|
|
171
|
+
children: title
|
|
172
|
+
}), subtitle && /* @__PURE__ */ jsx("div", {
|
|
173
|
+
className: "mt-0.5 truncate font-mono text-[10px] text-muted-foreground",
|
|
174
|
+
children: subtitle
|
|
175
|
+
})]
|
|
176
|
+
})]
|
|
177
|
+
}), (meta || t) && /* @__PURE__ */ jsxs("div", {
|
|
178
|
+
className: "flex items-center justify-between gap-2 border-t border-border/70 bg-secondary/25 px-3 py-2",
|
|
179
|
+
children: [meta ? /* @__PURE__ */ jsx("span", {
|
|
180
|
+
className: "truncate font-mono text-[10px] text-muted-foreground",
|
|
181
|
+
children: meta
|
|
182
|
+
}) : /* @__PURE__ */ jsx("span", {}), t && /* @__PURE__ */ jsx(FlowStatusBadge, { status: tone })]
|
|
183
|
+
})]
|
|
184
|
+
})]
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/nodes.tsx
|
|
189
|
+
const DEFAULT_HANDLES = {
|
|
190
|
+
trigger: {
|
|
191
|
+
target: false,
|
|
192
|
+
source: true
|
|
193
|
+
},
|
|
194
|
+
step: {
|
|
195
|
+
target: true,
|
|
196
|
+
source: true
|
|
197
|
+
},
|
|
198
|
+
router: {
|
|
199
|
+
target: true,
|
|
200
|
+
source: true
|
|
201
|
+
},
|
|
202
|
+
output: {
|
|
203
|
+
target: true,
|
|
204
|
+
source: false
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
function makeNode(kind) {
|
|
208
|
+
function BrandNode({ data, selected, sourcePosition, targetPosition, isConnectable }) {
|
|
209
|
+
const handles = {
|
|
210
|
+
...DEFAULT_HANDLES[kind],
|
|
211
|
+
...data.handles
|
|
212
|
+
};
|
|
213
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
214
|
+
className: "relative h-full w-full",
|
|
215
|
+
children: [
|
|
216
|
+
handles.target && /* @__PURE__ */ jsx(Handle, {
|
|
217
|
+
type: "target",
|
|
218
|
+
position: targetPosition ?? Position.Left,
|
|
219
|
+
isConnectable
|
|
220
|
+
}),
|
|
221
|
+
/* @__PURE__ */ jsx(NodeCard, {
|
|
222
|
+
kind,
|
|
223
|
+
title: data.title,
|
|
224
|
+
subtitle: data.subtitle,
|
|
225
|
+
meta: data.meta,
|
|
226
|
+
icon: data.icon,
|
|
227
|
+
tone: data.decoration?.status ?? data.tone,
|
|
228
|
+
selected
|
|
229
|
+
}),
|
|
230
|
+
/* @__PURE__ */ jsx(FlowNodeIssues, { issues: data.decoration?.issues }),
|
|
231
|
+
handles.source && /* @__PURE__ */ jsx(Handle, {
|
|
232
|
+
type: "source",
|
|
233
|
+
position: sourcePosition ?? Position.Right,
|
|
234
|
+
isConnectable
|
|
235
|
+
})
|
|
236
|
+
]
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
BrandNode.displayName = `${kind[0]?.toUpperCase()}${kind.slice(1)}Node`;
|
|
240
|
+
return BrandNode;
|
|
241
|
+
}
|
|
242
|
+
/** Entry point: source handle only. */
|
|
243
|
+
const TriggerNode = makeNode("trigger");
|
|
244
|
+
/** Processing step: target + source handles, optional meta and status. */
|
|
245
|
+
const StepNode = makeNode("step");
|
|
246
|
+
/** Conditional branch: target + source handles. */
|
|
247
|
+
const RouterNode = makeNode("router");
|
|
248
|
+
/** Terminal node: target handle only. */
|
|
249
|
+
const OutputNode = makeNode("output");
|
|
250
|
+
/** `nodeTypes` map for `<FlowCanvas>` / `<ReactFlow>` (define it once, outside render). */
|
|
251
|
+
const flowNodeTypes = {
|
|
252
|
+
trigger: TriggerNode,
|
|
253
|
+
step: StepNode,
|
|
254
|
+
router: RouterNode,
|
|
255
|
+
output: OutputNode
|
|
256
|
+
};
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/theme.ts
|
|
259
|
+
function readFlowTheme(theme) {
|
|
260
|
+
return {
|
|
261
|
+
theme,
|
|
262
|
+
edge: tokenColor("--muted-foreground"),
|
|
263
|
+
edgeActive: tokenColor("--primary"),
|
|
264
|
+
pattern: tokenColor("--border"),
|
|
265
|
+
color: (token, alpha) => tokenColor(token, alpha)
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Brand colours for the parts React Flow paints from props (arrow markers, minimap nodes,
|
|
270
|
+
* background pattern), resolved through `@gntik-ai/tokens/runtime` so they also survive
|
|
271
|
+
* SVG/PNG export. Re-read on every theme class change.
|
|
272
|
+
*/
|
|
273
|
+
function useFlowTheme() {
|
|
274
|
+
const [snapshot, setSnapshot] = useState(() => readFlowTheme(currentTheme()));
|
|
275
|
+
useEffect(() => observeTheme((next) => setSnapshot(readFlowTheme(next))), []);
|
|
276
|
+
return snapshot;
|
|
277
|
+
}
|
|
278
|
+
/** Closed arrow marker in a given colour. */
|
|
279
|
+
function flowMarker(color) {
|
|
280
|
+
return {
|
|
281
|
+
type: MarkerType.ArrowClosed,
|
|
282
|
+
width: 15,
|
|
283
|
+
height: 15,
|
|
284
|
+
color
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** Brand edge defaults: orthogonal (smoothstep) routing. */
|
|
288
|
+
const brandEdgeOptions = { type: "smoothstep" };
|
|
289
|
+
/** Adds an arrow to edges that have none: primary when animated (active), muted otherwise. */
|
|
290
|
+
function withBrandMarkers(edges, t) {
|
|
291
|
+
return edges.map((e) => e.markerEnd ? e : {
|
|
292
|
+
...e,
|
|
293
|
+
markerEnd: flowMarker(e.animated ? t.edgeActive : t.edge)
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/renderers.tsx
|
|
298
|
+
const POSITIONS = {
|
|
299
|
+
left: Position.Left,
|
|
300
|
+
right: Position.Right,
|
|
301
|
+
top: Position.Top,
|
|
302
|
+
bottom: Position.Bottom
|
|
303
|
+
};
|
|
304
|
+
/** Internal adapter; only the kit contracts above are exported from the package entry. */
|
|
305
|
+
function adaptNodeRenderers(renderers) {
|
|
306
|
+
const types = { ...flowNodeTypes };
|
|
307
|
+
for (const definition of renderers) {
|
|
308
|
+
const counts = {
|
|
309
|
+
left: 0,
|
|
310
|
+
right: 0,
|
|
311
|
+
top: 0,
|
|
312
|
+
bottom: 0
|
|
313
|
+
};
|
|
314
|
+
const handles = definition.handles.map((handle) => {
|
|
315
|
+
const position = handle.position ?? (handle.kind === "target" ? "left" : "right");
|
|
316
|
+
return {
|
|
317
|
+
...handle,
|
|
318
|
+
position,
|
|
319
|
+
index: ++counts[position]
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
const verticalCount = Math.max(counts.left, counts.right);
|
|
323
|
+
const horizontalCount = Math.max(counts.top, counts.bottom);
|
|
324
|
+
types[definition.type] = memo(function CustomNode({ id, data, selected, isConnectable }) {
|
|
325
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
326
|
+
className: "relative grid h-full w-full",
|
|
327
|
+
style: {
|
|
328
|
+
minHeight: verticalCount ? (verticalCount + 1) * 32 : void 0,
|
|
329
|
+
minWidth: horizontalCount ? (horizontalCount + 1) * 32 : void 0
|
|
330
|
+
},
|
|
331
|
+
children: [
|
|
332
|
+
handles.map((handle) => /* @__PURE__ */ jsx(Handle, {
|
|
333
|
+
className: "gu-flow-custom-handle",
|
|
334
|
+
id: handle.id,
|
|
335
|
+
type: handle.kind,
|
|
336
|
+
position: POSITIONS[handle.position],
|
|
337
|
+
isConnectable,
|
|
338
|
+
"aria-label": handle.label ?? handle.id,
|
|
339
|
+
role: "button",
|
|
340
|
+
tabIndex: isConnectable ? 0 : -1,
|
|
341
|
+
"aria-disabled": !isConnectable,
|
|
342
|
+
onKeyDown: (event) => {
|
|
343
|
+
if (!isConnectable || event.key !== "Enter" && event.key !== " ") return;
|
|
344
|
+
event.preventDefault();
|
|
345
|
+
event.stopPropagation();
|
|
346
|
+
event.currentTarget.click();
|
|
347
|
+
},
|
|
348
|
+
style: { [handle.position === "top" || handle.position === "bottom" ? "left" : "top"]: `${handle.index / (counts[handle.position] + 1) * 100}%` }
|
|
349
|
+
}, `${handle.kind}-${handle.id}`)),
|
|
350
|
+
definition.decorations !== "consumer" && /* @__PURE__ */ jsxs(Fragment, { children: [data.decoration?.status && /* @__PURE__ */ jsx("div", {
|
|
351
|
+
className: "pointer-events-none absolute -top-2 right-1 z-10",
|
|
352
|
+
children: /* @__PURE__ */ jsx(FlowStatusBadge, { status: data.decoration.status })
|
|
353
|
+
}), /* @__PURE__ */ jsx(FlowNodeIssues, { issues: data.decoration?.issues })] }),
|
|
354
|
+
definition.render({
|
|
355
|
+
id,
|
|
356
|
+
type: definition.type,
|
|
357
|
+
data,
|
|
358
|
+
selected,
|
|
359
|
+
connectable: isConnectable
|
|
360
|
+
})
|
|
361
|
+
]
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return types;
|
|
366
|
+
}
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/FlowCanvas.tsx
|
|
369
|
+
const STATIC_PROPS = {
|
|
370
|
+
nodesDraggable: false,
|
|
371
|
+
nodesConnectable: false,
|
|
372
|
+
elementsSelectable: false,
|
|
373
|
+
panOnDrag: false,
|
|
374
|
+
panOnScroll: false,
|
|
375
|
+
zoomOnScroll: false,
|
|
376
|
+
zoomOnPinch: false,
|
|
377
|
+
zoomOnDoubleClick: false,
|
|
378
|
+
preventScrolling: false
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* React Flow canvas themed from the brand tokens: dotted background, controls and minimap,
|
|
382
|
+
* brand node types, smoothstep edges with arrows (primary when animated). Drag, connect and
|
|
383
|
+
* zoom/pan are on by default. Import `@gntik-ai/flow/styles.css` once.
|
|
384
|
+
*/
|
|
385
|
+
function FlowCanvas({ "aria-label": label, height = 480, showControls = true, showInteractiveControls = true, showMiniMap = true, showBackground = true, interactive = true, className, canvasClassName, nodeTypes, nodeRenderers, edges, defaultEdgeOptions, proOptions, children, ...rest }) {
|
|
386
|
+
const t = useFlowTheme();
|
|
387
|
+
const customTypes = useMemo(() => nodeRenderers ? adaptNodeRenderers(nodeRenderers) : void 0, [nodeRenderers]);
|
|
388
|
+
const brandedEdges = useMemo(() => edges ? withBrandMarkers(edges, t) : void 0, [edges, t]);
|
|
389
|
+
const edgeOptions = useMemo(() => ({
|
|
390
|
+
...brandEdgeOptions,
|
|
391
|
+
markerEnd: flowMarker(t.edge),
|
|
392
|
+
...defaultEdgeOptions
|
|
393
|
+
}), [t, defaultEdgeOptions]);
|
|
394
|
+
const miniColor = useCallback((n) => t.color(nodeToken(n.data.tone, n.type)), [t]);
|
|
395
|
+
return /* @__PURE__ */ jsx("div", {
|
|
396
|
+
role: "region",
|
|
397
|
+
"aria-label": label,
|
|
398
|
+
"data-theme": t.theme,
|
|
399
|
+
className: cx("gu-flow relative w-full bg-background", className),
|
|
400
|
+
style: { height },
|
|
401
|
+
children: /* @__PURE__ */ jsxs(ReactFlow, {
|
|
402
|
+
nodeTypes: customTypes ?? nodeTypes ?? flowNodeTypes,
|
|
403
|
+
edges: brandedEdges,
|
|
404
|
+
defaultEdgeOptions: edgeOptions,
|
|
405
|
+
fitView: true,
|
|
406
|
+
fitViewOptions: {
|
|
407
|
+
padding: .12,
|
|
408
|
+
maxZoom: 1
|
|
409
|
+
},
|
|
410
|
+
minZoom: .3,
|
|
411
|
+
maxZoom: 1.75,
|
|
412
|
+
snapToGrid: true,
|
|
413
|
+
snapGrid: [16, 16],
|
|
414
|
+
proOptions: proOptions ?? (interactive ? void 0 : { hideAttribution: true }),
|
|
415
|
+
...interactive ? void 0 : STATIC_PROPS,
|
|
416
|
+
...rest,
|
|
417
|
+
className: canvasClassName,
|
|
418
|
+
children: [
|
|
419
|
+
showBackground && /* @__PURE__ */ jsx(Background, {
|
|
420
|
+
variant: BackgroundVariant.Dots,
|
|
421
|
+
gap: 20,
|
|
422
|
+
size: 1,
|
|
423
|
+
color: t.pattern
|
|
424
|
+
}),
|
|
425
|
+
interactive && showControls && /* @__PURE__ */ jsx(Controls, { showInteractive: showInteractiveControls }),
|
|
426
|
+
interactive && showMiniMap && /* @__PURE__ */ jsx(MiniMap, {
|
|
427
|
+
pannable: true,
|
|
428
|
+
zoomable: true,
|
|
429
|
+
nodeColor: miniColor,
|
|
430
|
+
nodeStrokeWidth: 0,
|
|
431
|
+
nodeBorderRadius: 3
|
|
432
|
+
}),
|
|
433
|
+
children
|
|
434
|
+
]
|
|
435
|
+
})
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/useFlowState.ts
|
|
440
|
+
/**
|
|
441
|
+
* Local nodes/edges state wired for drag, select and connect. New connections become
|
|
442
|
+
* active (animated) smoothstep edges. Spread the result into `<FlowCanvas>`.
|
|
443
|
+
*/
|
|
444
|
+
function useFlowState(initialNodes, initialEdges) {
|
|
445
|
+
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
446
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
447
|
+
return {
|
|
448
|
+
nodes,
|
|
449
|
+
edges,
|
|
450
|
+
setNodes,
|
|
451
|
+
setEdges,
|
|
452
|
+
onNodesChange,
|
|
453
|
+
onEdgesChange,
|
|
454
|
+
onConnect: useCallback((c) => {
|
|
455
|
+
const active = Object.assign({
|
|
456
|
+
type: "smoothstep",
|
|
457
|
+
animated: true
|
|
458
|
+
}, c);
|
|
459
|
+
setEdges((eds) => addEdge(active, eds));
|
|
460
|
+
}, [setEdges])
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
//#endregion
|
|
464
|
+
export { FLOW_TONES, FlowCanvas, FlowStatusBadge, KIND_CHIP, KindGlyph, NodeCard, OutputNode, RouterNode, StepNode, TriggerNode, brandEdgeOptions, flowMarker, flowNodeTypes, isFlowTone, nodeToken, useFlowState, useFlowTheme, withBrandMarkers };
|
|
465
|
+
|
|
466
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/tones.ts","../src/decorations.tsx","../src/glyphs.tsx","../src/NodeCard.tsx","../src/nodes.tsx","../src/theme.ts","../src/renderers.tsx","../src/FlowCanvas.tsx","../src/useFlowState.ts"],"sourcesContent":["/** Node status → brand colour. Green = active/healthy (never severity); amber = degraded;\n * red = failed; violet/rose = needs a human (handoff / review). */\nexport type FlowTone = 'running' | 'degraded' | 'failed' | 'paused' | 'done' | 'handoff' | 'review';\n\n/** The role of a node, which decides its handles and its icon chip. */\nexport type FlowNodeKind = 'trigger' | 'step' | 'router' | 'output';\n\nexport interface FlowToneStyle {\n label: string;\n /** Pill background + text (contrast-safe text aliases). */\n pill: string;\n /** Status dot. */\n dot: string;\n /** Thick card border, or null for the neutral border. */\n border: string | null;\n /** Brand token used for the minimap swatch. */\n token: string;\n /** Draws the marching segment around the card. */\n march?: boolean;\n}\n\nexport const FLOW_TONES: Record<FlowTone, FlowToneStyle> = {\n running: {\n label: 'Running',\n pill: 'bg-primary/14 text-primary-text',\n dot: 'bg-primary',\n border: 'border-primary/60',\n token: '--primary',\n march: true,\n },\n degraded: {\n label: 'Degraded',\n pill: 'bg-warning/16 text-warning-text',\n dot: 'bg-warning',\n border: 'border-warning/65',\n token: '--warning',\n },\n failed: {\n label: 'Failed',\n pill: 'bg-destructive/15 text-destructive-text',\n dot: 'bg-destructive',\n border: 'border-destructive/65',\n token: '--destructive',\n },\n paused: {\n label: 'Paused',\n pill: 'bg-muted-foreground/16 text-muted-foreground',\n dot: 'bg-muted-foreground',\n border: null,\n token: '--muted-foreground',\n },\n done: {\n label: 'Done',\n pill: 'bg-primary/14 text-primary-text',\n dot: 'bg-primary',\n border: 'border-primary/70',\n token: '--primary',\n },\n handoff: {\n label: 'Handoff',\n pill: 'bg-category-violet/16 text-foreground',\n dot: 'bg-category-violet',\n border: 'border-category-violet',\n token: '--category-violet',\n },\n review: {\n label: 'Review',\n pill: 'bg-category-rose/16 text-foreground',\n dot: 'bg-category-rose',\n border: 'border-category-rose',\n token: '--category-rose',\n },\n};\n\n/** Icon chip per node kind. */\nexport const KIND_CHIP: Record<FlowNodeKind, string> = {\n trigger: 'bg-primary text-primary-foreground',\n step: 'bg-primary/14 text-primary-text',\n router: 'border border-border bg-secondary text-foreground',\n output: 'bg-accent text-accent-foreground',\n};\n\nexport function isFlowTone(v: unknown): v is FlowTone {\n return typeof v === 'string' && v in FLOW_TONES;\n}\n\n/** Token for a node's minimap swatch: its tone, else muted for routers, else primary. */\nexport function nodeToken(tone: unknown, kind: unknown): string {\n if (isFlowTone(tone)) return FLOW_TONES[tone].token;\n return kind === 'router' ? '--muted-foreground' : '--primary';\n}\n\nexport function cx(...parts: Array<string | false | null | undefined>): string {\n return parts.filter(Boolean).join(' ');\n}\n","import { memo } from 'react';\nimport { cx, FLOW_TONES, type FlowTone } from './tones';\n\nexport interface FlowNodeDecoration {\n status?: FlowTone;\n issues?: readonly { code: string; message: string }[];\n}\n\n/** Memoized per node: unrelated status changes do not render this badge again. */\nexport const FlowStatusBadge = memo(function FlowStatusBadge({ status }: { status: FlowTone }) {\n const tone = FLOW_TONES[status];\n return (\n <span\n className={cx(\n 'inline-flex h-[18px] shrink-0 items-center gap-1 rounded px-1.5 font-mono text-[9.5px] font-semibold',\n tone.pill,\n )}\n >\n <span\n aria-hidden\n className={cx('h-1.5 w-1.5 rounded-full', tone.dot, tone.march && 'animate-pulse motion-reduce:animate-none')}\n />\n {tone.label}\n </span>\n );\n});\n\nexport function FlowNodeIssues({ issues }: { issues: FlowNodeDecoration['issues'] }) {\n if (!issues?.length) return null;\n return (\n <div className=\"absolute top-full left-0 flex w-full items-start gap-1 rounded border border-destructive/65 bg-card p-1 text-[11px] text-destructive-text\">\n <svg\n aria-hidden=\"true\"\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n className=\"shrink-0 fill-none stroke-current\"\n strokeWidth=\"2\"\n >\n <path d=\"m12 3 10 18H2Z M12 9v5 M12 17v1\" />\n </svg>\n <span>\n <span className=\"sr-only\">Validation issue: </span>\n {issues.map((issue) => issue.message).join('; ')}\n </span>\n </div>\n );\n}\n","import type { FlowNodeKind } from './tones';\n\nconst PATHS: Record<FlowNodeKind, string[]> = {\n trigger: ['M13 2 3 14h9l-1 8 10-12h-9l1-8z'],\n step: ['M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z', 'M9 9h6v6H9z'],\n router: ['M16 3h5v5', 'M8 3H3v5', 'M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3', 'm15 9 6-6'],\n output: ['M20 6 9 17l-5-5'],\n};\n\n/** Default decorative icon for a node kind (stroke = currentColor). */\nexport function KindGlyph({ kind, size = 16 }: { kind: FlowNodeKind; size?: number }) {\n return (\n <svg\n aria-hidden=\"true\"\n focusable=\"false\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n {PATHS[kind].map((d) => (\n <path key={d} d={d} />\n ))}\n </svg>\n );\n}\n","import type { ReactNode } from 'react';\nimport { FlowStatusBadge } from './decorations';\nimport { KindGlyph } from './glyphs';\nimport { cx, FLOW_TONES, KIND_CHIP, type FlowNodeKind, type FlowTone } from './tones';\n\nexport interface NodeCardProps {\n kind?: FlowNodeKind;\n title: string;\n subtitle?: string;\n /** Footer text (left), e.g. a version or an owner. */\n meta?: string;\n /** Icon element (decorative); defaults to the kind's glyph. */\n icon?: ReactNode;\n tone?: FlowTone;\n selected?: boolean;\n className?: string;\n}\n\n/** The brand node card, without handles (reused by the node types and static galleries). */\nexport function NodeCard({ kind = 'step', icon, title, subtitle, meta, tone, selected, className }: NodeCardProps) {\n const t = tone ? FLOW_TONES[tone] : undefined;\n const running = Boolean(t?.march);\n const border = selected\n ? 'border-2 border-primary/60 shadow-md ring-2 ring-primary/35'\n : t?.border\n ? cx('border-2 shadow-sm', t.border)\n : 'border border-border shadow-sm';\n return (\n <div className={cx('relative h-full w-full min-w-52', className)} data-tone={tone}>\n {running && (\n <svg\n aria-hidden=\"true\"\n width=\"100%\"\n height=\"100%\"\n className=\"gu-flow-run-frame pointer-events-none absolute inset-0 z-10\"\n >\n <rect\n className=\"gu-flow-run fill-none stroke-primary\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n pathLength={100}\n strokeDasharray=\"22 78\"\n />\n </svg>\n )}\n <div className={cx('flex h-full w-full flex-col overflow-hidden rounded-lg bg-card transition-shadow', border)}>\n <div className=\"flex flex-1 items-center gap-2.5 px-3 py-2.5\">\n <span aria-hidden className={cx('grid h-8 w-8 shrink-0 place-items-center rounded-lg', KIND_CHIP[kind])}>\n {icon ?? <KindGlyph kind={kind} />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate font-sans text-[12.5px] leading-tight font-semibold text-foreground\">{title}</div>\n {subtitle && <div className=\"mt-0.5 truncate font-mono text-[10px] text-muted-foreground\">{subtitle}</div>}\n </div>\n </div>\n {(meta || t) && (\n <div className=\"flex items-center justify-between gap-2 border-t border-border/70 bg-secondary/25 px-3 py-2\">\n {meta ? <span className=\"truncate font-mono text-[10px] text-muted-foreground\">{meta}</span> : <span />}\n {t && <FlowStatusBadge status={tone!} />}\n </div>\n )}\n </div>\n </div>\n );\n}\n","import type { ReactNode } from 'react';\nimport { Handle, Position, type Node, type NodeProps, type NodeTypes } from '@xyflow/react';\nimport { FlowNodeIssues, type FlowNodeDecoration } from './decorations';\nimport { NodeCard } from './NodeCard';\nimport type { FlowNodeKind, FlowTone } from './tones';\n\n/** Generic node content. No domain data: title, subtitle, meta, icon and a status tone. */\nexport type FlowNodeData = {\n decoration?: FlowNodeDecoration;\n title: string;\n subtitle?: string;\n meta?: string;\n /** Decorative icon element (e.g. a lucide icon); defaults to the kind's glyph. */\n icon?: ReactNode;\n tone?: FlowTone;\n /** Override the kind's default handles. */\n handles?: { target?: boolean; source?: boolean };\n};\n\n/** Built-in kinds by default; consumers can opt into custom type identifiers. */\nexport type FlowNode<Kind extends string = FlowNodeKind> = Omit<Node<FlowNodeData, FlowNodeKind>, 'type'> & {\n type: Kind;\n};\n\nconst DEFAULT_HANDLES: Record<FlowNodeKind, { target: boolean; source: boolean }> = {\n trigger: { target: false, source: true },\n step: { target: true, source: true },\n router: { target: true, source: true },\n output: { target: true, source: false },\n};\n\nfunction makeNode(kind: FlowNodeKind) {\n function BrandNode({ data, selected, sourcePosition, targetPosition, isConnectable }: NodeProps<FlowNode>) {\n const handles = { ...DEFAULT_HANDLES[kind], ...data.handles };\n return (\n <div className=\"relative h-full w-full\">\n {handles.target && (\n <Handle type=\"target\" position={targetPosition ?? Position.Left} isConnectable={isConnectable} />\n )}\n <NodeCard\n kind={kind}\n title={data.title}\n subtitle={data.subtitle}\n meta={data.meta}\n icon={data.icon}\n tone={data.decoration?.status ?? data.tone}\n selected={selected}\n />\n <FlowNodeIssues issues={data.decoration?.issues} />\n {handles.source && (\n <Handle type=\"source\" position={sourcePosition ?? Position.Right} isConnectable={isConnectable} />\n )}\n </div>\n );\n }\n BrandNode.displayName = `${kind[0]?.toUpperCase()}${kind.slice(1)}Node`;\n return BrandNode;\n}\n\n/** Entry point: source handle only. */\nexport const TriggerNode = makeNode('trigger');\n/** Processing step: target + source handles, optional meta and status. */\nexport const StepNode = makeNode('step');\n/** Conditional branch: target + source handles. */\nexport const RouterNode = makeNode('router');\n/** Terminal node: target handle only. */\nexport const OutputNode = makeNode('output');\n\n/** `nodeTypes` map for `<FlowCanvas>` / `<ReactFlow>` (define it once, outside render). */\nexport const flowNodeTypes = {\n trigger: TriggerNode,\n step: StepNode,\n router: RouterNode,\n output: OutputNode,\n} satisfies NodeTypes;\n","import { useEffect, useState } from 'react';\nimport { MarkerType, type DefaultEdgeOptions, type Edge, type EdgeMarker } from '@xyflow/react';\nimport { currentTheme, observeTheme, tokenColor, type Theme } from '@gntik-ai/tokens/runtime';\n\nexport interface FlowTheme {\n theme: Theme;\n /** Idle edge / arrow colour. */\n edge: string;\n /** Active (animated) edge / arrow colour. */\n edgeActive: string;\n /** Background dot pattern. */\n pattern: string;\n /** Resolves any brand token (e.g. \"--warning\") to a concrete colour. */\n color: (token: string, alpha?: number) => string;\n}\n\nfunction readFlowTheme(theme: Theme): FlowTheme {\n return {\n theme,\n edge: tokenColor('--muted-foreground'),\n edgeActive: tokenColor('--primary'),\n pattern: tokenColor('--border'),\n color: (token, alpha) => tokenColor(token, alpha),\n };\n}\n\n/**\n * Brand colours for the parts React Flow paints from props (arrow markers, minimap nodes,\n * background pattern), resolved through `@gntik-ai/tokens/runtime` so they also survive\n * SVG/PNG export. Re-read on every theme class change.\n */\nexport function useFlowTheme(): FlowTheme {\n const [snapshot, setSnapshot] = useState<FlowTheme>(() => readFlowTheme(currentTheme()));\n useEffect(() => observeTheme((next) => setSnapshot(readFlowTheme(next))), []);\n return snapshot;\n}\n\n/** Closed arrow marker in a given colour. */\nexport function flowMarker(color: string): EdgeMarker {\n return { type: MarkerType.ArrowClosed, width: 15, height: 15, color };\n}\n\n/** Brand edge defaults: orthogonal (smoothstep) routing. */\nexport const brandEdgeOptions: DefaultEdgeOptions = { type: 'smoothstep' };\n\n/** Adds an arrow to edges that have none: primary when animated (active), muted otherwise. */\nexport function withBrandMarkers<E extends Edge>(edges: readonly E[], t: FlowTheme): E[] {\n return edges.map((e) => (e.markerEnd ? e : { ...e, markerEnd: flowMarker(e.animated ? t.edgeActive : t.edge) }));\n}\n","import { memo, type ReactNode } from 'react';\nimport { Handle, Position, type NodeProps, type NodeTypes } from '@xyflow/react';\nimport { FlowNodeIssues, FlowStatusBadge } from './decorations';\nimport { flowNodeTypes, type FlowNode, type FlowNodeData } from './nodes';\n\n/** Named connection point. Positions default to left for targets and right for sources. */\nexport interface FlowHandle {\n id: string;\n kind: 'source' | 'target';\n position?: 'left' | 'right' | 'top' | 'bottom';\n label?: string;\n}\n\n/** Kit-owned renderer input: no React Flow component types are required by consumers. */\nexport interface FlowNodeRenderProps {\n id: string;\n type: string;\n data: FlowNodeData;\n selected: boolean;\n connectable: boolean;\n}\n\nexport interface FlowNodeRenderer {\n type: string;\n handles: readonly FlowHandle[];\n /** Defaults to kit overlays. Consumer renderers may own accessible status and issue decorations. */\n decorations?: 'kit' | 'consumer';\n render: (props: FlowNodeRenderProps) => ReactNode;\n}\n\nconst POSITIONS = {\n left: Position.Left,\n right: Position.Right,\n top: Position.Top,\n bottom: Position.Bottom,\n};\n\n/** Internal adapter; only the kit contracts above are exported from the package entry. */\nexport function adaptNodeRenderers(renderers: readonly FlowNodeRenderer[]): NodeTypes {\n const types: NodeTypes = { ...flowNodeTypes };\n for (const definition of renderers) {\n const counts = { left: 0, right: 0, top: 0, bottom: 0 };\n const handles = definition.handles.map((handle) => {\n const position = handle.position ?? (handle.kind === 'target' ? 'left' : 'right');\n return { ...handle, position, index: ++counts[position] };\n });\n const verticalCount = Math.max(counts.left, counts.right);\n const horizontalCount = Math.max(counts.top, counts.bottom);\n types[definition.type] = memo(function CustomNode({\n id,\n data,\n selected,\n isConnectable,\n }: NodeProps<FlowNode<string>>) {\n return (\n <div\n className=\"relative grid h-full w-full\"\n style={{\n minHeight: verticalCount ? (verticalCount + 1) * 32 : undefined,\n minWidth: horizontalCount ? (horizontalCount + 1) * 32 : undefined,\n }}\n >\n {handles.map((handle) => (\n <Handle\n key={`${handle.kind}-${handle.id}`}\n className=\"gu-flow-custom-handle\"\n id={handle.id}\n type={handle.kind}\n position={POSITIONS[handle.position]}\n isConnectable={isConnectable}\n aria-label={handle.label ?? handle.id}\n role=\"button\"\n tabIndex={isConnectable ? 0 : -1}\n aria-disabled={!isConnectable}\n onKeyDown={(event) => {\n if (!isConnectable || (event.key !== 'Enter' && event.key !== ' ')) return;\n event.preventDefault();\n event.stopPropagation();\n event.currentTarget.click();\n }}\n style={{\n [handle.position === 'top' || handle.position === 'bottom' ? 'left' : 'top']:\n `${(handle.index / (counts[handle.position] + 1)) * 100}%`,\n }}\n />\n ))}\n {definition.decorations !== 'consumer' && (\n <>\n {data.decoration?.status && (\n <div className=\"pointer-events-none absolute -top-2 right-1 z-10\">\n <FlowStatusBadge status={data.decoration.status} />\n </div>\n )}\n <FlowNodeIssues issues={data.decoration?.issues} />\n </>\n )}\n {definition.render({\n id,\n type: definition.type,\n data,\n selected,\n connectable: isConnectable,\n })}\n </div>\n );\n });\n }\n return types;\n}\n","import { useCallback, useMemo } from 'react';\nimport {\n Background,\n BackgroundVariant,\n Controls,\n MiniMap,\n ReactFlow,\n type Edge,\n type Node,\n type NodeTypes,\n type ReactFlowProps,\n} from '@xyflow/react';\nimport { flowNodeTypes, type FlowNode } from './nodes';\nimport { brandEdgeOptions, flowMarker, useFlowTheme, withBrandMarkers } from './theme';\nimport { adaptNodeRenderers, type FlowNodeRenderer } from './renderers';\nimport { cx, nodeToken } from './tones';\n\nexport interface FlowCanvasProps<N extends Node = FlowNode, E extends Edge = Edge> extends Omit<\n ReactFlowProps<N, E>,\n 'aria-label' | 'height' | 'width'\n> {\n /** Accessible name of the canvas region (required). */\n 'aria-label': string;\n /** Canvas height (px or CSS length). */\n height?: number | string;\n showControls?: boolean;\n /** Hide the control that toggles graph editing, while retaining zoom and fit controls. */\n showInteractiveControls?: boolean;\n showMiniMap?: boolean;\n showBackground?: boolean;\n /** false = static diagram: no pan/zoom/connect/select (nodes still render). */\n interactive?: boolean;\n /** Class for the inner `.react-flow` element (`className` goes on the wrapper). */\n canvasClassName?: string;\n /** Kit-owned custom node renderers with named handles. */\n nodeRenderers?: readonly FlowNodeRenderer[];\n}\n\nconst STATIC_PROPS = {\n nodesDraggable: false,\n nodesConnectable: false,\n elementsSelectable: false,\n panOnDrag: false,\n panOnScroll: false,\n zoomOnScroll: false,\n zoomOnPinch: false,\n zoomOnDoubleClick: false,\n preventScrolling: false,\n} as const;\n\n/**\n * React Flow canvas themed from the brand tokens: dotted background, controls and minimap,\n * brand node types, smoothstep edges with arrows (primary when animated). Drag, connect and\n * zoom/pan are on by default. Import `@gntik-ai/flow/styles.css` once.\n */\nexport function FlowCanvas<N extends Node = FlowNode, E extends Edge = Edge>({\n 'aria-label': label,\n height = 480,\n showControls = true,\n showInteractiveControls = true,\n showMiniMap = true,\n showBackground = true,\n interactive = true,\n className,\n canvasClassName,\n nodeTypes,\n nodeRenderers,\n edges,\n defaultEdgeOptions,\n proOptions,\n children,\n ...rest\n}: FlowCanvasProps<N, E>) {\n const t = useFlowTheme();\n const customTypes = useMemo(() => (nodeRenderers ? adaptNodeRenderers(nodeRenderers) : undefined), [nodeRenderers]);\n const brandedEdges = useMemo(() => (edges ? withBrandMarkers(edges, t) : undefined), [edges, t]);\n const edgeOptions = useMemo(\n () => ({ ...brandEdgeOptions, markerEnd: flowMarker(t.edge), ...defaultEdgeOptions }),\n [t, defaultEdgeOptions],\n );\n const miniColor = useCallback((n: N) => t.color(nodeToken(n.data.tone, n.type)), [t]);\n return (\n <div\n role=\"region\"\n aria-label={label}\n data-theme={t.theme}\n className={cx('gu-flow relative w-full bg-background', className)}\n style={{ height }}\n >\n <ReactFlow<N, E>\n nodeTypes={customTypes ?? nodeTypes ?? (flowNodeTypes as NodeTypes)}\n edges={brandedEdges}\n defaultEdgeOptions={edgeOptions}\n fitView\n fitViewOptions={{ padding: 0.12, maxZoom: 1 }}\n minZoom={0.3}\n maxZoom={1.75}\n snapToGrid\n snapGrid={[16, 16]}\n proOptions={proOptions ?? (interactive ? undefined : { hideAttribution: true })}\n {...(interactive ? undefined : STATIC_PROPS)}\n {...rest}\n className={canvasClassName}\n >\n {showBackground && <Background variant={BackgroundVariant.Dots} gap={20} size={1} color={t.pattern} />}\n {interactive && showControls && <Controls showInteractive={showInteractiveControls} />}\n {interactive && showMiniMap && (\n <MiniMap<N> pannable zoomable nodeColor={miniColor} nodeStrokeWidth={0} nodeBorderRadius={3} />\n )}\n {children}\n </ReactFlow>\n </div>\n );\n}\n","import { useCallback } from 'react';\nimport { addEdge, useEdgesState, useNodesState, type Connection, type Edge, type Node } from '@xyflow/react';\nimport type { FlowNode } from './nodes';\n\n/**\n * Local nodes/edges state wired for drag, select and connect. New connections become\n * active (animated) smoothstep edges. Spread the result into `<FlowCanvas>`.\n */\nexport function useFlowState<N extends Node = FlowNode, E extends Edge = Edge>(initialNodes: N[], initialEdges: E[]) {\n const [nodes, setNodes, onNodesChange] = useNodesState<N>(initialNodes);\n const [edges, setEdges, onEdgesChange] = useEdgesState<E>(initialEdges);\n const onConnect = useCallback(\n (c: Connection) => {\n // A Connection plus brand edge props (addEdge keeps the extra fields).\n const active: Connection = Object.assign({ type: 'smoothstep', animated: true }, c);\n setEdges((eds) => addEdge<E>(active, eds));\n },\n [setEdges],\n );\n return { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gntik-ai/flow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "React Flow (@xyflow/react 12) canvas, nodes and edges themed by the gntik-ui brand tokens.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"*.css"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src/styles.css"
|
|
13
|
+
],
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json",
|
|
22
|
+
"./styles.css": "./src/styles.css"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"registry": "https://registry.npmjs.org/",
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/gntik-ai/gntik-ui.git",
|
|
31
|
+
"directory": "packages/flow"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^19.0.0",
|
|
35
|
+
"react-dom": "^19.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@gntik-ai/tokens": "0.2.1",
|
|
39
|
+
"@xyflow/react": "^12.11.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
43
|
+
"@testing-library/react": "^16.3.3",
|
|
44
|
+
"@types/react": "^19.3.0",
|
|
45
|
+
"@types/react-dom": "^19.3.0",
|
|
46
|
+
"jsdom": "^30.1.1",
|
|
47
|
+
"react": "^19.3.0",
|
|
48
|
+
"react-dom": "^19.3.0",
|
|
49
|
+
"tsdown": "^0.23.0",
|
|
50
|
+
"typescript": "~6.0.3",
|
|
51
|
+
"vitest": "^5.0.2"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsdown",
|
|
55
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
56
|
+
"test": "vitest run"
|
|
57
|
+
}
|
|
6
58
|
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
@gntik-ai/flow/styles.css — React Flow themed by the gntik-ui brand tokens.
|
|
3
|
+
Load once, after the token layer (@gntik-ai/tokens/brand.css). Every --xy-*
|
|
4
|
+
variable points at a brand token, so the three themes of brand.css
|
|
5
|
+
(:root = light · .dark · .high_contrast) re-skin the canvas with no JS.
|
|
6
|
+
Scope: anything inside `.gu-flow` (FlowCanvas adds it to its wrapper).
|
|
7
|
+
============================================================================ */
|
|
8
|
+
@import "@xyflow/react/dist/style.css";
|
|
9
|
+
@source "../dist";
|
|
10
|
+
|
|
11
|
+
/* ── Variables: light (:root) and dark (.dark) share the mapping; the tokens differ ── */
|
|
12
|
+
.gu-flow,
|
|
13
|
+
.dark .gu-flow {
|
|
14
|
+
--xy-background-color: transparent;
|
|
15
|
+
--xy-background-pattern-color: hsl(var(--border));
|
|
16
|
+
|
|
17
|
+
--xy-edge-stroke: hsl(var(--muted-foreground) / 0.45);
|
|
18
|
+
--xy-edge-stroke-width: 1.5;
|
|
19
|
+
--xy-edge-stroke-selected: hsl(var(--primary));
|
|
20
|
+
--xy-edge-label-background-color: hsl(var(--card));
|
|
21
|
+
--xy-edge-label-color: hsl(var(--foreground));
|
|
22
|
+
--xy-connectionline-stroke: hsl(var(--primary));
|
|
23
|
+
--xy-connectionline-stroke-width: 1.75;
|
|
24
|
+
|
|
25
|
+
--xy-node-color: hsl(var(--card-foreground));
|
|
26
|
+
--xy-node-background-color: hsl(var(--card));
|
|
27
|
+
--xy-node-border: 1px solid hsl(var(--border));
|
|
28
|
+
--xy-node-border-radius: var(--radius-md);
|
|
29
|
+
--xy-node-group-background-color: hsl(var(--muted) / 0.4);
|
|
30
|
+
--xy-node-boxshadow-hover: var(--shadow-sm);
|
|
31
|
+
--xy-node-boxshadow-selected: 0 0 0 2px hsl(var(--primary) / 0.35);
|
|
32
|
+
|
|
33
|
+
--xy-handle-background-color: hsl(var(--card));
|
|
34
|
+
--xy-handle-border-color: hsl(var(--muted-foreground) / 0.55);
|
|
35
|
+
|
|
36
|
+
--xy-selection-background-color: hsl(var(--primary) / 0.08);
|
|
37
|
+
--xy-selection-border: 1px dashed hsl(var(--primary) / 0.6);
|
|
38
|
+
--xy-resize-background-color: hsl(var(--primary));
|
|
39
|
+
|
|
40
|
+
--xy-controls-button-background-color: hsl(var(--card));
|
|
41
|
+
--xy-controls-button-background-color-hover: hsl(var(--secondary));
|
|
42
|
+
--xy-controls-button-color: hsl(var(--foreground));
|
|
43
|
+
--xy-controls-button-color-hover: hsl(var(--foreground));
|
|
44
|
+
--xy-controls-button-border-color: hsl(var(--border));
|
|
45
|
+
--xy-controls-box-shadow: var(--shadow-md);
|
|
46
|
+
|
|
47
|
+
--xy-minimap-background-color: hsl(var(--card));
|
|
48
|
+
--xy-minimap-mask-background-color: hsl(var(--muted-foreground) / 0.12);
|
|
49
|
+
--xy-minimap-mask-stroke-color: hsl(var(--border));
|
|
50
|
+
--xy-minimap-mask-stroke-width: 1;
|
|
51
|
+
--xy-minimap-node-background-color: hsl(var(--muted-foreground) / 0.4);
|
|
52
|
+
--xy-minimap-node-stroke-color: transparent;
|
|
53
|
+
|
|
54
|
+
--xy-attribution-background-color: transparent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ── High contrast: solid strokes, stronger handles and mask ─────────────────── */
|
|
58
|
+
.high_contrast .gu-flow {
|
|
59
|
+
--xy-edge-stroke: hsl(var(--foreground) / 0.75);
|
|
60
|
+
--xy-edge-stroke-width: 2;
|
|
61
|
+
--xy-handle-border-color: hsl(var(--foreground));
|
|
62
|
+
--xy-node-border: 2px solid hsl(var(--border));
|
|
63
|
+
--xy-selection-border: 2px solid hsl(var(--primary));
|
|
64
|
+
--xy-minimap-mask-background-color: hsl(var(--foreground) / 0.18);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ── Structure ───────────────────────────────────────────────────────────────── */
|
|
68
|
+
.gu-flow .react-flow__pane { cursor: grab; }
|
|
69
|
+
.gu-flow .react-flow__pane.dragging { cursor: grabbing; }
|
|
70
|
+
|
|
71
|
+
/* Active (animated) edges and the edge being connected are primary. */
|
|
72
|
+
.gu-flow .react-flow__edge.animated .react-flow__edge-path { stroke: hsl(var(--primary)); stroke-width: 1.75px; }
|
|
73
|
+
.gu-flow .react-flow__edge:focus-visible .react-flow__edge-path { stroke: hsl(var(--primary)); }
|
|
74
|
+
.gu-flow .react-flow__edge-text { font-family: var(--font-mono); font-size: 10px; font-weight: 500; }
|
|
75
|
+
|
|
76
|
+
/* Handles: small card dot → primary on hover / while connecting. */
|
|
77
|
+
.gu-flow .react-flow__handle {
|
|
78
|
+
width: 9px; height: 9px; min-width: 0; min-height: 0; border-width: 1.5px; border-radius: 9999px;
|
|
79
|
+
transition: background-color 0.12s, border-color 0.12s;
|
|
80
|
+
}
|
|
81
|
+
.gu-flow .react-flow__handle:hover,
|
|
82
|
+
.gu-flow .react-flow__handle.connectingto,
|
|
83
|
+
.gu-flow .react-flow__handle.connectingfrom,
|
|
84
|
+
.gu-flow .react-flow__handle.valid { background: hsl(var(--primary)); border-color: hsl(var(--primary)); }
|
|
85
|
+
|
|
86
|
+
/* Named handles retain the small dot inside a separate 32px pointer target.
|
|
87
|
+
Custom renderers reserve space per side so adjacent targets do not overlap. */
|
|
88
|
+
.gu-flow .react-flow__handle.gu-flow-custom-handle {
|
|
89
|
+
width: 32px; height: 32px; border: none; background: transparent; z-index: 1;
|
|
90
|
+
}
|
|
91
|
+
.gu-flow .gu-flow-custom-handle:focus-visible {
|
|
92
|
+
outline: 2px solid hsl(var(--focus-ring)); outline-offset: 2px;
|
|
93
|
+
}
|
|
94
|
+
.gu-flow .gu-flow-custom-handle::after {
|
|
95
|
+
content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
96
|
+
width: 9px; height: 9px; border-radius: 9999px; pointer-events: none;
|
|
97
|
+
background: var(--xy-handle-background-color); border: 1.5px solid var(--xy-handle-border-color);
|
|
98
|
+
transition: background-color 0.12s, border-color 0.12s;
|
|
99
|
+
}
|
|
100
|
+
.gu-flow .gu-flow-custom-handle:hover::after,
|
|
101
|
+
.gu-flow .gu-flow-custom-handle.connectingto::after,
|
|
102
|
+
.gu-flow .gu-flow-custom-handle.connectingfrom::after,
|
|
103
|
+
.gu-flow .gu-flow-custom-handle.valid::after {
|
|
104
|
+
background: var(--xy-connectionline-stroke); border-color: var(--xy-connectionline-stroke);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Nodes: the brand card draws its own border/ring; reset the built-in node boxes
|
|
108
|
+
(default/input/output/group) so a custom type named like one never gets the white box. */
|
|
109
|
+
.gu-flow .react-flow__node { font-family: var(--font-sans); }
|
|
110
|
+
.gu-flow .react-flow__node:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; border-radius: var(--radius-lg); }
|
|
111
|
+
.gu-flow .react-flow__node.selected { box-shadow: none; }
|
|
112
|
+
.gu-flow .react-flow__node-default,
|
|
113
|
+
.gu-flow .react-flow__node-input,
|
|
114
|
+
.gu-flow .react-flow__node-output,
|
|
115
|
+
.gu-flow .react-flow__node-group {
|
|
116
|
+
padding: 0; width: auto; background: transparent; border: none; border-radius: 0;
|
|
117
|
+
color: inherit; box-shadow: none; text-align: left;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Controls + minimap. */
|
|
121
|
+
.gu-flow .react-flow__controls { border: 1px solid hsl(var(--border)); border-radius: var(--radius-md); overflow: hidden; }
|
|
122
|
+
.gu-flow .react-flow__controls-button { width: 26px; height: 26px; }
|
|
123
|
+
.gu-flow .react-flow__controls-button:last-child { border-bottom: none; }
|
|
124
|
+
.gu-flow .react-flow__controls-button svg { fill: currentColor; max-width: 13px; max-height: 13px; }
|
|
125
|
+
.gu-flow .react-flow__minimap { border: 1px solid hsl(var(--border)); border-radius: var(--radius-md); overflow: hidden; }
|
|
126
|
+
|
|
127
|
+
/* Attribution: discreet. */
|
|
128
|
+
.gu-flow .react-flow__attribution { padding: 2px 5px; }
|
|
129
|
+
.gu-flow .react-flow__attribution a { color: hsl(var(--muted-foreground) / 0.55); font-size: 9px; }
|
|
130
|
+
|
|
131
|
+
/* "Running" node: a primary segment travels around the card border. */
|
|
132
|
+
.gu-flow-run-frame { overflow: visible; }
|
|
133
|
+
.gu-flow-run {
|
|
134
|
+
x: 1.25px; y: 1.25px; width: calc(100% - 2.5px); height: calc(100% - 2.5px); rx: 9px;
|
|
135
|
+
animation: gu-flow-run 1.8s linear infinite;
|
|
136
|
+
}
|
|
137
|
+
@keyframes gu-flow-run { to { stroke-dashoffset: -100; } }
|
|
138
|
+
@media (prefers-reduced-motion: reduce) { .gu-flow-run { animation: none; } }
|
package/README.md
DELETED