devglide 0.1.1

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.
Files changed (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/bin/claude-md-template.js +94 -0
  4. package/bin/devglide.js +387 -0
  5. package/package.json +85 -0
  6. package/pnpm-workspace.yaml +3 -0
  7. package/src/apps/coder/.turbo/turbo-lint.log +5 -0
  8. package/src/apps/coder/package.json +16 -0
  9. package/src/apps/coder/public/favicon.svg +7 -0
  10. package/src/apps/coder/public/page.css +275 -0
  11. package/src/apps/coder/public/page.js +528 -0
  12. package/src/apps/coder/server.js +3 -0
  13. package/src/apps/documentation/public/page.css +597 -0
  14. package/src/apps/documentation/public/page.js +609 -0
  15. package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
  16. package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
  17. package/src/apps/kanban/package.json +32 -0
  18. package/src/apps/kanban/public/favicon.svg +7 -0
  19. package/src/apps/kanban/public/page.css +1010 -0
  20. package/src/apps/kanban/public/page.js +1730 -0
  21. package/src/apps/kanban/public/vendor/marked.min.js +6 -0
  22. package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
  23. package/src/apps/kanban/src/db.ts +319 -0
  24. package/src/apps/kanban/src/index.ts +14 -0
  25. package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
  26. package/src/apps/kanban/src/mcp-helpers.ts +60 -0
  27. package/src/apps/kanban/src/mcp.ts +59 -0
  28. package/src/apps/kanban/src/routes/attachments.ts +161 -0
  29. package/src/apps/kanban/src/routes/features.ts +233 -0
  30. package/src/apps/kanban/src/routes/issues.ts +373 -0
  31. package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
  32. package/src/apps/kanban/src/tools/item-tools.ts +307 -0
  33. package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
  34. package/src/apps/kanban/tsconfig.check.json +9 -0
  35. package/src/apps/kanban/tsconfig.json +9 -0
  36. package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
  37. package/src/apps/keymap/package.json +16 -0
  38. package/src/apps/keymap/public/page.css +275 -0
  39. package/src/apps/keymap/public/page.js +294 -0
  40. package/src/apps/keymap/server.js +25 -0
  41. package/src/apps/log/.turbo/turbo-build.log +5 -0
  42. package/src/apps/log/.turbo/turbo-lint.log +45 -0
  43. package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
  44. package/src/apps/log/node_modules/.bin/tsc +21 -0
  45. package/src/apps/log/node_modules/.bin/tsserver +21 -0
  46. package/src/apps/log/node_modules/.bin/tsx +21 -0
  47. package/src/apps/log/package.json +36 -0
  48. package/src/apps/log/public/console-sniffer.js +221 -0
  49. package/src/apps/log/public/favicon.svg +7 -0
  50. package/src/apps/log/public/page.css +322 -0
  51. package/src/apps/log/public/page.js +463 -0
  52. package/src/apps/log/src/index.ts +9 -0
  53. package/src/apps/log/src/mcp.ts +122 -0
  54. package/src/apps/log/src/routes/log.ts +333 -0
  55. package/src/apps/log/src/routes/status.ts +25 -0
  56. package/src/apps/log/src/server-sniffer.ts +118 -0
  57. package/src/apps/log/src/services/file-patterns.ts +39 -0
  58. package/src/apps/log/src/services/file-tailer.ts +228 -0
  59. package/src/apps/log/src/services/line-parser.ts +94 -0
  60. package/src/apps/log/src/services/log-writer.ts +39 -0
  61. package/src/apps/log/tsconfig.json +8 -0
  62. package/src/apps/prompts/.turbo/turbo-build.log +5 -0
  63. package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
  64. package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
  65. package/src/apps/prompts/mcp.ts +175 -0
  66. package/src/apps/prompts/node_modules/.bin/tsc +21 -0
  67. package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
  68. package/src/apps/prompts/node_modules/.bin/tsx +21 -0
  69. package/src/apps/prompts/package.json +25 -0
  70. package/src/apps/prompts/public/page.css +315 -0
  71. package/src/apps/prompts/public/page.js +541 -0
  72. package/src/apps/prompts/services/prompt-store.ts +212 -0
  73. package/src/apps/prompts/src/index.ts +9 -0
  74. package/src/apps/prompts/tsconfig.json +8 -0
  75. package/src/apps/prompts/types.ts +27 -0
  76. package/src/apps/shell/.turbo/turbo-build.log +5 -0
  77. package/src/apps/shell/.turbo/turbo-lint.log +34 -0
  78. package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
  79. package/src/apps/shell/package.json +35 -0
  80. package/src/apps/shell/public/favicon.svg +7 -0
  81. package/src/apps/shell/public/page.css +407 -0
  82. package/src/apps/shell/public/page.js +1577 -0
  83. package/src/apps/shell/src/index.ts +150 -0
  84. package/src/apps/shell/src/mcp.ts +398 -0
  85. package/src/apps/shell/src/shell-types.ts +41 -0
  86. package/src/apps/shell/tsconfig.json +8 -0
  87. package/src/apps/test/.turbo/turbo-build.log +5 -0
  88. package/src/apps/test/.turbo/turbo-lint.log +27 -0
  89. package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
  90. package/src/apps/test/node_modules/.bin/tsc +21 -0
  91. package/src/apps/test/node_modules/.bin/tsserver +21 -0
  92. package/src/apps/test/node_modules/.bin/tsx +21 -0
  93. package/src/apps/test/node_modules/.bin/uuid +21 -0
  94. package/src/apps/test/package.json +35 -0
  95. package/src/apps/test/public/favicon.svg +7 -0
  96. package/src/apps/test/public/page.css +499 -0
  97. package/src/apps/test/public/page.js +417 -0
  98. package/src/apps/test/public/scenario-runner.js +450 -0
  99. package/src/apps/test/src/index.ts +9 -0
  100. package/src/apps/test/src/mcp.ts +192 -0
  101. package/src/apps/test/src/routes/trigger.ts +285 -0
  102. package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
  103. package/src/apps/test/src/services/scenario-manager.ts +361 -0
  104. package/src/apps/test/src/services/scenario-store.ts +145 -0
  105. package/src/apps/test/tsconfig.json +8 -0
  106. package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
  107. package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
  108. package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
  109. package/src/apps/vocabulary/mcp.ts +173 -0
  110. package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
  111. package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
  112. package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
  113. package/src/apps/vocabulary/package.json +25 -0
  114. package/src/apps/vocabulary/public/page.css +247 -0
  115. package/src/apps/vocabulary/public/page.js +444 -0
  116. package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
  117. package/src/apps/vocabulary/src/index.ts +10 -0
  118. package/src/apps/vocabulary/tsconfig.json +8 -0
  119. package/src/apps/vocabulary/types.ts +22 -0
  120. package/src/apps/voice/.turbo/turbo-build.log +5 -0
  121. package/src/apps/voice/.turbo/turbo-lint.log +43 -0
  122. package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
  123. package/src/apps/voice/node_modules/.bin/openai +21 -0
  124. package/src/apps/voice/node_modules/.bin/tsc +21 -0
  125. package/src/apps/voice/node_modules/.bin/tsserver +21 -0
  126. package/src/apps/voice/node_modules/.bin/tsx +21 -0
  127. package/src/apps/voice/package.json +35 -0
  128. package/src/apps/voice/public/favicon.svg +7 -0
  129. package/src/apps/voice/public/page.css +388 -0
  130. package/src/apps/voice/public/page.js +718 -0
  131. package/src/apps/voice/src/index.ts +10 -0
  132. package/src/apps/voice/src/mcp.ts +70 -0
  133. package/src/apps/voice/src/providers/index.ts +85 -0
  134. package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
  135. package/src/apps/voice/src/providers/types.ts +27 -0
  136. package/src/apps/voice/src/routes/config.ts +118 -0
  137. package/src/apps/voice/src/routes/transcribe.ts +90 -0
  138. package/src/apps/voice/src/services/config-store.ts +129 -0
  139. package/src/apps/voice/src/services/stats.ts +108 -0
  140. package/src/apps/voice/src/transcribe.ts +11 -0
  141. package/src/apps/voice/src/utils/mime.ts +16 -0
  142. package/src/apps/voice/tsconfig.json +8 -0
  143. package/src/apps/workflow/.turbo/turbo-build.log +5 -0
  144. package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
  145. package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
  146. package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
  147. package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
  148. package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
  149. package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
  150. package/src/apps/workflow/engine/executors/index.ts +28 -0
  151. package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
  152. package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
  153. package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
  154. package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
  155. package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
  156. package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
  157. package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
  158. package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
  159. package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
  160. package/src/apps/workflow/engine/graph-runner.ts +438 -0
  161. package/src/apps/workflow/engine/node-executor.ts +104 -0
  162. package/src/apps/workflow/engine/node-registry.ts +15 -0
  163. package/src/apps/workflow/engine/variable-resolver.ts +109 -0
  164. package/src/apps/workflow/mcp.ts +223 -0
  165. package/src/apps/workflow/node_modules/.bin/tsc +21 -0
  166. package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
  167. package/src/apps/workflow/node_modules/.bin/tsx +21 -0
  168. package/src/apps/workflow/package.json +25 -0
  169. package/src/apps/workflow/public/editor/canvas.js +366 -0
  170. package/src/apps/workflow/public/editor/drag-manager.js +326 -0
  171. package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
  172. package/src/apps/workflow/public/editor/history-manager.js +147 -0
  173. package/src/apps/workflow/public/editor/layout-engine.js +159 -0
  174. package/src/apps/workflow/public/editor/node-renderer.js +199 -0
  175. package/src/apps/workflow/public/editor/selection-manager.js +193 -0
  176. package/src/apps/workflow/public/favicon.svg +7 -0
  177. package/src/apps/workflow/public/models/node-types.js +300 -0
  178. package/src/apps/workflow/public/models/workflow-model.js +257 -0
  179. package/src/apps/workflow/public/page.css +406 -0
  180. package/src/apps/workflow/public/page.js +658 -0
  181. package/src/apps/workflow/public/panels/inspector.js +360 -0
  182. package/src/apps/workflow/public/panels/palette.js +106 -0
  183. package/src/apps/workflow/public/panels/run-view.js +275 -0
  184. package/src/apps/workflow/public/panels/toolbar.js +232 -0
  185. package/src/apps/workflow/public/panels/workflow-list.js +237 -0
  186. package/src/apps/workflow/public/state/store.js +47 -0
  187. package/src/apps/workflow/services/custom-node-loader.ts +48 -0
  188. package/src/apps/workflow/services/legacy-converter.ts +72 -0
  189. package/src/apps/workflow/services/run-manager.ts +190 -0
  190. package/src/apps/workflow/services/workflow-store.ts +424 -0
  191. package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
  192. package/src/apps/workflow/services/workflow-validator.ts +98 -0
  193. package/src/apps/workflow/src/index.ts +10 -0
  194. package/src/apps/workflow/templates/ci-pipeline.json +18 -0
  195. package/src/apps/workflow/templates/code-review.json +22 -0
  196. package/src/apps/workflow/templates/kanban-testing.json +24 -0
  197. package/src/apps/workflow/tsconfig.json +8 -0
  198. package/src/apps/workflow/types.ts +268 -0
  199. package/src/packages/auth-middleware.ts +14 -0
  200. package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
  201. package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
  202. package/src/packages/design-tokens/build.js +413 -0
  203. package/src/packages/design-tokens/demo/index.html +1367 -0
  204. package/src/packages/design-tokens/demo/proposition-a.html +717 -0
  205. package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
  206. package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
  207. package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
  208. package/src/packages/design-tokens/dist/tokens.css +345 -0
  209. package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
  210. package/src/packages/design-tokens/dist/tokens.js +386 -0
  211. package/src/packages/design-tokens/package.json +25 -0
  212. package/src/packages/design-tokens/tokens.json +228 -0
  213. package/src/packages/devtools-middleware.ts +22 -0
  214. package/src/packages/eslint-config/index.js +63 -0
  215. package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
  216. package/src/packages/eslint-config/package.json +18 -0
  217. package/src/packages/json-file-store.ts +232 -0
  218. package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
  219. package/src/packages/mcp-utils/dist/index.d.ts +33 -0
  220. package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
  221. package/src/packages/mcp-utils/dist/index.js +126 -0
  222. package/src/packages/mcp-utils/dist/index.js.map +1 -0
  223. package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
  224. package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
  225. package/src/packages/mcp-utils/package.json +32 -0
  226. package/src/packages/mcp-utils/src/index.ts +171 -0
  227. package/src/packages/mcp-utils/tsconfig.json +9 -0
  228. package/src/packages/paths.ts +18 -0
  229. package/src/packages/project-context/index.js +55 -0
  230. package/src/packages/project-context/package.json +13 -0
  231. package/src/packages/project-store.ts +127 -0
  232. package/src/packages/server-sniffer.ts +132 -0
  233. package/src/packages/shared-assets/favicon.svg +7 -0
  234. package/src/packages/shared-assets/keymap-registry.js +512 -0
  235. package/src/packages/shared-assets/logo.svg +6 -0
  236. package/src/packages/shared-assets/package.json +11 -0
  237. package/src/packages/shared-assets/ui-utils.js +48 -0
  238. package/src/packages/shared-assets/voice-widget.d.ts +37 -0
  239. package/src/packages/shared-assets/voice-widget.js +695 -0
  240. package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
  241. package/src/packages/shared-types/dist/index.d.ts +39 -0
  242. package/src/packages/shared-types/dist/index.d.ts.map +1 -0
  243. package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
  244. package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
  245. package/src/packages/shared-types/package.json +25 -0
  246. package/src/packages/shared-types/src/index.ts +41 -0
  247. package/src/packages/shared-types/tsconfig.json +11 -0
  248. package/src/packages/tsconfig/base.json +15 -0
  249. package/src/packages/tsconfig/next.json +14 -0
  250. package/src/packages/tsconfig/node.json +11 -0
  251. package/src/packages/tsconfig/package.json +10 -0
  252. package/turbo.json +25 -0
@@ -0,0 +1,326 @@
1
+ // ── Workflow Editor — Drag & Drop Manager ──────────────────────────────
2
+ // Handles palette drops, node moves, and port-to-port edge creation
3
+ // using the Pointer Events API with setPointerCapture.
4
+
5
+ import { store } from '../state/store.js';
6
+ import { WorkflowModel } from '../models/workflow-model.js';
7
+ import { NODE_TYPES, resolveOutputPorts } from '../models/node-types.js';
8
+
9
+ const GRID_SIZE = 20;
10
+
11
+ let _canvas = null; // Canvas module reference
12
+ let _nodeContainer = null; // World element (nodes)
13
+ let _svgLayer = null; // SVG world group (edges)
14
+ let _snapEnabled = true;
15
+
16
+ // Active drag state
17
+ let _drag = null; // { type: 'node'|'palette'|'edge', ... }
18
+
19
+ // ── Snap helper ─────────────────────────────────────────────────────────
20
+
21
+ function snap(x, y) {
22
+ if (!_snapEnabled) return { x, y };
23
+ return {
24
+ x: Math.round(x / GRID_SIZE) * GRID_SIZE,
25
+ y: Math.round(y / GRID_SIZE) * GRID_SIZE,
26
+ };
27
+ }
28
+
29
+ // ── Palette drag ────────────────────────────────────────────────────────
30
+
31
+ function startPaletteDrag(e, nodeType) {
32
+ const typeDef = NODE_TYPES[nodeType];
33
+ if (!typeDef) return;
34
+
35
+ // Create ghost element
36
+ const ghost = document.createElement('div');
37
+ ghost.className = 'wfb-drag-ghost';
38
+ ghost.textContent = `${typeDef.icon} ${typeDef.label}`;
39
+ ghost.style.cssText = `
40
+ position: fixed;
41
+ left: ${e.clientX - 60}px;
42
+ top: ${e.clientY - 20}px;
43
+ width: 120px;
44
+ padding: var(--df-space-2) var(--df-space-3);
45
+ background: var(--df-color-bg-surface);
46
+ border: 1px solid ${typeDef.color};
47
+ border-left: 4px solid ${typeDef.color};
48
+ font-family: var(--df-font-mono);
49
+ font-size: var(--df-font-size-xs);
50
+ color: var(--df-color-text-primary);
51
+ opacity: 0.85;
52
+ pointer-events: none;
53
+ z-index: 9999;
54
+ white-space: nowrap;
55
+ overflow: hidden;
56
+ text-overflow: ellipsis;
57
+ `;
58
+ document.body.appendChild(ghost);
59
+
60
+ _drag = { type: 'palette', nodeType, ghost, pointerId: e.pointerId };
61
+ }
62
+
63
+ function movePaletteDrag(e) {
64
+ if (_drag?.ghost) {
65
+ _drag.ghost.style.left = `${e.clientX - 60}px`;
66
+ _drag.ghost.style.top = `${e.clientY - 20}px`;
67
+ }
68
+ }
69
+
70
+ function endPaletteDrag(e) {
71
+ if (!_drag || _drag.type !== 'palette') return;
72
+
73
+ // Remove ghost
74
+ _drag.ghost.remove();
75
+
76
+ // Check if dropped over canvas
77
+ if (_canvas) {
78
+ const root = _canvas.getRootElement?.();
79
+ if (root) {
80
+ const rect = root.getBoundingClientRect();
81
+ if (e.clientX >= rect.left && e.clientX <= rect.right &&
82
+ e.clientY >= rect.top && e.clientY <= rect.bottom) {
83
+ const worldPos = _canvas.screenToWorld(e.clientX, e.clientY);
84
+ const snapped = snap(worldPos.x, worldPos.y);
85
+ const typeDef = NODE_TYPES[_drag.nodeType];
86
+ WorkflowModel.addNode(_drag.nodeType, typeDef?.label ?? _drag.nodeType, snapped);
87
+ }
88
+ }
89
+ }
90
+
91
+ _drag = null;
92
+ }
93
+
94
+ // ── Node move ───────────────────────────────────────────────────────────
95
+
96
+ function startNodeMove(e, nodeId, nodeEl) {
97
+ const wf = store.get('workflow');
98
+ const node = wf?.nodes.find(n => n.id === nodeId);
99
+ if (!node) return;
100
+
101
+ const zoom = store.get('zoom');
102
+ const selectedIds = store.get('selectedNodeIds');
103
+
104
+ // If this node is not selected, select only it (unless shift is held)
105
+ if (!selectedIds.has(nodeId) && !e.shiftKey) {
106
+ store.set('selectedNodeIds', new Set([nodeId]));
107
+ } else if (!selectedIds.has(nodeId) && e.shiftKey) {
108
+ selectedIds.add(nodeId);
109
+ store.set('selectedNodeIds', new Set(selectedIds));
110
+ }
111
+
112
+ // Capture start positions of all selected nodes
113
+ const movingIds = store.get('selectedNodeIds');
114
+ const startPositions = new Map();
115
+ for (const id of movingIds) {
116
+ const n = wf.nodes.find(nd => nd.id === id);
117
+ if (n) startPositions.set(id, { x: n.position.x, y: n.position.y });
118
+ }
119
+
120
+ _drag = {
121
+ type: 'node',
122
+ nodeId,
123
+ startX: e.clientX,
124
+ startY: e.clientY,
125
+ startPositions,
126
+ zoom,
127
+ pointerId: e.pointerId,
128
+ moved: false,
129
+ };
130
+
131
+ nodeEl.style.cursor = 'grabbing';
132
+ nodeEl.setPointerCapture(e.pointerId);
133
+ }
134
+
135
+ function moveNodeMove(e) {
136
+ if (!_drag || _drag.type !== 'node') return;
137
+ _drag.moved = true;
138
+
139
+ const dx = (e.clientX - _drag.startX) / _drag.zoom;
140
+ const dy = (e.clientY - _drag.startY) / _drag.zoom;
141
+
142
+ for (const [id, startPos] of _drag.startPositions) {
143
+ const snapped = snap(startPos.x + dx, startPos.y + dy);
144
+ WorkflowModel.moveNode(id, snapped.x, snapped.y);
145
+ }
146
+ }
147
+
148
+ function endNodeMove(e) {
149
+ if (!_drag || _drag.type !== 'node') return;
150
+ const nodeEl = _nodeContainer?.querySelector(`[data-node-id="${_drag.nodeId}"]`);
151
+ if (nodeEl) {
152
+ nodeEl.style.cursor = 'grab';
153
+ nodeEl.releasePointerCapture(_drag.pointerId);
154
+ }
155
+ _drag = null;
156
+ }
157
+
158
+ // ── Edge creation ───────────────────────────────────────────────────────
159
+
160
+ function startEdgeDrag(e, nodeId, portId, portType) {
161
+ if (portType !== 'out') return; // Only drag from output ports
162
+
163
+ const wf = store.get('workflow');
164
+ const node = wf?.nodes.find(n => n.id === nodeId);
165
+ if (!node) return;
166
+
167
+ // Import EdgeRenderer dynamically to avoid circular deps
168
+ import('./edge-renderer.js').then(({ EdgeRenderer }) => {
169
+ const tempPath = EdgeRenderer.createTempEdge();
170
+ _svgLayer?.appendChild(tempPath);
171
+
172
+ const outPorts = resolveOutputPorts(node);
173
+ let portIdx = outPorts.findIndex(p => p.id === portId);
174
+ if (portIdx === -1) portIdx = 0;
175
+
176
+ const NODE_HEIGHT = 80;
177
+ const spacing = outPorts.length <= 1
178
+ ? NODE_HEIGHT / 2
179
+ : NODE_HEIGHT / (outPorts.length + 1);
180
+ const startX = node.position.x + 220; // NODE_WIDTH
181
+ const startY = outPorts.length <= 1
182
+ ? node.position.y + NODE_HEIGHT / 2
183
+ : node.position.y + spacing * (portIdx + 1);
184
+
185
+ _drag = {
186
+ type: 'edge',
187
+ sourceNodeId: nodeId,
188
+ sourcePort: portId,
189
+ tempPath,
190
+ startX,
191
+ startY,
192
+ pointerId: e.pointerId,
193
+ EdgeRenderer,
194
+ };
195
+
196
+ EdgeRenderer.updateTempEdge(tempPath, startX, startY, startX, startY);
197
+ });
198
+ }
199
+
200
+ function moveEdgeDrag(e) {
201
+ if (!_drag || _drag.type !== 'edge') return;
202
+ const worldPos = _canvas?.screenToWorld(e.clientX, e.clientY);
203
+ if (worldPos && _drag.EdgeRenderer) {
204
+ _drag.EdgeRenderer.updateTempEdge(_drag.tempPath, _drag.startX, _drag.startY, worldPos.x, worldPos.y);
205
+ }
206
+ }
207
+
208
+ function endEdgeDrag(e) {
209
+ if (!_drag || _drag.type !== 'edge') return;
210
+
211
+ // Remove temp edge
212
+ if (_drag.EdgeRenderer) {
213
+ _drag.EdgeRenderer.removeTempEdge(_drag.tempPath);
214
+ }
215
+
216
+ // Check if we released over an input port
217
+ const targetEl = document.elementFromPoint(e.clientX, e.clientY);
218
+ if (targetEl?.classList?.contains('wfb-port-in') ||
219
+ (targetEl?.dataset?.portType === 'in')) {
220
+ const targetNodeId = targetEl.dataset.nodeId;
221
+ if (targetNodeId && targetNodeId !== _drag.sourceNodeId) {
222
+ WorkflowModel.addEdge(_drag.sourceNodeId, targetNodeId, _drag.sourcePort);
223
+ }
224
+ }
225
+
226
+ _drag = null;
227
+ }
228
+
229
+ // ── Unified event handlers ──────────────────────────────────────────────
230
+
231
+ function onPointerDown(e) {
232
+ // Check if clicking a port
233
+ const portEl = e.target.closest?.('.wfb-port');
234
+ if (portEl) {
235
+ e.preventDefault();
236
+ e.stopPropagation();
237
+ startEdgeDrag(e, portEl.dataset.nodeId, portEl.dataset.portId, portEl.dataset.portType);
238
+ return;
239
+ }
240
+
241
+ // Check if clicking a node body (not port)
242
+ const nodeEl = e.target.closest?.('.wfb-node');
243
+ if (nodeEl && e.button === 0) {
244
+ e.preventDefault();
245
+ startNodeMove(e, nodeEl.dataset.nodeId, nodeEl);
246
+ return;
247
+ }
248
+ }
249
+
250
+ function onPointerMove(e) {
251
+ if (!_drag) return;
252
+ switch (_drag.type) {
253
+ case 'palette': movePaletteDrag(e); break;
254
+ case 'node': moveNodeMove(e); break;
255
+ case 'edge': moveEdgeDrag(e); break;
256
+ }
257
+ }
258
+
259
+ function onPointerUp(e) {
260
+ if (!_drag) return;
261
+ switch (_drag.type) {
262
+ case 'palette': endPaletteDrag(e); break;
263
+ case 'node': endNodeMove(e); break;
264
+ case 'edge': endEdgeDrag(e); break;
265
+ }
266
+ }
267
+
268
+ // ── Public API ──────────────────────────────────────────────────────────
269
+
270
+ export const DragManager = {
271
+ /**
272
+ * Initialize drag handling.
273
+ * @param {object} canvas - Canvas module
274
+ * @param {HTMLElement} nodeContainer - World element
275
+ * @param {SVGGElement} svgLayer - SVG world group
276
+ */
277
+ init(canvas, nodeContainer, svgLayer) {
278
+ _canvas = canvas;
279
+ _nodeContainer = nodeContainer;
280
+ _svgLayer = svgLayer;
281
+
282
+ // Node/port interactions on the world element
283
+ _nodeContainer.addEventListener('pointerdown', onPointerDown);
284
+ document.addEventListener('pointermove', onPointerMove);
285
+ document.addEventListener('pointerup', onPointerUp);
286
+ document.addEventListener('pointercancel', onPointerUp);
287
+ },
288
+
289
+ /**
290
+ * Remove all event listeners.
291
+ */
292
+ destroy() {
293
+ if (_nodeContainer) {
294
+ _nodeContainer.removeEventListener('pointerdown', onPointerDown);
295
+ }
296
+ document.removeEventListener('pointermove', onPointerMove);
297
+ document.removeEventListener('pointerup', onPointerUp);
298
+ document.removeEventListener('pointercancel', onPointerUp);
299
+
300
+ if (_drag?.type === 'palette' && _drag.ghost) {
301
+ _drag.ghost.remove();
302
+ }
303
+ _drag = null;
304
+ _canvas = null;
305
+ _nodeContainer = null;
306
+ _svgLayer = null;
307
+ },
308
+
309
+ /**
310
+ * Toggle snap-to-grid.
311
+ * @param {boolean} enabled
312
+ */
313
+ setSnapToGrid(enabled) {
314
+ _snapEnabled = enabled;
315
+ },
316
+
317
+ /**
318
+ * Begin a palette drag from an external element (e.g., node palette).
319
+ * Call this from a pointerdown handler on a palette item.
320
+ * @param {PointerEvent} e
321
+ * @param {string} nodeType
322
+ */
323
+ startPaletteDrag(e, nodeType) {
324
+ startPaletteDrag(e, nodeType);
325
+ },
326
+ };
@@ -0,0 +1,235 @@
1
+ // ── Workflow Editor — SVG Bezier Edge Rendering ────────────────────────
2
+ // Creates and updates SVG cubic bezier paths between node ports.
3
+
4
+ import { store } from '../state/store.js';
5
+ import { resolveOutputPorts } from '../models/node-types.js';
6
+
7
+ const NODE_WIDTH = 220;
8
+ const NODE_HEIGHT_ESTIMATE = 80;
9
+ const SVG_NS = 'http://www.w3.org/2000/svg';
10
+
11
+ /**
12
+ * Get output port position for a source node.
13
+ * For single-output nodes, port is at the right-center.
14
+ * For multi-output nodes, ports are spaced vertically.
15
+ */
16
+ function getSourcePoint(node, sourcePort, _typeDef) {
17
+ const x = node.position.x + NODE_WIDTH;
18
+ const outPorts = resolveOutputPorts(node);
19
+ if (outPorts.length <= 1) {
20
+ return { x, y: node.position.y + NODE_HEIGHT_ESTIMATE / 2 };
21
+ }
22
+ // Multi-output: find port index
23
+ let idx = outPorts.findIndex(p => p.id === sourcePort);
24
+ if (idx === -1) idx = 0;
25
+ const spacing = NODE_HEIGHT_ESTIMATE / (outPorts.length + 1);
26
+ return { x, y: node.position.y + spacing * (idx + 1) };
27
+ }
28
+
29
+ /**
30
+ * Get input port position for a target node.
31
+ * Input port is at the left-center.
32
+ */
33
+ function getTargetPoint(node, targetPort, typeDef) {
34
+ const x = node.position.x;
35
+ const inCount = typeDef?.ports?.in ?? 1;
36
+ if (inCount <= 1) {
37
+ return { x, y: node.position.y + NODE_HEIGHT_ESTIMATE / 2 };
38
+ }
39
+ let idx = 0; // Default to first port
40
+ if (targetPort) {
41
+ const match = targetPort.match(/in-(\d+)/);
42
+ if (match) idx = parseInt(match[1], 10);
43
+ }
44
+ const spacing = NODE_HEIGHT_ESTIMATE / (inCount + 1);
45
+ return { x, y: node.position.y + spacing * (idx + 1) };
46
+ }
47
+
48
+ /**
49
+ * Compute the cubic bezier 'd' attribute for a path.
50
+ */
51
+ function computePathD(x1, y1, x2, y2) {
52
+ const dx = Math.max(50, Math.abs(x2 - x1) * 0.4);
53
+ return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`;
54
+ }
55
+
56
+ // ── Public API ──────────────────────────────────────────────────────────
57
+
58
+ export const EdgeRenderer = {
59
+ /**
60
+ * Create an SVG path element for a workflow edge.
61
+ * @param {object} edge - { id, source, target, sourcePort, condition }
62
+ * @param {object} sourceNode
63
+ * @param {object} targetNode
64
+ * @param {object} [sourceTypeDef] - NODE_TYPES entry for source
65
+ * @param {object} [targetTypeDef] - NODE_TYPES entry for target
66
+ * @returns {SVGGElement} An SVG group containing the path and optional label
67
+ */
68
+ createEdgePath(edge, sourceNode, targetNode, sourceTypeDef, targetTypeDef) {
69
+ const group = document.createElementNS(SVG_NS, 'g');
70
+ group.setAttribute('class', 'wfb-edge');
71
+ group.dataset.edgeId = edge.id;
72
+
73
+ // Hit area (wider, invisible path for easier selection)
74
+ const hitPath = document.createElementNS(SVG_NS, 'path');
75
+ hitPath.setAttribute('class', 'wfb-edge-hit');
76
+ hitPath.setAttribute('fill', 'none');
77
+ hitPath.setAttribute('stroke', 'transparent');
78
+ hitPath.setAttribute('stroke-width', '12');
79
+ hitPath.style.cursor = 'pointer';
80
+ hitPath.style.pointerEvents = 'stroke';
81
+
82
+ // Visible path
83
+ const path = document.createElementNS(SVG_NS, 'path');
84
+ path.setAttribute('class', 'wfb-edge-path');
85
+ path.setAttribute('fill', 'none');
86
+ path.setAttribute('stroke', 'var(--df-color-border-default)');
87
+ path.setAttribute('stroke-width', '2');
88
+ path.setAttribute('marker-end', 'url(#wfb-arrowhead)');
89
+ path.style.transition = `stroke var(--df-duration-fast) ease`;
90
+ path.style.pointerEvents = 'none';
91
+
92
+ // Compute positions
93
+ const src = getSourcePoint(sourceNode, edge.sourcePort, sourceTypeDef);
94
+ const tgt = getTargetPoint(targetNode, null, targetTypeDef);
95
+ const d = computePathD(src.x, src.y, tgt.x, tgt.y);
96
+ path.setAttribute('d', d);
97
+ hitPath.setAttribute('d', d);
98
+
99
+ group.appendChild(hitPath);
100
+ group.appendChild(path);
101
+
102
+ // Label
103
+ if (edge.condition) {
104
+ const midX = (src.x + tgt.x) / 2;
105
+ const midY = (src.y + tgt.y) / 2 - 8;
106
+ const text = document.createElementNS(SVG_NS, 'text');
107
+ text.setAttribute('class', 'wfb-edge-label');
108
+ text.setAttribute('x', String(midX));
109
+ text.setAttribute('y', String(midY));
110
+ text.setAttribute('text-anchor', 'middle');
111
+ text.setAttribute('fill', 'var(--df-color-text-muted)');
112
+ text.setAttribute('font-size', '11');
113
+ text.setAttribute('font-family', 'var(--df-font-mono)');
114
+ text.style.pointerEvents = 'none';
115
+ text.textContent = edge.condition;
116
+ group.appendChild(text);
117
+ }
118
+
119
+ return group;
120
+ },
121
+
122
+ /**
123
+ * Update the path 'd' attribute for an existing edge group.
124
+ * @param {SVGGElement} groupEl
125
+ * @param {object} sourceNode
126
+ * @param {object} targetNode
127
+ * @param {object} [sourceTypeDef]
128
+ * @param {object} [targetTypeDef]
129
+ * @param {string} [sourcePort]
130
+ */
131
+ updateEdgePath(groupEl, sourceNode, targetNode, sourceTypeDef, targetTypeDef, sourcePort) {
132
+ const src = getSourcePoint(sourceNode, sourcePort, sourceTypeDef);
133
+ const tgt = getTargetPoint(targetNode, null, targetTypeDef);
134
+ const d = computePathD(src.x, src.y, tgt.x, tgt.y);
135
+
136
+ const hitPath = groupEl.querySelector('.wfb-edge-hit');
137
+ const path = groupEl.querySelector('.wfb-edge-path');
138
+ if (hitPath) hitPath.setAttribute('d', d);
139
+ if (path) path.setAttribute('d', d);
140
+
141
+ // Update label position
142
+ const label = groupEl.querySelector('.wfb-edge-label');
143
+ if (label) {
144
+ label.setAttribute('x', String((src.x + tgt.x) / 2));
145
+ label.setAttribute('y', String((src.y + tgt.y) / 2 - 8));
146
+ }
147
+ },
148
+
149
+ /**
150
+ * Create a temporary edge path for edge creation drag.
151
+ * @returns {SVGPathElement}
152
+ */
153
+ createTempEdge() {
154
+ const path = document.createElementNS(SVG_NS, 'path');
155
+ path.setAttribute('class', 'wfb-temp-edge');
156
+ path.setAttribute('fill', 'none');
157
+ path.setAttribute('stroke', 'var(--df-color-accent-default)');
158
+ path.setAttribute('stroke-width', '2');
159
+ path.setAttribute('stroke-dasharray', '6 3');
160
+ path.setAttribute('marker-end', 'url(#wfb-arrowhead-selected)');
161
+ path.style.pointerEvents = 'none';
162
+ path.style.opacity = '0.7';
163
+ return path;
164
+ },
165
+
166
+ /**
167
+ * Update the temporary edge path during drag.
168
+ * @param {SVGPathElement} pathEl
169
+ * @param {number} x1
170
+ * @param {number} y1
171
+ * @param {number} x2
172
+ * @param {number} y2
173
+ */
174
+ updateTempEdge(pathEl, x1, y1, x2, y2) {
175
+ pathEl.setAttribute('d', computePathD(x1, y1, x2, y2));
176
+ },
177
+
178
+ /**
179
+ * Remove a temporary edge path from the SVG layer.
180
+ * @param {SVGPathElement} pathEl
181
+ */
182
+ removeTempEdge(pathEl) {
183
+ pathEl?.remove();
184
+ },
185
+
186
+ /**
187
+ * Set execution status highlight on an edge.
188
+ * @param {SVGGElement} groupEl
189
+ * @param {'running'|'passed'|'failed'|null} status
190
+ */
191
+ setEdgeStatus(groupEl, status) {
192
+ const path = groupEl?.querySelector('.wfb-edge-path');
193
+ if (!path) return;
194
+
195
+ switch (status) {
196
+ case 'running':
197
+ path.setAttribute('stroke', 'var(--df-color-state-recording)');
198
+ path.setAttribute('stroke-width', '3');
199
+ path.setAttribute('marker-end', 'url(#wfb-arrowhead-selected)');
200
+ break;
201
+ case 'passed':
202
+ path.setAttribute('stroke', 'var(--df-color-state-success)');
203
+ path.setAttribute('stroke-width', '2');
204
+ break;
205
+ case 'failed':
206
+ path.setAttribute('stroke', 'var(--df-color-state-error)');
207
+ path.setAttribute('stroke-width', '2');
208
+ break;
209
+ default:
210
+ path.setAttribute('stroke', 'var(--df-color-border-default)');
211
+ path.setAttribute('stroke-width', '2');
212
+ path.setAttribute('marker-end', 'url(#wfb-arrowhead)');
213
+ break;
214
+ }
215
+ },
216
+
217
+ /**
218
+ * Mark an edge as selected or deselected.
219
+ * @param {SVGGElement} groupEl
220
+ * @param {boolean} selected
221
+ */
222
+ setSelected(groupEl, selected) {
223
+ const path = groupEl?.querySelector('.wfb-edge-path');
224
+ if (!path) return;
225
+ if (selected) {
226
+ path.setAttribute('stroke', 'var(--df-color-accent-default)');
227
+ path.setAttribute('stroke-width', '3');
228
+ path.setAttribute('marker-end', 'url(#wfb-arrowhead-selected)');
229
+ } else {
230
+ path.setAttribute('stroke', 'var(--df-color-border-default)');
231
+ path.setAttribute('stroke-width', '2');
232
+ path.setAttribute('marker-end', 'url(#wfb-arrowhead)');
233
+ }
234
+ },
235
+ };
@@ -0,0 +1,147 @@
1
+ // ── Workflow Editor — Undo/Redo History Manager ───────────────────────
2
+ // Command stack storing JSON snapshots, capped at 50 entries.
3
+
4
+ import { store } from '../state/store.js';
5
+
6
+ const MAX_HISTORY = 50;
7
+
8
+ let _stack = []; // Array of JSON strings (workflow snapshots)
9
+ let _index = -1; // Current position in the stack
10
+ let _unsub = null; // Store subscription teardown
11
+ let _skipNext = false; // Prevent re-push during undo/redo restore
12
+
13
+ // ── Public API ──────────────────────────────────────────────────────────
14
+
15
+ export const HistoryManager = {
16
+ /**
17
+ * Initialize history tracking by subscribing to workflow changes.
18
+ */
19
+ init() {
20
+ _stack = [];
21
+ _index = -1;
22
+ _skipNext = false;
23
+
24
+ _unsub = store.on('workflow', (wf) => {
25
+ if (_skipNext) {
26
+ _skipNext = false;
27
+ return;
28
+ }
29
+ if (!wf) return;
30
+ this.push(wf);
31
+ });
32
+
33
+ // Keyboard shortcuts
34
+ this._onKeyDown = (e) => {
35
+ // Ctrl/Cmd+Z — undo
36
+ if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
37
+ if (isInputFocused()) return;
38
+ e.preventDefault();
39
+ this.undo();
40
+ return;
41
+ }
42
+ // Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y — redo
43
+ if (((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z') ||
44
+ ((e.ctrlKey || e.metaKey) && e.key === 'y')) {
45
+ if (isInputFocused()) return;
46
+ e.preventDefault();
47
+ this.redo();
48
+ return;
49
+ }
50
+ };
51
+ document.addEventListener('keydown', this._onKeyDown);
52
+ },
53
+
54
+ /**
55
+ * Unsubscribe from the store.
56
+ */
57
+ destroy() {
58
+ if (_unsub) {
59
+ _unsub();
60
+ _unsub = null;
61
+ }
62
+ if (this._onKeyDown) {
63
+ document.removeEventListener('keydown', this._onKeyDown);
64
+ this._onKeyDown = null;
65
+ }
66
+ _stack = [];
67
+ _index = -1;
68
+ },
69
+
70
+ /**
71
+ * Push a workflow snapshot onto the history stack.
72
+ * Truncates any redo entries ahead of the current index.
73
+ * @param {object} state - Workflow object to snapshot
74
+ */
75
+ push(state) {
76
+ const json = JSON.stringify(state);
77
+
78
+ // Avoid duplicates (same as current top)
79
+ if (_index >= 0 && _stack[_index] === json) return;
80
+
81
+ // Truncate redo entries
82
+ _stack = _stack.slice(0, _index + 1);
83
+ _stack.push(json);
84
+
85
+ // Cap at MAX_HISTORY
86
+ if (_stack.length > MAX_HISTORY) {
87
+ _stack.shift();
88
+ }
89
+ _index = _stack.length - 1;
90
+ },
91
+
92
+ /**
93
+ * Restore the previous state.
94
+ */
95
+ undo() {
96
+ if (!this.canUndo()) return;
97
+ _index--;
98
+ _skipNext = true;
99
+ const snapshot = JSON.parse(_stack[_index]);
100
+ store.set('workflow', snapshot);
101
+ store.set('isDirty', true);
102
+ },
103
+
104
+ /**
105
+ * Restore the next state (after an undo).
106
+ */
107
+ redo() {
108
+ if (!this.canRedo()) return;
109
+ _index++;
110
+ _skipNext = true;
111
+ const snapshot = JSON.parse(_stack[_index]);
112
+ store.set('workflow', snapshot);
113
+ store.set('isDirty', true);
114
+ },
115
+
116
+ /**
117
+ * @returns {boolean}
118
+ */
119
+ canUndo() {
120
+ return _index > 0;
121
+ },
122
+
123
+ /**
124
+ * @returns {boolean}
125
+ */
126
+ canRedo() {
127
+ return _index < _stack.length - 1;
128
+ },
129
+
130
+ /**
131
+ * Clear all history.
132
+ */
133
+ clear() {
134
+ _stack = [];
135
+ _index = -1;
136
+ },
137
+ };
138
+
139
+ /**
140
+ * Check if an input/textarea/contenteditable is focused.
141
+ */
142
+ function isInputFocused() {
143
+ const el = document.activeElement;
144
+ if (!el) return false;
145
+ const tag = el.tagName.toLowerCase();
146
+ return tag === 'input' || tag === 'textarea' || tag === 'select' || el.isContentEditable;
147
+ }