@bysages/workflow 0.1.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/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/index.d.mts +202 -0
- package/dist/index.mjs +561 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 By Sages
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @bysages/workflow
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
> The workflow canvas for By Sages Elements — a headless graph protocol (store, change stream, versioned serialization) with an [AntV X6](https://x6.antv.antgroup.com) adapter on top, dressed entirely by the paper-and-ink styles from `@bysages/core`.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🧠 **Headless first** — the protocol layer (`types`, `store`, `serialize`) imports no X6: executors, persistence, and tests run in Node, and the canvas engine stays replaceable
|
|
12
|
+
- 🖋 **Visuals live in core** — cards, handles, edges, and the minimap are `data-scope="workflow"` styles driven by tokens; light/dark, accent, contrast, and density tiers come free
|
|
13
|
+
- 🧩 **Host-owned nodes** — `renderNode` hands every node card to your framework (Vue, React, or plain DOM); the canvas never mounts content of its own
|
|
14
|
+
- 🎛 **Editor-grade defaults** — drag, connect with direction-aware handles, rubberband select, undo/redo, delete, minimap, fit view; the elkjs auto layout reads the edges' ports and sweeps the way the graph flows
|
|
15
|
+
- ⛓ **Execution stays out of history** — state writes (`node:state`, `edge:state`) ride a separate change source and never enter the undo stack
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# pnpm
|
|
21
|
+
pnpm add @bysages/workflow
|
|
22
|
+
|
|
23
|
+
# npm
|
|
24
|
+
npm install @bysages/workflow
|
|
25
|
+
|
|
26
|
+
# yarn
|
|
27
|
+
yarn add @bysages/workflow
|
|
28
|
+
|
|
29
|
+
# bun
|
|
30
|
+
bun add @bysages/workflow
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
X6 is browser-only — import the canvas from client code only (a dynamic import in SSR apps).
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
Describe the graph as data, mount a canvas, and hand each card to your framework:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createWorkflowCanvas, createWorkflowStore } from "@bysages/workflow";
|
|
41
|
+
|
|
42
|
+
const store = createWorkflowStore({
|
|
43
|
+
nodes: [
|
|
44
|
+
{
|
|
45
|
+
id: "start",
|
|
46
|
+
type: "start",
|
|
47
|
+
position: { x: 60, y: 40 },
|
|
48
|
+
ports: [{ id: "out-1", dir: "out" }],
|
|
49
|
+
data: { label: "Start" },
|
|
50
|
+
},
|
|
51
|
+
// …
|
|
52
|
+
],
|
|
53
|
+
edges: [],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const canvas = createWorkflowCanvas(host, store, {
|
|
57
|
+
// Your framework mounts the content of every node card.
|
|
58
|
+
renderNode: (card, node) => {
|
|
59
|
+
card.textContent = String(node.data.label ?? node.id);
|
|
60
|
+
},
|
|
61
|
+
minimap: { container: mapHost },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Execution writes states back through the store — the canvas lights up.
|
|
65
|
+
store.setNodeState("start", "running");
|
|
66
|
+
|
|
67
|
+
// Persist the whole graph with a version envelope.
|
|
68
|
+
const saved = serializeWorkflowGraph(store.getGraph());
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Every write goes through the store and fans out as a change; the canvas applies host changes, and host listeners observe canvas changes — drag a node and your store subscription sees it, write a position and the canvas moves it.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
- [MIT](../../LICENSE) © [By Sages](https://www.bysages.com/)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Graph } from "@antv/x6";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
/** The engine-independent facts of a workflow graph. The canvas adapter
|
|
4
|
+
* reads them; the execution backend writes states back through the
|
|
5
|
+
* store. Nothing here knows about SVG, DOM, or any rendering engine.
|
|
6
|
+
*
|
|
7
|
+
* These shapes deliberately don't reuse X6's (NodeMetadata, TerminalData,
|
|
8
|
+
* PointLike, …): importing them would couple the protocol's type surface
|
|
9
|
+
* to the canvas engine, and executors — servers included — would then
|
|
10
|
+
* carry X6's types just to describe a graph. Duplication here is the
|
|
11
|
+
* boundary, not an oversight. */
|
|
12
|
+
/** Runtime state of one node — the same `data-state` contract every
|
|
13
|
+
* component reads. The canvas only displays it; the executor writes it. */
|
|
14
|
+
export type NodeState = "idle" | "running" | "success" | "error";
|
|
15
|
+
export interface Position {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
}
|
|
19
|
+
/** A logical port. `dir` fixes the geometry (in = top, out = bottom);
|
|
20
|
+
* `type` is reserved for typed-connection labels. */
|
|
21
|
+
export interface WorkflowPort {
|
|
22
|
+
id: string;
|
|
23
|
+
dir: "in" | "out";
|
|
24
|
+
type?: string;
|
|
25
|
+
}
|
|
26
|
+
export type WorkflowNodeData = Record<string, unknown>;
|
|
27
|
+
/** One node on the graph. `type` is the renderer's lookup key; `ports`
|
|
28
|
+
* is required — the adapter derives geometry and connection validation
|
|
29
|
+
* from it. `state` defaults to idle. */
|
|
30
|
+
export interface WorkflowNode {
|
|
31
|
+
id: string;
|
|
32
|
+
type: string;
|
|
33
|
+
position: Position;
|
|
34
|
+
ports: WorkflowPort[];
|
|
35
|
+
state?: NodeState;
|
|
36
|
+
data: WorkflowNodeData;
|
|
37
|
+
}
|
|
38
|
+
export interface WorkflowEndpoint {
|
|
39
|
+
node: string;
|
|
40
|
+
port: string;
|
|
41
|
+
}
|
|
42
|
+
export interface WorkflowEdge {
|
|
43
|
+
id: string;
|
|
44
|
+
source: WorkflowEndpoint;
|
|
45
|
+
target: WorkflowEndpoint;
|
|
46
|
+
/** Runtime state — the same `data-state` contract the CSS reads on the
|
|
47
|
+
* edge line. The canvas only displays it; the executor writes it. */
|
|
48
|
+
state?: NodeState;
|
|
49
|
+
}
|
|
50
|
+
export interface WorkflowGraph {
|
|
51
|
+
nodes: readonly WorkflowNode[];
|
|
52
|
+
edges: readonly WorkflowEdge[];
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/store.d.ts
|
|
56
|
+
/** A record of one graph change. It is both the listener payload and the
|
|
57
|
+
* command protocol: replaying changes is how an executor (or a future
|
|
58
|
+
* undo stack) drives the graph from the outside. */
|
|
59
|
+
export type WorkflowChange = {
|
|
60
|
+
kind: "node:add";
|
|
61
|
+
node: WorkflowNode;
|
|
62
|
+
} | {
|
|
63
|
+
kind: "node:remove";
|
|
64
|
+
id: string;
|
|
65
|
+
} | {
|
|
66
|
+
kind: "node:move";
|
|
67
|
+
id: string;
|
|
68
|
+
position: Position;
|
|
69
|
+
} | {
|
|
70
|
+
kind: "node:state";
|
|
71
|
+
id: string;
|
|
72
|
+
state: NodeState;
|
|
73
|
+
} | {
|
|
74
|
+
kind: "node:data";
|
|
75
|
+
id: string;
|
|
76
|
+
patch: WorkflowNodeData;
|
|
77
|
+
} | {
|
|
78
|
+
kind: "edge:connect";
|
|
79
|
+
edge: WorkflowEdge;
|
|
80
|
+
} | {
|
|
81
|
+
kind: "edge:state";
|
|
82
|
+
id: string;
|
|
83
|
+
state: NodeState;
|
|
84
|
+
} | {
|
|
85
|
+
kind: "edge:remove";
|
|
86
|
+
id: string;
|
|
87
|
+
};
|
|
88
|
+
/** Who set a change in motion: the canvas translating one of its own
|
|
89
|
+
* gestures, or anything else (the host, an executor, a future undo
|
|
90
|
+
* stack). Listeners read it to skip echoing a change back to where it
|
|
91
|
+
* came from — the same office Yjs' transaction origin serves. */
|
|
92
|
+
export type WorkflowChangeSource = "canvas" | "host";
|
|
93
|
+
export interface WorkflowStore {
|
|
94
|
+
getGraph(): WorkflowGraph;
|
|
95
|
+
getNode(id: string): Readonly<WorkflowNode> | undefined;
|
|
96
|
+
getEdge(id: string): Readonly<WorkflowEdge> | undefined;
|
|
97
|
+
addNode(node: WorkflowNode): void;
|
|
98
|
+
/** Removes a node and every edge attached to it, each removal its own
|
|
99
|
+
* change event. Unknown ids are a silent no-op — idempotence is what
|
|
100
|
+
* breaks the canvas event loops. */
|
|
101
|
+
removeNode(id: string): void;
|
|
102
|
+
moveNode(id: string, position: Position): void;
|
|
103
|
+
setNodeState(id: string, state: NodeState): void;
|
|
104
|
+
/** Shallow-merges into the node's data. */
|
|
105
|
+
setNodeData(id: string, patch: WorkflowNodeData): void;
|
|
106
|
+
/** Connects two ports and returns the edge; an already existing
|
|
107
|
+
* source→target pair (or id) returns that edge untouched. */
|
|
108
|
+
connect(edge: Omit<WorkflowEdge, "id"> & {
|
|
109
|
+
id?: string;
|
|
110
|
+
}): WorkflowEdge | undefined;
|
|
111
|
+
/** Marks an edge's execution state — the edge twin of setNodeState. */
|
|
112
|
+
setEdgeState(id: string, state: NodeState): void;
|
|
113
|
+
disconnect(id: string): void;
|
|
114
|
+
/** Runs fn, attributing every change it commits to `source`. The
|
|
115
|
+
* attribution covers this one synchronous call only: a listener the
|
|
116
|
+
* changes notify writes back as the host, never as `source` — which
|
|
117
|
+
* is what keeps a listener's own follow-up writes from being
|
|
118
|
+
* silently swallowed by the listener that skips echoes. */
|
|
119
|
+
asSource<T>(source: WorkflowChangeSource, fn: () => T): T;
|
|
120
|
+
subscribe(listener: (change: WorkflowChange, source: WorkflowChangeSource) => void): () => void;
|
|
121
|
+
}
|
|
122
|
+
/** The single writer of the graph. Every operation is idempotent (unknown
|
|
123
|
+
* ids are silent no-ops, redundant writes don't re-emit) so event loops
|
|
124
|
+
* between the canvas and the store die on their own. */
|
|
125
|
+
export declare function createWorkflowStore(graph?: WorkflowGraph): WorkflowStore;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/serialize.d.ts
|
|
128
|
+
/** The wire format's contract version. Bump it whenever the graph shape
|
|
129
|
+
* breaks what older readers expect, and teach deserialize to migrate. */
|
|
130
|
+
export declare const WORKFLOW_GRAPH_VERSION = 1;
|
|
131
|
+
/** The versioned envelope a graph travels in. */
|
|
132
|
+
export interface SerializedWorkflowGraph {
|
|
133
|
+
version: typeof WORKFLOW_GRAPH_VERSION;
|
|
134
|
+
graph: WorkflowGraph;
|
|
135
|
+
}
|
|
136
|
+
export declare const serializeWorkflowGraph: (graph: WorkflowGraph) => SerializedWorkflowGraph;
|
|
137
|
+
/** Reads a serialized graph back. An unrecognized version means the
|
|
138
|
+
* payload was written by an incompatible writer — refuse it loudly
|
|
139
|
+
* rather than render a half-understood graph. */
|
|
140
|
+
export declare const deserializeWorkflowGraph: (serialized: {
|
|
141
|
+
version: number;
|
|
142
|
+
graph: WorkflowGraph;
|
|
143
|
+
}) => WorkflowGraph;
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/x6-canvas.d.ts
|
|
146
|
+
/** How the layered engine should sweep the graph. Left out, the
|
|
147
|
+
* direction is read off the graph: edges drawn between side handles
|
|
148
|
+
* vote horizontal, edges between the top/bottom handles vote
|
|
149
|
+
* vertical. Top-to-bottom suits the node's first in/out pair — out
|
|
150
|
+
* handles on the bottom, in handles on top. */
|
|
151
|
+
export interface WorkflowAutoLayoutOptions {
|
|
152
|
+
direction?: "TB" | "LR" | "BT" | "RL";
|
|
153
|
+
/** The gap between layers, in px. */
|
|
154
|
+
rankSep?: number;
|
|
155
|
+
/** The gap between siblings on one layer, in px. */
|
|
156
|
+
nodeSep?: number;
|
|
157
|
+
}
|
|
158
|
+
/** The framework mount point: the host mounts its components into the
|
|
159
|
+
* node's card and returns an unmount cleanup (optional). Content updates
|
|
160
|
+
* stay with the host — subscribe to the store in the closure. */
|
|
161
|
+
export interface WorkflowCanvasOptions {
|
|
162
|
+
renderNode?: (host: HTMLElement, node: Readonly<WorkflowNode>) => void | (() => void);
|
|
163
|
+
/** Handed the live Graph once it exists — the escape hatch for plugin
|
|
164
|
+
* wiring this adapter doesn't cover. */
|
|
165
|
+
onReady?: (graph: Graph) => void;
|
|
166
|
+
/** Renders a live minimap into the given element. */
|
|
167
|
+
minimap?: {
|
|
168
|
+
container: HTMLElement;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** Viewport options for fitView. X6's own fit options are accepted in
|
|
172
|
+
* full through the graph escape hatch; these cover the calls a toolbar
|
|
173
|
+
* actually makes. */
|
|
174
|
+
export interface WorkflowFitViewOptions {
|
|
175
|
+
padding?: number;
|
|
176
|
+
maxScale?: number;
|
|
177
|
+
minScale?: number;
|
|
178
|
+
}
|
|
179
|
+
export interface WorkflowCanvas {
|
|
180
|
+
readonly store: WorkflowStore;
|
|
181
|
+
/** Read-only escape hatch; the lifecycle stays with the canvas. */
|
|
182
|
+
readonly graph: Graph;
|
|
183
|
+
/** Multiplies the current zoom — the relative step X6's own API
|
|
184
|
+
* doesn't offer, and what every zoom toolbar button is. */
|
|
185
|
+
zoomBy(factor: number): void;
|
|
186
|
+
/** Frames the whole graph. Defaults to what a toolbar wants: a
|
|
187
|
+
* breathing margin and no magnifying past 100%. */
|
|
188
|
+
fitView(options?: WorkflowFitViewOptions): void;
|
|
189
|
+
/** Sweeps the graph with the ELK layered algorithm and writes every
|
|
190
|
+
* node's new position back through the store. Async: the layout
|
|
191
|
+
* engine is heavy, so it's pulled in only when this runs. */
|
|
192
|
+
layout(options?: WorkflowAutoLayoutOptions): Promise<void>;
|
|
193
|
+
destroy(): void;
|
|
194
|
+
}
|
|
195
|
+
/** Mounts an X6 canvas onto `container` and keeps it in step with the
|
|
196
|
+
* graph protocol. The store is the single writer; canvas gestures are
|
|
197
|
+
* translated back into store calls, and store changes — from an executor
|
|
198
|
+
* or the host app — are replayed onto the canvas. Every store operation
|
|
199
|
+
* is idempotent, which is what lets those two event streams cross
|
|
200
|
+
* without loops. */
|
|
201
|
+
export declare function createWorkflowCanvas(container: HTMLElement, source: WorkflowGraph | WorkflowStore, options?: WorkflowCanvasOptions): WorkflowCanvas;
|
|
202
|
+
//#endregion
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { Graph, History, Keyboard, MiniMap, Selection, Shape } from "@antv/x6";
|
|
2
|
+
import { injectComponentStyle } from "@bysages/core";
|
|
3
|
+
//#region src/store.ts
|
|
4
|
+
/** The single writer of the graph. Every operation is idempotent (unknown
|
|
5
|
+
* ids are silent no-ops, redundant writes don't re-emit) so event loops
|
|
6
|
+
* between the canvas and the store die on their own. */
|
|
7
|
+
function createWorkflowStore(graph) {
|
|
8
|
+
const copyNode = (node) => ({
|
|
9
|
+
...node,
|
|
10
|
+
position: { ...node.position },
|
|
11
|
+
ports: node.ports.map((port) => ({ ...port })),
|
|
12
|
+
data: { ...node.data }
|
|
13
|
+
});
|
|
14
|
+
const nodes = new Map(graph?.nodes.map((n) => [n.id, copyNode(n)]) ?? []);
|
|
15
|
+
const edges = new Map(graph?.edges.map((e) => [e.id, { ...e }]) ?? []);
|
|
16
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
17
|
+
let edgeSeq = 0;
|
|
18
|
+
let changeSource = "host";
|
|
19
|
+
const nextEdgeId = () => {
|
|
20
|
+
let id;
|
|
21
|
+
do
|
|
22
|
+
id = `edge-${++edgeSeq}`;
|
|
23
|
+
while (edges.has(id));
|
|
24
|
+
return id;
|
|
25
|
+
};
|
|
26
|
+
const commit = (change) => {
|
|
27
|
+
const source = changeSource;
|
|
28
|
+
changeSource = "host";
|
|
29
|
+
try {
|
|
30
|
+
for (const listener of Array.from(listeners)) listener(change, source);
|
|
31
|
+
} finally {
|
|
32
|
+
changeSource = source;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const store = {
|
|
36
|
+
getGraph: () => ({
|
|
37
|
+
nodes: [...nodes.values()],
|
|
38
|
+
edges: [...edges.values()]
|
|
39
|
+
}),
|
|
40
|
+
getNode: (id) => nodes.get(id),
|
|
41
|
+
getEdge: (id) => edges.get(id),
|
|
42
|
+
addNode(node) {
|
|
43
|
+
if (nodes.has(node.id)) return;
|
|
44
|
+
nodes.set(node.id, copyNode(node));
|
|
45
|
+
commit({
|
|
46
|
+
kind: "node:add",
|
|
47
|
+
node: copyNode(node)
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
removeNode(id) {
|
|
51
|
+
if (!nodes.delete(id)) return;
|
|
52
|
+
for (const edge of Array.from(edges.values())) if (edge.source.node === id || edge.target.node === id) store.disconnect(edge.id);
|
|
53
|
+
commit({
|
|
54
|
+
kind: "node:remove",
|
|
55
|
+
id
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
moveNode(id, position) {
|
|
59
|
+
const node = nodes.get(id);
|
|
60
|
+
if (!node) return;
|
|
61
|
+
if (node.position.x === position.x && node.position.y === position.y) return;
|
|
62
|
+
node.position = { ...position };
|
|
63
|
+
commit({
|
|
64
|
+
kind: "node:move",
|
|
65
|
+
id,
|
|
66
|
+
position: { ...position }
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
setNodeState(id, state) {
|
|
70
|
+
const node = nodes.get(id);
|
|
71
|
+
if (!node || node.state === state) return;
|
|
72
|
+
node.state = state;
|
|
73
|
+
commit({
|
|
74
|
+
kind: "node:state",
|
|
75
|
+
id,
|
|
76
|
+
state
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
setNodeData(id, patch) {
|
|
80
|
+
const node = nodes.get(id);
|
|
81
|
+
if (!node) return;
|
|
82
|
+
node.data = {
|
|
83
|
+
...node.data,
|
|
84
|
+
...patch
|
|
85
|
+
};
|
|
86
|
+
commit({
|
|
87
|
+
kind: "node:data",
|
|
88
|
+
id,
|
|
89
|
+
patch: { ...patch }
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
connect(edge) {
|
|
93
|
+
const byId = edge.id != null ? edges.get(edge.id) : void 0;
|
|
94
|
+
if (byId) return byId;
|
|
95
|
+
const source = { ...edge.source };
|
|
96
|
+
const target = { ...edge.target };
|
|
97
|
+
for (const existing of edges.values()) if (existing.source.node === source.node && existing.source.port === source.port && existing.target.node === target.node && existing.target.port === target.port) return existing;
|
|
98
|
+
const record = {
|
|
99
|
+
id: edge.id ?? nextEdgeId(),
|
|
100
|
+
source,
|
|
101
|
+
target
|
|
102
|
+
};
|
|
103
|
+
if (edge.state) record.state = edge.state;
|
|
104
|
+
edges.set(record.id, record);
|
|
105
|
+
commit({
|
|
106
|
+
kind: "edge:connect",
|
|
107
|
+
edge: { ...record }
|
|
108
|
+
});
|
|
109
|
+
return record;
|
|
110
|
+
},
|
|
111
|
+
setEdgeState(id, state) {
|
|
112
|
+
const edge = edges.get(id);
|
|
113
|
+
if (!edge || edge.state === state) return;
|
|
114
|
+
edge.state = state;
|
|
115
|
+
commit({
|
|
116
|
+
kind: "edge:state",
|
|
117
|
+
id,
|
|
118
|
+
state
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
disconnect(id) {
|
|
122
|
+
if (!edges.delete(id)) return;
|
|
123
|
+
commit({
|
|
124
|
+
kind: "edge:remove",
|
|
125
|
+
id
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
asSource(source, fn) {
|
|
129
|
+
const outer = changeSource;
|
|
130
|
+
changeSource = source;
|
|
131
|
+
try {
|
|
132
|
+
return fn();
|
|
133
|
+
} finally {
|
|
134
|
+
changeSource = outer;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
subscribe(listener) {
|
|
138
|
+
listeners.add(listener);
|
|
139
|
+
return () => listeners.delete(listener);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
return store;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/serialize.ts
|
|
146
|
+
/** The wire format's contract version. Bump it whenever the graph shape
|
|
147
|
+
* breaks what older readers expect, and teach deserialize to migrate. */
|
|
148
|
+
const WORKFLOW_GRAPH_VERSION = 1;
|
|
149
|
+
const serializeWorkflowGraph = (graph) => ({
|
|
150
|
+
version: 1,
|
|
151
|
+
graph
|
|
152
|
+
});
|
|
153
|
+
/** Reads a serialized graph back. An unrecognized version means the
|
|
154
|
+
* payload was written by an incompatible writer — refuse it loudly
|
|
155
|
+
* rather than render a half-understood graph. */
|
|
156
|
+
const deserializeWorkflowGraph = (serialized) => {
|
|
157
|
+
if (serialized.version !== 1) throw new Error(`Unsupported workflow graph version ${serialized.version} (expected 1)`);
|
|
158
|
+
return serialized.graph;
|
|
159
|
+
};
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/x6-canvas.ts
|
|
162
|
+
const NODE_WIDTH = 256;
|
|
163
|
+
const NODE_HEIGHT = 88;
|
|
164
|
+
const HANDLE = 12;
|
|
165
|
+
const canvasContexts = /* @__PURE__ */ new WeakMap();
|
|
166
|
+
const SHAPE_NAME = "workflow-html-node";
|
|
167
|
+
Shape.HTML.register({
|
|
168
|
+
shape: SHAPE_NAME,
|
|
169
|
+
effect: [],
|
|
170
|
+
width: NODE_WIDTH,
|
|
171
|
+
height: NODE_HEIGHT,
|
|
172
|
+
html(cell) {
|
|
173
|
+
const model = cell.model;
|
|
174
|
+
const context = model ? canvasContexts.get(model.graph) : void 0;
|
|
175
|
+
if (!context) return document.createElement("div");
|
|
176
|
+
const node = context.store.getNode(cell.id);
|
|
177
|
+
if (!node) return document.createElement("div");
|
|
178
|
+
const host = document.createElement("div");
|
|
179
|
+
host.dataset.scope = "workflow";
|
|
180
|
+
host.dataset.part = "node";
|
|
181
|
+
const card = document.createElement("div");
|
|
182
|
+
card.className = "workflow-node-card";
|
|
183
|
+
card.dataset.state = node.state ?? "idle";
|
|
184
|
+
host.append(card);
|
|
185
|
+
const dispose = context.renderNode?.(card, node);
|
|
186
|
+
if (dispose) context.cleanups.set(cell.id, dispose);
|
|
187
|
+
cell.on("change:data", ({ current }) => {
|
|
188
|
+
const data = current ?? {};
|
|
189
|
+
card.dataset.state = data.workflowState ?? "idle";
|
|
190
|
+
});
|
|
191
|
+
return host;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
/** One live canvas per container — re-creating over an existing canvas
|
|
195
|
+
* (hot reload does this) tears the old one down first. */
|
|
196
|
+
const live = /* @__PURE__ */ new WeakMap();
|
|
197
|
+
/** Mounts an X6 canvas onto `container` and keeps it in step with the
|
|
198
|
+
* graph protocol. The store is the single writer; canvas gestures are
|
|
199
|
+
* translated back into store calls, and store changes — from an executor
|
|
200
|
+
* or the host app — are replayed onto the canvas. Every store operation
|
|
201
|
+
* is idempotent, which is what lets those two event streams cross
|
|
202
|
+
* without loops. */
|
|
203
|
+
function createWorkflowCanvas(container, source, options = {}) {
|
|
204
|
+
live.get(container)?.destroy();
|
|
205
|
+
injectComponentStyle("workflow");
|
|
206
|
+
const store = "getGraph" in source ? source : createWorkflowStore(source);
|
|
207
|
+
const cleanups = /* @__PURE__ */ new Map();
|
|
208
|
+
const sideGroup = (position, dir) => ({
|
|
209
|
+
position,
|
|
210
|
+
markup: [{
|
|
211
|
+
tagName: "rect",
|
|
212
|
+
selector: "handle"
|
|
213
|
+
}],
|
|
214
|
+
attrs: { handle: {
|
|
215
|
+
magnet: true,
|
|
216
|
+
x: -6,
|
|
217
|
+
y: -6,
|
|
218
|
+
width: HANDLE,
|
|
219
|
+
height: HANDLE,
|
|
220
|
+
"data-part": "handle",
|
|
221
|
+
"data-dir": dir
|
|
222
|
+
} }
|
|
223
|
+
});
|
|
224
|
+
const portGroups = {
|
|
225
|
+
in: sideGroup("top", "in"),
|
|
226
|
+
"in-alt": sideGroup("left", "in"),
|
|
227
|
+
out: sideGroup("bottom", "out"),
|
|
228
|
+
"out-alt": sideGroup("right", "out")
|
|
229
|
+
};
|
|
230
|
+
const edgeAttrs = { line: {
|
|
231
|
+
class: "workflow-edge-line",
|
|
232
|
+
targetMarker: {
|
|
233
|
+
name: "classic",
|
|
234
|
+
size: 6,
|
|
235
|
+
fill: "context-stroke"
|
|
236
|
+
}
|
|
237
|
+
} };
|
|
238
|
+
const toNodeConfig = (node) => ({
|
|
239
|
+
id: node.id,
|
|
240
|
+
shape: SHAPE_NAME,
|
|
241
|
+
x: node.position.x,
|
|
242
|
+
y: node.position.y,
|
|
243
|
+
data: {
|
|
244
|
+
...node.data,
|
|
245
|
+
workflowType: node.type,
|
|
246
|
+
workflowState: node.state ?? "idle"
|
|
247
|
+
},
|
|
248
|
+
ports: {
|
|
249
|
+
groups: portGroups,
|
|
250
|
+
items: (() => {
|
|
251
|
+
let ins = 0;
|
|
252
|
+
let outs = 0;
|
|
253
|
+
return node.ports.map((p) => ({
|
|
254
|
+
id: p.id,
|
|
255
|
+
group: p.dir === "in" ? ins++ % 2 === 0 ? "in" : "in-alt" : outs++ % 2 === 0 ? "out" : "out-alt"
|
|
256
|
+
}));
|
|
257
|
+
})()
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
const toEdgeConfig = (edge) => ({
|
|
261
|
+
id: edge.id,
|
|
262
|
+
source: {
|
|
263
|
+
cell: edge.source.node,
|
|
264
|
+
port: edge.source.port
|
|
265
|
+
},
|
|
266
|
+
target: {
|
|
267
|
+
cell: edge.target.node,
|
|
268
|
+
port: edge.target.port
|
|
269
|
+
},
|
|
270
|
+
attrs: { line: {
|
|
271
|
+
...edgeAttrs.line,
|
|
272
|
+
...edge.state ? { "data-state": edge.state } : {}
|
|
273
|
+
} }
|
|
274
|
+
});
|
|
275
|
+
const graph = new Graph({
|
|
276
|
+
container,
|
|
277
|
+
grid: false,
|
|
278
|
+
connecting: {
|
|
279
|
+
connector: "smooth",
|
|
280
|
+
allowBlank: false,
|
|
281
|
+
allowLoop: false,
|
|
282
|
+
allowNode: false,
|
|
283
|
+
allowMulti: "withPort",
|
|
284
|
+
validateConnection({ sourceCell, sourcePort, targetCell, targetPort }) {
|
|
285
|
+
if (!sourceCell || !targetCell || !sourcePort || !targetPort) return false;
|
|
286
|
+
const dirOf = (cellId, portId) => store.getNode(cellId)?.ports.find((p) => p.id === portId)?.dir;
|
|
287
|
+
return dirOf(sourceCell.id, sourcePort) === "out" && dirOf(targetCell.id, targetPort) === "in";
|
|
288
|
+
},
|
|
289
|
+
createEdge: () => graph.createEdge({ attrs: edgeAttrs })
|
|
290
|
+
},
|
|
291
|
+
mousewheel: {
|
|
292
|
+
enabled: true,
|
|
293
|
+
factor: 1.1,
|
|
294
|
+
minScale: .25,
|
|
295
|
+
maxScale: 4,
|
|
296
|
+
zoomAtMousePosition: true
|
|
297
|
+
},
|
|
298
|
+
panning: { enabled: true }
|
|
299
|
+
});
|
|
300
|
+
canvasContexts.set(graph, {
|
|
301
|
+
store,
|
|
302
|
+
cleanups,
|
|
303
|
+
renderNode: options.renderNode
|
|
304
|
+
});
|
|
305
|
+
container.dataset.scope = "workflow";
|
|
306
|
+
container.dataset.part = "canvas";
|
|
307
|
+
container.style.setProperty("--bs-workflow-handle", `${HANDLE}px`);
|
|
308
|
+
graph.use(new Selection({
|
|
309
|
+
enabled: true,
|
|
310
|
+
rubberband: true,
|
|
311
|
+
modifiers: ["shift"],
|
|
312
|
+
multiple: true
|
|
313
|
+
}));
|
|
314
|
+
graph.use(new History({
|
|
315
|
+
enabled: true,
|
|
316
|
+
beforeAddCommand: (event, args) => {
|
|
317
|
+
if (event !== "cell:change:*") return true;
|
|
318
|
+
const key = args.key;
|
|
319
|
+
return key !== "data" && key !== "attrs";
|
|
320
|
+
}
|
|
321
|
+
}));
|
|
322
|
+
const keyboard = new Keyboard({ enabled: true });
|
|
323
|
+
graph.use(keyboard);
|
|
324
|
+
keyboard.bindKey(["ctrl+z", "meta+z"], () => graph.undo());
|
|
325
|
+
keyboard.bindKey(["ctrl+shift+z", "meta+shift+z"], () => graph.redo());
|
|
326
|
+
keyboard.bindKey(["delete", "backspace"], () => {
|
|
327
|
+
graph.removeCells(graph.getSelectedCells());
|
|
328
|
+
});
|
|
329
|
+
if (options.minimap) {
|
|
330
|
+
options.minimap.container.dataset.scope = "workflow";
|
|
331
|
+
options.minimap.container.dataset.part = "minimap";
|
|
332
|
+
graph.use(new MiniMap({
|
|
333
|
+
container: options.minimap.container,
|
|
334
|
+
width: 200,
|
|
335
|
+
height: 140,
|
|
336
|
+
padding: 8
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
options.onReady?.(graph);
|
|
340
|
+
const followHost = () => graph.resize(container.clientWidth, container.clientHeight);
|
|
341
|
+
followHost();
|
|
342
|
+
const hostResize = new ResizeObserver(followHost);
|
|
343
|
+
hostResize.observe(container);
|
|
344
|
+
for (const node of store.getGraph().nodes) graph.addNode(toNodeConfig(node));
|
|
345
|
+
for (const edge of store.getGraph().edges) graph.addEdge(toEdgeConfig(edge));
|
|
346
|
+
let replaying = false;
|
|
347
|
+
const guard = (fn) => {
|
|
348
|
+
if (replaying) return;
|
|
349
|
+
store.asSource("canvas", fn);
|
|
350
|
+
};
|
|
351
|
+
graph.on("node:moved", ({ node }) => guard(() => store.moveNode(node.id, node.getPosition())));
|
|
352
|
+
graph.on("node:change:position", ({ node, options }) => {
|
|
353
|
+
if (options?.ui) return;
|
|
354
|
+
guard(() => store.moveNode(node.id, node.getPosition()));
|
|
355
|
+
});
|
|
356
|
+
graph.on("node:removed", ({ node }) => {
|
|
357
|
+
cleanups.get(node.id)?.();
|
|
358
|
+
cleanups.delete(node.id);
|
|
359
|
+
guard(() => store.removeNode(node.id));
|
|
360
|
+
});
|
|
361
|
+
graph.on("node:added", ({ node }) => guard(() => {
|
|
362
|
+
const { workflowType, workflowState, ...rest } = node.getData() ?? {};
|
|
363
|
+
store.addNode({
|
|
364
|
+
id: node.id,
|
|
365
|
+
type: workflowType ?? "unknown",
|
|
366
|
+
position: node.getPosition(),
|
|
367
|
+
ports: node.getPorts().map((p) => ({
|
|
368
|
+
id: p.id ?? "",
|
|
369
|
+
dir: String(p.group ?? "").startsWith("out") ? "out" : "in"
|
|
370
|
+
})),
|
|
371
|
+
state: workflowState,
|
|
372
|
+
data: rest
|
|
373
|
+
});
|
|
374
|
+
}));
|
|
375
|
+
graph.on("edge:connected", ({ edge, isNew }) => {
|
|
376
|
+
if (!isNew) return;
|
|
377
|
+
guard(() => {
|
|
378
|
+
store.connect({
|
|
379
|
+
id: edge.id,
|
|
380
|
+
source: {
|
|
381
|
+
node: edge.getSourceCellId(),
|
|
382
|
+
port: edge.getSourcePortId() ?? ""
|
|
383
|
+
},
|
|
384
|
+
target: {
|
|
385
|
+
node: edge.getTargetCellId(),
|
|
386
|
+
port: edge.getTargetPortId() ?? ""
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
graph.on("edge:removed", ({ edge }) => guard(() => store.disconnect(edge.id)));
|
|
392
|
+
graph.on("edge:added", ({ edge }) => guard(() => {
|
|
393
|
+
const sourceNode = edge.getSourceCellId();
|
|
394
|
+
const targetNode = edge.getTargetCellId();
|
|
395
|
+
const sourcePort = edge.getSourcePortId();
|
|
396
|
+
const targetPort = edge.getTargetPortId();
|
|
397
|
+
if (!sourceNode || !targetNode || !sourcePort || !targetPort) return;
|
|
398
|
+
store.connect({
|
|
399
|
+
id: edge.id,
|
|
400
|
+
source: {
|
|
401
|
+
node: sourceNode,
|
|
402
|
+
port: sourcePort
|
|
403
|
+
},
|
|
404
|
+
target: {
|
|
405
|
+
node: targetNode,
|
|
406
|
+
port: targetPort
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}));
|
|
410
|
+
const apply = (change) => {
|
|
411
|
+
replaying = true;
|
|
412
|
+
try {
|
|
413
|
+
switch (change.kind) {
|
|
414
|
+
case "node:add":
|
|
415
|
+
graph.addNode(toNodeConfig(change.node));
|
|
416
|
+
break;
|
|
417
|
+
case "node:remove":
|
|
418
|
+
graph.removeCell(change.id);
|
|
419
|
+
break;
|
|
420
|
+
case "node:move": {
|
|
421
|
+
const cell = graph.getCellById(change.id);
|
|
422
|
+
if (cell?.isNode()) cell.position(change.position.x, change.position.y);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case "node:state":
|
|
426
|
+
graph.getCellById(change.id)?.setData({ workflowState: change.state });
|
|
427
|
+
break;
|
|
428
|
+
case "node:data":
|
|
429
|
+
graph.getCellById(change.id)?.setData({ ...change.patch });
|
|
430
|
+
break;
|
|
431
|
+
case "edge:connect":
|
|
432
|
+
graph.addEdge(toEdgeConfig(change.edge));
|
|
433
|
+
break;
|
|
434
|
+
case "edge:state":
|
|
435
|
+
graph.getCellById(change.id)?.attr("line/data-state", change.state);
|
|
436
|
+
break;
|
|
437
|
+
case "edge:remove": graph.removeCell(change.id);
|
|
438
|
+
}
|
|
439
|
+
} finally {
|
|
440
|
+
replaying = false;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const unsubscribe = store.subscribe((change, source) => {
|
|
444
|
+
if (source === "canvas") return;
|
|
445
|
+
apply(change);
|
|
446
|
+
});
|
|
447
|
+
let elkLoader;
|
|
448
|
+
let layoutRun = Promise.resolve();
|
|
449
|
+
const inferDirection = () => {
|
|
450
|
+
const sideOf = (nodeId, portId, dir) => {
|
|
451
|
+
const node = store.getNode(nodeId);
|
|
452
|
+
if (!node) return dir === "in" ? "top" : "bottom";
|
|
453
|
+
let ordinal = -1;
|
|
454
|
+
let found = -1;
|
|
455
|
+
for (const port of node.ports) {
|
|
456
|
+
if (port.dir !== dir) continue;
|
|
457
|
+
ordinal++;
|
|
458
|
+
if (port.id === portId) {
|
|
459
|
+
found = ordinal;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (found < 0) return dir === "in" ? "top" : "bottom";
|
|
464
|
+
const first = found % 2 === 0;
|
|
465
|
+
if (dir === "in") return first ? "top" : "left";
|
|
466
|
+
return first ? "bottom" : "right";
|
|
467
|
+
};
|
|
468
|
+
let horizontal = 0;
|
|
469
|
+
let vertical = 0;
|
|
470
|
+
for (const edge of store.getGraph().edges) {
|
|
471
|
+
const out = sideOf(edge.source.node, edge.source.port, "out");
|
|
472
|
+
const into = sideOf(edge.target.node, edge.target.port, "in");
|
|
473
|
+
if (out === "right" || into === "left") horizontal++;
|
|
474
|
+
else vertical++;
|
|
475
|
+
}
|
|
476
|
+
return horizontal > vertical ? "LR" : "TB";
|
|
477
|
+
};
|
|
478
|
+
const doLayout = async (options) => {
|
|
479
|
+
const direction = options?.direction ?? inferDirection();
|
|
480
|
+
const rankSep = options?.rankSep ?? 80;
|
|
481
|
+
const nodeSep = options?.nodeSep ?? 40;
|
|
482
|
+
elkLoader ??= import("elkjs").then(({ default: ELK }) => new ELK());
|
|
483
|
+
const elk = await elkLoader;
|
|
484
|
+
const { nodes, edges } = store.getGraph();
|
|
485
|
+
const result = await elk.layout({
|
|
486
|
+
id: "workflow",
|
|
487
|
+
layoutOptions: {
|
|
488
|
+
"elk.algorithm": "layered",
|
|
489
|
+
"elk.direction": {
|
|
490
|
+
TB: "DOWN",
|
|
491
|
+
LR: "RIGHT",
|
|
492
|
+
BT: "UP",
|
|
493
|
+
RL: "LEFT"
|
|
494
|
+
}[direction],
|
|
495
|
+
"elk.spacing.nodeNode": String(nodeSep),
|
|
496
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": String(rankSep)
|
|
497
|
+
},
|
|
498
|
+
children: nodes.map((node) => ({
|
|
499
|
+
id: node.id,
|
|
500
|
+
width: NODE_WIDTH,
|
|
501
|
+
height: NODE_HEIGHT
|
|
502
|
+
})),
|
|
503
|
+
edges: edges.map((edge) => ({
|
|
504
|
+
id: edge.id,
|
|
505
|
+
sources: [edge.source.node],
|
|
506
|
+
targets: [edge.target.node]
|
|
507
|
+
}))
|
|
508
|
+
});
|
|
509
|
+
if (destroyed) return;
|
|
510
|
+
graph.startBatch("workflow-layout");
|
|
511
|
+
try {
|
|
512
|
+
for (const child of result.children ?? []) {
|
|
513
|
+
if (child.x == null || child.y == null) continue;
|
|
514
|
+
store.moveNode(child.id, {
|
|
515
|
+
x: child.x,
|
|
516
|
+
y: child.y
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
} finally {
|
|
520
|
+
graph.stopBatch("workflow-layout");
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
const layout = (options) => {
|
|
524
|
+
const run = layoutRun.catch(() => {}).then(() => doLayout(options));
|
|
525
|
+
layoutRun = run;
|
|
526
|
+
return run;
|
|
527
|
+
};
|
|
528
|
+
let destroyed = false;
|
|
529
|
+
const canvas = {
|
|
530
|
+
store,
|
|
531
|
+
graph,
|
|
532
|
+
zoomBy(factor) {
|
|
533
|
+
graph.zoomTo(graph.zoom() * factor);
|
|
534
|
+
},
|
|
535
|
+
fitView(options) {
|
|
536
|
+
graph.zoomToFit({
|
|
537
|
+
padding: 24,
|
|
538
|
+
maxScale: 1,
|
|
539
|
+
...options
|
|
540
|
+
});
|
|
541
|
+
},
|
|
542
|
+
layout,
|
|
543
|
+
destroy() {
|
|
544
|
+
if (destroyed) return;
|
|
545
|
+
destroyed = true;
|
|
546
|
+
live.delete(container);
|
|
547
|
+
replaying = true;
|
|
548
|
+
unsubscribe();
|
|
549
|
+
hostResize.disconnect();
|
|
550
|
+
graph.dispose();
|
|
551
|
+
for (const child of Array.from(container.children)) if (child.classList.value.startsWith("x6-")) child.remove();
|
|
552
|
+
canvasContexts.delete(graph);
|
|
553
|
+
for (const dispose of cleanups.values()) dispose();
|
|
554
|
+
cleanups.clear();
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
live.set(container, canvas);
|
|
558
|
+
return canvas;
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
export { WORKFLOW_GRAPH_VERSION, createWorkflowCanvas, createWorkflowStore, deserializeWorkflowGraph, serializeWorkflowGraph };
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bysages/workflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-ready workflow graphs for By Sages Elements — a headless graph protocol dressed in the paper-and-ink system, rendered by a swappable canvas engine",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"design-system",
|
|
8
|
+
"design-tokens",
|
|
9
|
+
"diagram",
|
|
10
|
+
"flowchart",
|
|
11
|
+
"graph",
|
|
12
|
+
"theming",
|
|
13
|
+
"ui",
|
|
14
|
+
"workflow"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/bysages/elements#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/bysages/elements/issues"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": {
|
|
22
|
+
"name": "By Sages",
|
|
23
|
+
"email": "contact@bysages.com",
|
|
24
|
+
"url": "https://www.bysages.com/"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/bysages/elements"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"type": "module",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"import": "./dist/index.mjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@antv/x6": "3.1.8",
|
|
42
|
+
"elkjs": "0.12.0",
|
|
43
|
+
"@bysages/core": "0.1.2"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/react": "19.3.0",
|
|
47
|
+
"@types/react-dom": "19.3.0",
|
|
48
|
+
"react": "19.3.0",
|
|
49
|
+
"react-dom": "19.3.0",
|
|
50
|
+
"vue": "3.5.42",
|
|
51
|
+
"@bysages/vue": "0.1.1",
|
|
52
|
+
"@bysages/react": "0.1.1"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"dev": "basis build --stub",
|
|
56
|
+
"build": "vp pack",
|
|
57
|
+
"release": "basis publish"
|
|
58
|
+
}
|
|
59
|
+
}
|