@bysages/workflow 0.2.2 → 0.3.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.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Graph } from "@antv/x6";
2
+ import { LayoutOptions } from "elkjs/lib/elk.bundled.js";
2
3
  //#region src/types.d.ts
3
4
  /** The engine-independent facts of a workflow graph. The canvas adapter
4
5
  * reads them; the execution backend writes states back through the
@@ -142,6 +143,29 @@ export declare const deserializeWorkflowGraph: (serialized: {
142
143
  graph: WorkflowGraph;
143
144
  }) => WorkflowGraph;
144
145
  //#endregion
146
+ //#region src/mindmap.d.ts
147
+ /** A section''s payload — a ```json fence placed under its heading rides
148
+ * along parsed into data, so the outline carries both the tree and the
149
+ * machine-readable facts of each branch. */
150
+ export interface MindmapNodeData {
151
+ label: string;
152
+ payload?: unknown;
153
+ }
154
+ /** Turns a markdown outline into a workflow graph: every heading and
155
+ * every list item becomes a node, the nesting becomes edges, and a json
156
+ * fence deepens its nearest section with parsed payload. An h1 names the
157
+ * root; without one the first entry grows into the trunk. Positions all
158
+ * start at the origin — the canvas sweep (ELK''s mrtree) hands out the
159
+ * real geometry. Every node keeps both handles, so any branch can grow
160
+ * on the canvas later. */
161
+ export declare function outlineToGraph(markdown: string): WorkflowGraph;
162
+ /** Folds a tree-shaped graph back into the outline the parser accepts:
163
+ * the root as the h1, first-level branches as h2s, deeper nodes as
164
+ * nested list items, and a payload re-entering as a json fence under
165
+ * its section. Extra parents (a node several edges reach) are ignored —
166
+ * the first edge wins, so the fold stays a tree. */
167
+ export declare function graphToOutline(graph: WorkflowGraph): string;
168
+ //#endregion
145
169
  //#region src/x6-canvas.d.ts
146
170
  /** How the layered engine should sweep the graph. Left out, the
147
171
  * direction is read off the graph: edges drawn between side handles
@@ -150,6 +174,11 @@ export declare const deserializeWorkflowGraph: (serialized: {
150
174
  * handles on the bottom, in handles on top. */
151
175
  export interface WorkflowAutoLayoutOptions {
152
176
  direction?: "TB" | "LR" | "BT" | "RL";
177
+ /** The ELK algorithm id — layered by default; org.eclipse.elk.mrtree
178
+ * sweeps the same graph as a tree. The vocabulary belongs to ELK's own
179
+ * option table, so the field reads its type from there instead of
180
+ * closing over a hand-written union. */
181
+ algorithm?: LayoutOptions["elk.algorithm"];
153
182
  /** The gap between layers, in px. */
154
183
  rankSep?: number;
155
184
  /** The gap between siblings on one layer, in px. */
@@ -167,6 +196,10 @@ export interface WorkflowCanvasOptions {
167
196
  minimap?: {
168
197
  container: HTMLElement;
169
198
  };
199
+ /** Which way the flow reads — the edge the first in/out handles sit
200
+ * on follows it, and an unspecified layout direction inherits it.
201
+ * Vertical stays the default: every existing graph keeps its anatomy. */
202
+ orientation?: "TB" | "LR";
170
203
  }
171
204
  /** Viewport options for fitView. X6's own fit options are accepted in
172
205
  * full through the graph escape hatch; these cover the calls a toolbar
@@ -186,7 +219,7 @@ export interface WorkflowCanvas {
186
219
  /** Frames the whole graph. Defaults to what a toolbar wants: a
187
220
  * breathing margin and no magnifying past 100%. */
188
221
  fitView(options?: WorkflowFitViewOptions): void;
189
- /** Sweeps the graph with the ELK layered algorithm and writes every
222
+ /** Sweeps the graph with the chosen ELK algorithm and writes every
190
223
  * node's new position back through the store. Async: the layout
191
224
  * engine is heavy, so it's pulled in only when this runs. */
192
225
  layout(options?: WorkflowAutoLayoutOptions): Promise<void>;
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseMarkdown } from "@tanstack/markdown";
1
2
  import { Graph, History, Keyboard, MiniMap, Selection, Shape } from "@antv/x6";
2
3
  import { injectComponentStyle } from "@bysages/core";
3
4
  //#region src/store.ts
@@ -158,6 +159,143 @@ const deserializeWorkflowGraph = (serialized) => {
158
159
  return serialized.graph;
159
160
  };
160
161
  //#endregion
162
+ //#region src/mindmap.ts
163
+ const inlineText = (nodes) => nodes.map((node) => {
164
+ if (node.type === "text" || node.type === "inlineCode") return node.value;
165
+ if (node.type === "break") return " ";
166
+ if ("children" in node) return inlineText(node.children);
167
+ if (node.type === "footnoteReference") return node.id;
168
+ return "";
169
+ }).join("");
170
+ const blockText = (block) => {
171
+ if (block.type === "heading" || block.type === "paragraph") return inlineText(block.children);
172
+ return "";
173
+ };
174
+ /** Turns a markdown outline into a workflow graph: every heading and
175
+ * every list item becomes a node, the nesting becomes edges, and a json
176
+ * fence deepens its nearest section with parsed payload. An h1 names the
177
+ * root; without one the first entry grows into the trunk. Positions all
178
+ * start at the origin — the canvas sweep (ELK''s mrtree) hands out the
179
+ * real geometry. Every node keeps both handles, so any branch can grow
180
+ * on the canvas later. */
181
+ function outlineToGraph(markdown) {
182
+ const document = parseMarkdown(markdown);
183
+ const nodes = [];
184
+ const edges = [];
185
+ const stack = [];
186
+ let counter = 0;
187
+ const attachPayload = (nodeId, block) => {
188
+ const node = nodes.find((candidate) => candidate.id === nodeId);
189
+ if (!node) return;
190
+ try {
191
+ node.data.payload = JSON.parse(block.value);
192
+ } catch {
193
+ node.data.payload = block.value;
194
+ }
195
+ };
196
+ const grow = (label, depth) => {
197
+ counter += 1;
198
+ const id = `m${counter}`;
199
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) stack.pop();
200
+ nodes.push({
201
+ id,
202
+ type: "mindmap",
203
+ position: {
204
+ x: 0,
205
+ y: 0
206
+ },
207
+ ports: stack.length === 0 ? [{
208
+ id: "out",
209
+ dir: "out"
210
+ }] : [{
211
+ id: "in",
212
+ dir: "in"
213
+ }, {
214
+ id: "out",
215
+ dir: "out"
216
+ }],
217
+ data: { label }
218
+ });
219
+ if (stack.length > 0) edges.push({
220
+ id: `e${counter}`,
221
+ source: {
222
+ node: stack[stack.length - 1].nodeId,
223
+ port: "out"
224
+ },
225
+ target: {
226
+ node: id,
227
+ port: "in"
228
+ }
229
+ });
230
+ stack.push({
231
+ depth,
232
+ nodeId: id
233
+ });
234
+ return id;
235
+ };
236
+ const walkList = (list, depth) => {
237
+ for (const item of list.items) {
238
+ const lead = item.children[0];
239
+ const id = grow(lead ? blockText(lead) || "—" : "—", depth);
240
+ for (const child of item.children) if (child.type === "list") walkList(child, depth + 1);
241
+ else if (child.type === "code" && child.lang === "json") attachPayload(id, child);
242
+ }
243
+ };
244
+ const headings = document.children.filter((block) => block.type === "heading");
245
+ if (headings[0]?.depth === 1) grow(inlineText(headings[0].children), 0);
246
+ for (const block of document.children) if (block.type === "heading") {
247
+ if (!(block === headings[0] && block.depth === 1)) grow(inlineText(block.children), block.depth - 1);
248
+ } else if (block.type === "list") walkList(block, stack[stack.length - 1].depth + 1);
249
+ else if (block.type === "code" && block.lang === "json" && stack.length > 0) attachPayload(stack[stack.length - 1].nodeId, block);
250
+ return {
251
+ nodes,
252
+ edges
253
+ };
254
+ }
255
+ const FENCE = "```";
256
+ /** Folds a tree-shaped graph back into the outline the parser accepts:
257
+ * the root as the h1, first-level branches as h2s, deeper nodes as
258
+ * nested list items, and a payload re-entering as a json fence under
259
+ * its section. Extra parents (a node several edges reach) are ignored —
260
+ * the first edge wins, so the fold stays a tree. */
261
+ function graphToOutline(graph) {
262
+ const dataOf = (id) => {
263
+ const node = graph.nodes.find((candidate) => candidate.id === id);
264
+ return {
265
+ label: typeof node?.data.label === "string" ? node.data.label : id,
266
+ payload: node?.data.payload
267
+ };
268
+ };
269
+ const parentOf = /* @__PURE__ */ new Map();
270
+ const childrenOf = /* @__PURE__ */ new Map();
271
+ for (const edge of graph.edges) {
272
+ if (parentOf.has(edge.target.node)) continue;
273
+ parentOf.set(edge.target.node, edge.source.node);
274
+ const siblings = childrenOf.get(edge.source.node) ?? [];
275
+ siblings.push(edge.target.node);
276
+ childrenOf.set(edge.source.node, siblings);
277
+ }
278
+ const lines = [];
279
+ const emitPayload = (indent, payload) => {
280
+ lines.push("");
281
+ const pad = " ".repeat(indent);
282
+ lines.push(pad + "```json");
283
+ for (const line of JSON.stringify(payload, null, 2).split("\n")) lines.push(pad + line);
284
+ lines.push(pad + FENCE, "");
285
+ };
286
+ const walk = (id, depth) => {
287
+ const { label, payload } = dataOf(id);
288
+ if (depth <= 1) {
289
+ if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
290
+ lines.push(depth === 0 ? `# ${label}` : `## ${label}`, "");
291
+ } else lines.push(`${" ".repeat((depth - 2) * 2)}- ${label}`);
292
+ if (payload !== void 0) emitPayload(depth >= 2 ? (depth - 2) * 2 + 2 : 0, payload);
293
+ for (const child of childrenOf.get(id) ?? []) walk(child, depth + 1);
294
+ };
295
+ for (const node of graph.nodes) if (!parentOf.has(node.id)) walk(node.id, 0);
296
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
297
+ }
298
+ //#endregion
161
299
  //#region src/x6-canvas.ts
162
300
  const NODE_WIDTH = 256;
163
301
  const NODE_HEIGHT = 88;
@@ -221,7 +359,12 @@ function createWorkflowCanvas(container, source, options = {}) {
221
359
  "data-dir": dir
222
360
  } }
223
361
  });
224
- const portGroups = {
362
+ const portGroups = options.orientation === "LR" ? {
363
+ in: sideGroup("left", "in"),
364
+ "in-alt": sideGroup("top", "in"),
365
+ out: sideGroup("right", "out"),
366
+ "out-alt": sideGroup("bottom", "out")
367
+ } : {
225
368
  in: sideGroup("top", "in"),
226
369
  "in-alt": sideGroup("left", "in"),
227
370
  out: sideGroup("bottom", "out"),
@@ -333,11 +476,26 @@ function createWorkflowCanvas(container, source, options = {}) {
333
476
  container: options.minimap.container,
334
477
  width: 200,
335
478
  height: 140,
336
- padding: 8
479
+ padding: 8,
480
+ createGraph: (paperOptions) => {
481
+ const paper = new Graph(paperOptions);
482
+ const translate = paper.translate.bind(paper);
483
+ const readTranslate = paper.translate.bind(paper);
484
+ paper.translate = ((tx, ty) => {
485
+ if (tx === void 0 && ty === void 0) return readTranslate();
486
+ return tx !== void 0 && ty !== void 0 && Number.isFinite(tx) && Number.isFinite(ty) ? translate(tx, ty) : paper;
487
+ });
488
+ const resize = paper.resize.bind(paper);
489
+ paper.resize = (width, height) => Number.isFinite(width) && Number.isFinite(height) ? resize(width, height) : paper;
490
+ return paper;
491
+ }
337
492
  }));
338
493
  }
339
494
  options.onReady?.(graph);
340
- const followHost = () => graph.resize(container.clientWidth, container.clientHeight);
495
+ const followHost = () => {
496
+ if (container.clientWidth === 0 || container.clientHeight === 0) return;
497
+ graph.resize(container.clientWidth, container.clientHeight);
498
+ };
341
499
  followHost();
342
500
  const hostResize = new ResizeObserver(followHost);
343
501
  hostResize.observe(container);
@@ -447,6 +605,7 @@ function createWorkflowCanvas(container, source, options = {}) {
447
605
  let elkLoader;
448
606
  let layoutRun = Promise.resolve();
449
607
  const inferDirection = () => {
608
+ if (options.orientation) return options.orientation;
450
609
  const sideOf = (nodeId, portId, dir) => {
451
610
  const node = store.getNode(nodeId);
452
611
  if (!node) return dir === "in" ? "top" : "bottom";
@@ -479,13 +638,14 @@ function createWorkflowCanvas(container, source, options = {}) {
479
638
  const direction = options?.direction ?? inferDirection();
480
639
  const rankSep = options?.rankSep ?? 80;
481
640
  const nodeSep = options?.nodeSep ?? 40;
641
+ const algorithm = options?.algorithm ?? "layered";
482
642
  elkLoader ??= import("elkjs/lib/elk.bundled.js").then(({ default: ELK }) => new ELK());
483
643
  const elk = await elkLoader;
484
644
  const { nodes, edges } = store.getGraph();
485
645
  const result = await elk.layout({
486
646
  id: "workflow",
487
647
  layoutOptions: {
488
- "elk.algorithm": "layered",
648
+ "elk.algorithm": algorithm,
489
649
  "elk.direction": {
490
650
  TB: "DOWN",
491
651
  LR: "RIGHT",
@@ -558,4 +718,4 @@ function createWorkflowCanvas(container, source, options = {}) {
558
718
  return canvas;
559
719
  }
560
720
  //#endregion
561
- export { WORKFLOW_GRAPH_VERSION, createWorkflowCanvas, createWorkflowStore, deserializeWorkflowGraph, serializeWorkflowGraph };
721
+ export { WORKFLOW_GRAPH_VERSION, createWorkflowCanvas, createWorkflowStore, deserializeWorkflowGraph, graphToOutline, outlineToGraph, serializeWorkflowGraph };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bysages/workflow",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
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
5
  "keywords": [
6
6
  "ai",
@@ -39,8 +39,9 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@antv/x6": "3.1.8",
42
+ "@tanstack/markdown": "1.0.0",
42
43
  "elkjs": "0.12.0",
43
- "@bysages/core": "0.2.2"
44
+ "@bysages/core": "0.3.0"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@types/react": "19.3.0",
@@ -48,8 +49,8 @@
48
49
  "react": "19.3.0",
49
50
  "react-dom": "19.3.0",
50
51
  "vue": "3.5.43",
51
- "@bysages/react": "0.2.2",
52
- "@bysages/vue": "0.2.2"
52
+ "@bysages/react": "0.3.0",
53
+ "@bysages/vue": "0.3.0"
53
54
  },
54
55
  "scripts": {
55
56
  "dev": "basis build --stub",