@pauloricca/spagbol 0.1.0 → 0.1.3
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/README.md +42 -4
- package/dist/core/controller.js +4 -4
- package/dist/core/controller.js.map +1 -1
- package/dist/core/model.d.ts +2 -1
- package/dist/core/model.d.ts.map +1 -1
- package/dist/core/model.js.map +1 -1
- package/dist/editors.css +157 -0
- package/dist/react/SpagbolCanvas.d.ts.map +1 -1
- package/dist/react/SpagbolCanvas.js +106 -29
- package/dist/react/SpagbolCanvas.js.map +1 -1
- package/dist/react/SpagbolCanvasControls.d.ts.map +1 -1
- package/dist/react/SpagbolCanvasControls.js +5 -5
- package/dist/react/SpagbolCanvasControls.js.map +1 -1
- package/dist/react/SpagbolControlJoystick.d.ts +9 -0
- package/dist/react/SpagbolControlJoystick.d.ts.map +1 -0
- package/dist/react/SpagbolControlJoystick.js +111 -0
- package/dist/react/SpagbolControlJoystick.js.map +1 -0
- package/dist/react/SpagbolControlZoom.d.ts.map +1 -1
- package/dist/react/SpagbolControlZoom.js +3 -2
- package/dist/react/SpagbolControlZoom.js.map +1 -1
- package/dist/react/SpagbolCurveEditor.d.ts +45 -0
- package/dist/react/SpagbolCurveEditor.d.ts.map +1 -0
- package/dist/react/SpagbolCurveEditor.js +142 -0
- package/dist/react/SpagbolCurveEditor.js.map +1 -0
- package/dist/react/SpagbolNodeFrame.d.ts.map +1 -1
- package/dist/react/SpagbolNodeFrame.js +18 -16
- package/dist/react/SpagbolNodeFrame.js.map +1 -1
- package/dist/react/SpagbolNodeFrameTypePicker.js +1 -1
- package/dist/react/SpagbolNodeFrameTypePicker.js.map +1 -1
- package/dist/react/SpagbolSequencer.d.ts +35 -0
- package/dist/react/SpagbolSequencer.d.ts.map +1 -0
- package/dist/react/SpagbolSequencer.js +166 -0
- package/dist/react/SpagbolSequencer.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/nodeDrag.d.ts +7 -0
- package/dist/react/nodeDrag.d.ts.map +1 -0
- package/dist/react/nodeDrag.js +27 -0
- package/dist/react/nodeDrag.js.map +1 -0
- package/dist/react/zoom.d.ts +5 -0
- package/dist/react/zoom.d.ts.map +1 -0
- package/dist/react/zoom.js +19 -0
- package/dist/react/zoom.js.map +1 -0
- package/dist/styles.css +112 -41
- package/docs/language/standard-node-comparison.md +92 -0
- package/docs/language/standard-node-contracts.md +237 -0
- package/docs/language/standard-nodes.md +198 -0
- package/docs/language/ui-components.md +79 -0
- package/package.json +5 -3
- package/src/core/controller.ts +4 -4
- package/src/core/model.ts +2 -1
- package/src/react/SpagbolCanvas.tsx +121 -31
- package/src/react/SpagbolCanvasControls.tsx +2 -7
- package/src/react/SpagbolControlJoystick.tsx +147 -0
- package/src/react/SpagbolControlZoom.tsx +3 -2
- package/src/react/SpagbolCurveEditor.tsx +166 -0
- package/src/react/SpagbolNodeFrame.tsx +30 -24
- package/src/react/SpagbolNodeFrameTypePicker.tsx +1 -1
- package/src/react/SpagbolSequencer.tsx +265 -0
- package/src/react/editors.css +157 -0
- package/src/react/index.ts +4 -0
- package/src/react/nodeDrag.ts +33 -0
- package/src/react/styles.css +112 -41
- package/src/react/zoom.ts +18 -0
package/dist/editors.css
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/* Controlled editors extracted from Teia; hosts own storage and execution. */
|
|
2
|
+
.spagbol-curve-editor {
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-column: 1 / -1;
|
|
5
|
+
gap: 0;
|
|
6
|
+
width: 100%;
|
|
7
|
+
min-width: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.spagbol-curve-editor-chart {
|
|
11
|
+
position: relative;
|
|
12
|
+
height: var(--node-display-height, 128px);
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.spagbol-curve-editor-canvas {
|
|
17
|
+
position: relative;
|
|
18
|
+
z-index: 1;
|
|
19
|
+
display: block;
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
|
+
border: 0;
|
|
23
|
+
background: transparent;
|
|
24
|
+
cursor: crosshair;
|
|
25
|
+
touch-action: none;
|
|
26
|
+
user-select: none;
|
|
27
|
+
-webkit-user-select: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.spagbol-curve-editor-value-label {
|
|
31
|
+
--spagbol-curve-editor-value-offset: calc(7px * var(--graph-screen-scale, 1));
|
|
32
|
+
position: absolute;
|
|
33
|
+
z-index: 3;
|
|
34
|
+
padding: calc(2px * var(--graph-screen-scale, 1)) calc(4px * var(--graph-screen-scale, 1));
|
|
35
|
+
transform: translate(var(--spagbol-curve-editor-value-offset), calc(-100% - var(--spagbol-curve-editor-value-offset)));
|
|
36
|
+
border: calc(1px * var(--graph-screen-scale, 1)) solid var(--color-foreground-62, rgba(255,255,255,.62));
|
|
37
|
+
background: var(--color-background, var(--spagbol-bg, #000));
|
|
38
|
+
color: var(--color-foreground, var(--spagbol-foreground, #fff));
|
|
39
|
+
font-size: calc(1.1em * var(--graph-screen-scale, 1));
|
|
40
|
+
font-variant-numeric: tabular-nums;
|
|
41
|
+
line-height: 1;
|
|
42
|
+
white-space: nowrap;
|
|
43
|
+
pointer-events: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.spagbol-curve-editor-value-label.is-left {
|
|
47
|
+
transform: translate(calc(-100% - var(--spagbol-curve-editor-value-offset)), calc(-100% - var(--spagbol-curve-editor-value-offset)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.spagbol-curve-editor-value-label.is-below {
|
|
51
|
+
transform: translate(var(--spagbol-curve-editor-value-offset), var(--spagbol-curve-editor-value-offset));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.spagbol-curve-editor-value-label.is-left.is-below {
|
|
55
|
+
transform: translate(calc(-100% - var(--spagbol-curve-editor-value-offset)), var(--spagbol-curve-editor-value-offset));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.spagbol-curve-editor-chrome {
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.spagbol-curve-editor-path {
|
|
63
|
+
fill: none;
|
|
64
|
+
stroke: var(--color-accent, var(--spagbol-accent, #2fd061));
|
|
65
|
+
stroke-linecap: square;
|
|
66
|
+
stroke-linejoin: round;
|
|
67
|
+
stroke-width: calc(2px * var(--graph-stroke-scale, 1));
|
|
68
|
+
vector-effect: non-scaling-stroke;
|
|
69
|
+
}
|
|
70
|
+
.spagbol-curve-editor-path.is-background { opacity: .38; }
|
|
71
|
+
.spagbol-curve-editor-hit-target {
|
|
72
|
+
fill: transparent;
|
|
73
|
+
cursor: grab;
|
|
74
|
+
pointer-events: all;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.spagbol-curve-editor-hit-target.is-locked {
|
|
78
|
+
cursor: default;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.spagbol-curve-editor-handle {
|
|
82
|
+
fill: var(--color-background, var(--spagbol-bg, #000));
|
|
83
|
+
stroke: var(--color-accent, var(--spagbol-accent, #2fd061));
|
|
84
|
+
stroke-width: calc(2px * var(--graph-stroke-scale, 1) * var(--spagbol-curve-editor-point-scale, 1));
|
|
85
|
+
cursor: grab;
|
|
86
|
+
pointer-events: none;
|
|
87
|
+
vector-effect: non-scaling-stroke;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.spagbol-curve-editor-endpoint {
|
|
91
|
+
stroke: var(--color-foreground, var(--spagbol-foreground, #fff));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.spagbol-curve-editor-handle.is-locked {
|
|
95
|
+
cursor: default;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
.spagbol-sequencer { display: grid; grid-auto-rows: minmax(0, 1fr); width: 100%; min-width: 0; }
|
|
100
|
+
.spagbol-sequencer-row { display: grid; grid-template-columns: 32px minmax(0, 1fr); min-width: 0; }
|
|
101
|
+
.spagbol-sequencer-label-right .spagbol-sequencer-row { grid-template-columns: minmax(0, 1fr) var(--spagbol-sequencer-label-width, 44px); }
|
|
102
|
+
.spagbol-sequencer-label { display: flex; align-items: center; justify-content: flex-end; padding-right: 5px; }
|
|
103
|
+
.spagbol-sequencer-label-right .spagbol-sequencer-label { justify-content: flex-start; padding-right: 0; }
|
|
104
|
+
.spagbol-sequencer-cells { position: relative; display: grid; grid-template-columns: repeat(var(--spagbol-sequencer-steps), minmax(0, 1fr)); min-width: 0; overflow: hidden; }
|
|
105
|
+
.spagbol-sequencer-cell { min-width: 0; padding: 0; border: 0; background: transparent; }
|
|
106
|
+
.spagbol-sequencer-gate { position: absolute; z-index: 2; bottom: 0; min-width: 2px; background: var(--color-accent, var(--spagbol-accent, #2fd061)); cursor: grab; touch-action: none; }
|
|
107
|
+
.spagbol-sequencer-create-preview { pointer-events: none; opacity: 0.7; }
|
|
108
|
+
.spagbol-sequencer-gate-handle { position: absolute; top: 0; bottom: 0; width: 7px; cursor: ew-resize; }
|
|
109
|
+
.spagbol-sequencer-gate-handle-left { left: 0; }.spagbol-sequencer-gate-handle-right { right: 0; }
|
|
110
|
+
.spagbol-sequencer-level-handle { position: absolute; top: 0; left: 0; right: 0; height: 7px; cursor: ns-resize; }
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
.spagbol-sequencer .spagbol-sequencer-cells { height: 100%; }
|
|
114
|
+
.spagbol-sequencer .spagbol-sequencer-cell {
|
|
115
|
+
width: 100%;
|
|
116
|
+
height: 100%;
|
|
117
|
+
border-right: 1px solid var(--color-foreground-44, rgba(255,255,255,.44));
|
|
118
|
+
border-bottom: 1px solid var(--color-foreground-44, rgba(255,255,255,.44));
|
|
119
|
+
box-sizing: border-box;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
}
|
|
122
|
+
.spagbol-sequencer .spagbol-sequencer-row:first-child .spagbol-sequencer-cell { border-top: 1px solid var(--color-foreground-44, rgba(255,255,255,.44)); }
|
|
123
|
+
.spagbol-sequencer .spagbol-sequencer-cell:first-child { border-left: 1px solid var(--color-foreground-44, rgba(255,255,255,.44)); }
|
|
124
|
+
.spagbol-sequencer .spagbol-sequencer-cell-beat-highlight { background: var(--color-foreground-15, rgba(255,255,255,.15)); }
|
|
125
|
+
.spagbol-sequencer .spagbol-sequencer-cell:hover,
|
|
126
|
+
.spagbol-sequencer .spagbol-sequencer-cell:focus-visible { background: var(--color-foreground-20, rgba(255,255,255,.2)); outline: 0; }
|
|
127
|
+
.spagbol-sequencer .spagbol-sequencer-cell-current {
|
|
128
|
+
background: var(--color-foreground-12, rgba(255,255,255,.12));
|
|
129
|
+
box-shadow: inset 0 0 0 2px var(--color-foreground-28, rgba(255,255,255,.28));
|
|
130
|
+
}
|
|
131
|
+
.spagbol-sequencer .spagbol-sequencer-gate { border: 0; border-radius: 0; box-shadow: none; }
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
.spagbol-control-joystick {
|
|
135
|
+
--joystick-handle-size: 12px;
|
|
136
|
+
position: relative;
|
|
137
|
+
height: var(--node-display-height, 120px);
|
|
138
|
+
min-width: 0;
|
|
139
|
+
box-sizing: border-box;
|
|
140
|
+
border: calc(1px * var(--graph-stroke-scale, 1)) solid var(--color-foreground, var(--spagbol-foreground, #fff));
|
|
141
|
+
background: transparent;
|
|
142
|
+
cursor: crosshair;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
touch-action: none;
|
|
145
|
+
user-select: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.spagbol-control-joystick-handle {
|
|
149
|
+
position: absolute;
|
|
150
|
+
width: var(--joystick-handle-size);
|
|
151
|
+
height: var(--joystick-handle-size);
|
|
152
|
+
box-sizing: border-box;
|
|
153
|
+
border: calc(1px * var(--graph-stroke-scale, 1)) solid var(--color-foreground, var(--spagbol-foreground, #fff));
|
|
154
|
+
background: var(--color-accent, var(--spagbol-accent, #2fd061));
|
|
155
|
+
pointer-events: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpagbolCanvas.d.ts","sourceRoot":"","sources":["../../src/react/SpagbolCanvas.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SpagbolCanvas.d.ts","sourceRoot":"","sources":["../../src/react/SpagbolCanvas.tsx"],"names":[],"mappings":"AAKA,OAAO,EA0BL,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAWL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAQL,KAAK,UAAU,EAEf,KAAK,WAAW,EAChB,KAAK,SAAS,EAIf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAKL,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,4BAA4B,CAAC;AA4CpC,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AACD,MAAM,WAAW,kBAAmB,SAAQ,kBAAkB;IAC5D,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;IAClD,oFAAoF;IACpF,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,8EAA8E;IAC9E,gBAAgB,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,WAAW,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IACpC,gBAAgB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC,CAAC;IACrE,wEAAwE;IACxE,YAAY,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,eAAe,CAAC,EAAE,QAAQ,CAAC;IAC3B,gBAAgB,CAAC,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC5C,MAAM,CAAC,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC3C,mFAAmF;IACnF,SAAS,CAAC,EAAE,KAAK,GAAG;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,cAAc,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC;IAChE,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,QAAQ,CAAC,EAAE,4BAA4B,GAAG,KAAK,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,+BAetD"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { SPAGBOL_ZOOM_BASELINE, spagbolHeaderTitleScale } from "./zoom.js";
|
|
3
|
+
import { previewNodePositions, applyNodePositions } from "./nodeDrag.js";
|
|
2
4
|
import { createPortal } from "react-dom";
|
|
3
5
|
import { SpagbolCanvasLinkControls } from "./SpagbolCanvasLinkControls.js";
|
|
4
6
|
import { SpagbolCanvasFlow } from "./SpagbolCanvasFlow.js";
|
|
5
|
-
import { BaseEdge, ConnectionLineType, EdgeLabelRenderer, Handle, Position, ReactFlow, ReactFlowProvider, SelectionMode, ViewportPortal, useReactFlow, useStore, useStoreApi, useUpdateNodeInternals, useNodeId, useNodesInitialized, useViewport, } from "@xyflow/react";
|
|
7
|
+
import { applyNodeChanges, BaseEdge, ConnectionLineType, EdgeLabelRenderer, Handle, Position, ReactFlow, ReactFlowProvider, SelectionMode, ViewportPortal, useReactFlow, useStore, useStoreApi, useUpdateNodeInternals, useNodeId, useNodesInitialized, useViewport, } from "@xyflow/react";
|
|
6
8
|
import { memo, useCallback, useEffect, useId, useMemo, useRef, useState, } from "react";
|
|
7
9
|
import { areaMembers, groupPanelLayout, spagbolGroupKinds, copyFragment, createId, portsFor, readFragment, } from "../core/index.js";
|
|
8
10
|
import { GraphContext, useEditorSnapshot, useEditorSelector, useSpagbol, } from "./context.js";
|
|
@@ -13,6 +15,8 @@ import { rememberMeasurements, reuseFlowPresentations, stationaryReconnectEdge,
|
|
|
13
15
|
import { duplicateDragPresentation, } from "./duplicateDrag.js";
|
|
14
16
|
import { translateExtentForVisibleContent, clampViewportToTranslateExtent, infiniteCanvasExtent, } from "./viewport.js";
|
|
15
17
|
import { anchorFlowId, areaFlowId, linkPath, nodeFlowId, parseHandle, sourceHandle, targetHandle, } from "./geometry.js";
|
|
18
|
+
const defaultGraphViewport = { x: 0, y: 0, zoom: SPAGBOL_ZOOM_BASELINE };
|
|
19
|
+
const emptyFlowNodes = [];
|
|
16
20
|
const emptyViews = Object.freeze({});
|
|
17
21
|
const emptyPresentations = Object.freeze({});
|
|
18
22
|
const defaultFeatures = { compact: true, nestedGraphs: true, panels: true, areas: true, linkProperties: true };
|
|
@@ -149,22 +153,26 @@ function SpagbolCanvasArea({ data, selected }) {
|
|
|
149
153
|
height: area.collapsed
|
|
150
154
|
? 34 + (area.controlHeight ?? 0)
|
|
151
155
|
: area.size.height,
|
|
152
|
-
}, children: [_jsxs("div", { className: "spagbol-canvas-area-header nodrag nopan", onPointerDown: (event) => start(event), ...pointerHandlers, children: [titleEditing ? _jsx("input", { autoFocus: true, className: "spagbol-canvas-area-title-input nodrag nopan", "aria-label": "Area title", value: titleDraft, onFocus: event => event.currentTarget.select(), onChange: event => setTitleDraft(event.currentTarget.value), onPointerDown: event => event.stopPropagation(), onBlur: () => { editor.updateArea(area.id, { title: titleDraft.trim() || '
|
|
156
|
+
}, children: [_jsxs("div", { className: "spagbol-canvas-area-header nodrag nopan", onPointerDown: (event) => start(event), ...pointerHandlers, children: [titleEditing ? _jsx("input", { autoFocus: true, className: "spagbol-canvas-area-title-input nodrag nopan", "aria-label": "Area title", value: titleDraft, onFocus: event => event.currentTarget.select(), onChange: event => setTitleDraft(event.currentTarget.value), onPointerDown: event => event.stopPropagation(), onBlur: () => { editor.updateArea(area.id, { title: titleDraft.trim() || 'Container' }); setTitleEditing(false); }, onKeyDown: event => { event.stopPropagation(); if (event.key === 'Enter')
|
|
153
157
|
event.currentTarget.blur(); if (event.key === 'Escape') {
|
|
154
158
|
setTitleDraft(area.title);
|
|
155
159
|
setTitleEditing(false);
|
|
156
160
|
} } }) : _jsx("span", { className: "spagbol-canvas-area-title", onDoubleClick: event => { event.stopPropagation(); if (editor.editing) {
|
|
157
161
|
setTitleDraft(area.title);
|
|
158
162
|
setTitleEditing(true);
|
|
159
|
-
} }, children: area.title }), _jsxs("div", { className: "spagbol-canvas-area-actions", onPointerDown: (event) => event.stopPropagation(), children: [_jsx("button", {
|
|
163
|
+
} }, children: area.title }), _jsxs("div", { className: "spagbol-canvas-area-actions", onPointerDown: (event) => event.stopPropagation(), children: [_jsx("button", { type: "button", className: "spagbol-canvas-area-lock", title: area.locked ? "Unlock area membership" : "Lock area membership", "aria-label": area.locked ? "Unlock area membership" : "Lock area membership", "aria-pressed": area.locked === true, disabled: !editor.editing, onDoubleClick: event => event.stopPropagation(), onClick: event => {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
event.stopPropagation();
|
|
166
|
+
editor.lockArea(area.id, !area.locked);
|
|
167
|
+
}, children: _jsx("span", { className: "spagbol-canvas-area-lock-icon", "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: "spagbol-canvas-area-compact", "aria-pressed": area.collapsed === true, title: area.collapsed ? "Expand area" : "Collapse area", "aria-label": area.collapsed ? "Expand area" : "Collapse area", disabled: !editor.editing, onClick: () => editor.updateArea(area.id, { collapsed: !area.collapsed }), children: area.collapsed ? "+" : "−" })] })] }), !area.collapsed && area.controlHeight ? (_jsx("div", { className: "spagbol-canvas-area-divider", style: { top: 34 + area.controlHeight } })) : null, !area.collapsed && editor.editing && (_jsx("button", { className: "spagbol-canvas-area-resize nodrag nopan", "aria-label": "Resize area", onPointerDown: (event) => start(event, true), ...pointerHandlers })), _jsx(Handle, { id: "collapsed:in", type: "target", position: Position.Left, isConnectable: false, className: "spagbol-canvas-area-pin", style: { top: 17 } }), _jsx(Handle, { id: "collapsed:out", type: "source", position: Position.Right, isConnectable: false, className: "spagbol-canvas-area-pin", style: { top: 17 } })] }));
|
|
160
168
|
}
|
|
161
169
|
const SpagbolCanvasLink = memo(function SpagbolCanvasLink(props) {
|
|
162
170
|
const { editor, diagnostics, linkOverlay, connecting } = useSpagbol();
|
|
163
171
|
const flow = useReactFlow();
|
|
164
172
|
const viewport = useViewport();
|
|
165
173
|
const [controlsVisible, setControlsVisible] = useState(false);
|
|
166
|
-
const
|
|
167
|
-
const showControls = props.selected &&
|
|
174
|
+
const selectedLinkId = useEditorSelector(editor, snapshot => snapshot.selection.linkIds.length === 1 ? snapshot.selection.linkIds[0] : undefined);
|
|
175
|
+
const showControls = props.selected && selectedLinkId === props.id && editor.editing && !connecting && !props.data?.virtual;
|
|
168
176
|
useEffect(() => {
|
|
169
177
|
if (!showControls) {
|
|
170
178
|
setControlsVisible(false);
|
|
@@ -206,13 +214,15 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
206
214
|
const flow = useReactFlow();
|
|
207
215
|
const flowStore = useStoreApi();
|
|
208
216
|
const nodesInitialized = useNodesInitialized();
|
|
209
|
-
|
|
217
|
+
// Only cable-property anchors need live native geometry. Subscribing to every
|
|
218
|
+
// position change otherwise causes redundant full-canvas renders during drags.
|
|
219
|
+
const flowNodes = useStore((state) => features.linkProperties ? state.nodes : emptyFlowNodes);
|
|
210
220
|
// Measurements are canvas state, not saved graph data. Retaining them keeps
|
|
211
221
|
// React Flow from hiding/reinitializing every node on each drag preview frame.
|
|
212
222
|
const measuredSizes = useRef(new Map());
|
|
213
223
|
const [measurementRevision, setMeasurementRevision] = useState(0);
|
|
214
|
-
const [zoom, setZoom] = useState(props.defaultViewport?.zoom ??
|
|
215
|
-
const [settledViewport, setSettledViewport] = useState(props.defaultViewport ??
|
|
224
|
+
const [zoom, setZoom] = useState(props.defaultViewport?.zoom ?? SPAGBOL_ZOOM_BASELINE);
|
|
225
|
+
const [settledViewport, setSettledViewport] = useState(props.defaultViewport ?? defaultGraphViewport);
|
|
216
226
|
const [editorSize, setEditorSize] = useState({ width: 0, height: 0 });
|
|
217
227
|
const viewportMoving = useRef(false);
|
|
218
228
|
useEffect(() => {
|
|
@@ -235,6 +245,9 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
235
245
|
const [activeMode, setActiveMode] = useState(editor.policy.modes?.[0] ?? editor.modes.list()[0]?.id ?? "set");
|
|
236
246
|
const [picker, setPicker] = useState(null);
|
|
237
247
|
const [search, setSearch] = useState("");
|
|
248
|
+
const [pickerIndex, setPickerIndex] = useState(0);
|
|
249
|
+
const pickerListId = useId();
|
|
250
|
+
const pickerList = useRef(null);
|
|
238
251
|
const [error, setError] = useState(null);
|
|
239
252
|
const [drawing, setDrawing] = useState(null);
|
|
240
253
|
const connection = useRef(null);
|
|
@@ -244,10 +257,24 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
244
257
|
const [reconnectOriginal, setReconnectOriginal] = useState(null);
|
|
245
258
|
const duplicate = useRef(null);
|
|
246
259
|
const [duplicatePreview, setDuplicatePreview] = useState(null);
|
|
247
|
-
const
|
|
260
|
+
const cancelledNodeDrag = useRef(false);
|
|
261
|
+
// Position previews belong to the canvas, not the persistent document. A drag
|
|
262
|
+
// commits once, avoiding whole-document edits/validation on every pointer move.
|
|
263
|
+
const nodeDrag = useRef(null);
|
|
264
|
+
const [positionRevision, setPositionRevision] = useState(0);
|
|
265
|
+
const clearNodeDrag = () => {
|
|
266
|
+
nodeDrag.current = null;
|
|
267
|
+
// Force a controlled refresh on cancellation, even though the saved position
|
|
268
|
+
// never changed: React Flow currently holds the transient pointer position.
|
|
269
|
+
setPositionRevision(value => value + 1);
|
|
270
|
+
};
|
|
248
271
|
useEffect(() => {
|
|
272
|
+
if (!snapshot.gesture && nodeDrag.current) {
|
|
273
|
+
cancelledNodeDrag.current = true;
|
|
274
|
+
clearNodeDrag();
|
|
275
|
+
}
|
|
249
276
|
if (!snapshot.gesture && duplicate.current) {
|
|
250
|
-
|
|
277
|
+
cancelledNodeDrag.current = true;
|
|
251
278
|
duplicate.current = null;
|
|
252
279
|
setDuplicatePreview(null);
|
|
253
280
|
}
|
|
@@ -283,7 +310,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
283
310
|
const area = editor.graph.areas.find(area => area.id === id);
|
|
284
311
|
if (!area)
|
|
285
312
|
return;
|
|
286
|
-
|
|
313
|
+
cancelledNodeDrag.current = false;
|
|
287
314
|
const fragment = copyFragment(editor.graph, [], [id]);
|
|
288
315
|
duplicate.current = { namespace: createId('preview'), fragment, nodeId: '', origin: area.position, positions: new Map(fragment.nodes.map(node => [node.id, node.position])), areaPositions: new Map(fragment.areas.map(area => [area.id, area.position])), external };
|
|
289
316
|
setDuplicatePreview(duplicate.current);
|
|
@@ -391,7 +418,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
391
418
|
...renderGraph.nodes.map((node) => ({
|
|
392
419
|
id: nodeFlowId(node.id),
|
|
393
420
|
type: "spagbolNode",
|
|
394
|
-
position: node.position,
|
|
421
|
+
position: nodeDrag.current?.get(node.id) ?? node.position,
|
|
395
422
|
selected: snapshot.selection.nodeIds.includes(node.id),
|
|
396
423
|
hidden: collapsed.nodes.has(node.id),
|
|
397
424
|
draggable: editor.editing && views[node.kind]?.draggable !== false &&
|
|
@@ -407,6 +434,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
407
434
|
deletable: false,
|
|
408
435
|
dragHandle: ".spagbol-canvas-drag-handle",
|
|
409
436
|
data: {
|
|
437
|
+
positionRevision,
|
|
410
438
|
node: duplicatePresentation?.originalNodes.get(node.id) ?? node,
|
|
411
439
|
projected: collapsed.projected.has(node.id),
|
|
412
440
|
stationaryOriginal: duplicatePresentation?.originalNodes.has(node.id),
|
|
@@ -426,6 +454,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
426
454
|
})),
|
|
427
455
|
], [
|
|
428
456
|
renderGraph,
|
|
457
|
+
positionRevision,
|
|
429
458
|
duplicatePresentation,
|
|
430
459
|
snapshot.selection,
|
|
431
460
|
snapshot.mode,
|
|
@@ -483,6 +512,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
483
512
|
collapsed,
|
|
484
513
|
flow,
|
|
485
514
|
measurementKey,
|
|
515
|
+
measurementRevision,
|
|
486
516
|
settledViewport.zoom,
|
|
487
517
|
editorSize,
|
|
488
518
|
props.panBounds,
|
|
@@ -758,8 +788,19 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
758
788
|
setDuplicatePreview(next);
|
|
759
789
|
return;
|
|
760
790
|
}
|
|
761
|
-
if (
|
|
791
|
+
if (cancelledNodeDrag.current)
|
|
762
792
|
return;
|
|
793
|
+
if (positions.length && nodeDrag.current) {
|
|
794
|
+
const next = previewNodePositions(nodeDrag.current, positions);
|
|
795
|
+
if (next !== nodeDrag.current)
|
|
796
|
+
nodeDrag.current = next;
|
|
797
|
+
// This is a controlled React Flow: recording positions in a ref does not
|
|
798
|
+
// update native geometry. Publish only the transient node presentation;
|
|
799
|
+
// the graph, validation and history still change once, on drop.
|
|
800
|
+
const state = flowStore.getState();
|
|
801
|
+
state.setNodes(applyNodeChanges(positions, state.nodes));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
763
804
|
if (positions.length)
|
|
764
805
|
editor.edit("Move nodes", "layout", (graph) => {
|
|
765
806
|
for (const change of positions)
|
|
@@ -792,6 +833,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
792
833
|
return;
|
|
793
834
|
const bounds = root.current?.getBoundingClientRect();
|
|
794
835
|
setSearch("");
|
|
836
|
+
setPickerIndex(0);
|
|
795
837
|
setPicker({
|
|
796
838
|
point: flow.screenToFlowPosition(client),
|
|
797
839
|
screen: {
|
|
@@ -892,8 +934,10 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
892
934
|
return;
|
|
893
935
|
const command = event.metaKey || event.ctrlKey, key = event.key.toLowerCase();
|
|
894
936
|
if (key === "escape") {
|
|
895
|
-
if (duplicate.current)
|
|
896
|
-
|
|
937
|
+
if (duplicate.current || nodeDrag.current)
|
|
938
|
+
cancelledNodeDrag.current = true;
|
|
939
|
+
if (nodeDrag.current)
|
|
940
|
+
clearNodeDrag();
|
|
897
941
|
duplicate.current = null;
|
|
898
942
|
setDuplicatePreview(null);
|
|
899
943
|
editor.cancelGesture();
|
|
@@ -943,7 +987,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
943
987
|
}
|
|
944
988
|
if (command && key === "0") {
|
|
945
989
|
event.preventDefault();
|
|
946
|
-
void flow.zoomTo(
|
|
990
|
+
void flow.zoomTo(SPAGBOL_ZOOM_BASELINE);
|
|
947
991
|
return;
|
|
948
992
|
}
|
|
949
993
|
if (key === "delete" || key === "backspace") {
|
|
@@ -988,14 +1032,20 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
988
1032
|
const touch = event.changedTouches[0];
|
|
989
1033
|
return touch ? { x: touch.clientX, y: touch.clientY } : undefined;
|
|
990
1034
|
}
|
|
991
|
-
const availableNodes = [...editor.nodes.list().filter(definition => !props.nodeKinds || props.nodeKinds.includes(definition.kind)), ...(features.areas && !picker?.origin && !picker?.linkId ? [{ kind: 'spagbol.area', label: '
|
|
1035
|
+
const availableNodes = [...editor.nodes.list().filter(definition => !props.nodeKinds || props.nodeKinds.includes(definition.kind)), ...(features.areas && !picker?.origin && !picker?.linkId ? [{ kind: 'spagbol.area', label: 'Container', createData: () => ({}), ports: () => [] }] : [])]
|
|
992
1036
|
.filter(definition => snapshot.path.length > 0 || ![spagbolGroupKinds.inputs, spagbolGroupKinds.outputs].includes(definition.kind))
|
|
993
1037
|
.filter((definition) => `${definition.label} ${definition.category ?? ""} ${definition.keywords?.join(" ") ?? ""}`
|
|
994
1038
|
.toLowerCase()
|
|
995
1039
|
.includes(search.toLowerCase()));
|
|
1040
|
+
const selectedPickerIndex = Math.min(pickerIndex, Math.max(0, availableNodes.length - 1));
|
|
1041
|
+
const selectedPickerNode = availableNodes[selectedPickerIndex];
|
|
1042
|
+
useEffect(() => {
|
|
1043
|
+
if (picker)
|
|
1044
|
+
pickerList.current?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
1045
|
+
}, [picker, selectedPickerIndex, search]);
|
|
996
1046
|
return (_jsx(GraphContext.Provider, { value: context, children: _jsxs("div", { ref: root, className: `spagbol-canvas ${props.className ?? ""}`, style: {
|
|
997
1047
|
...props.style,
|
|
998
|
-
"--spagbol-title-scale": zoom
|
|
1048
|
+
"--spagbol-title-scale": spagbolHeaderTitleScale(zoom),
|
|
999
1049
|
"--spagbol-canvas-height": `${editorSize.height}px`,
|
|
1000
1050
|
}, tabIndex: 0, "aria-label": props.ariaLabel ?? "Graph editor", onKeyDown: keyDown, onDoubleClick: (event) => {
|
|
1001
1051
|
if (event.target.classList.contains("react-flow__pane")) {
|
|
@@ -1045,8 +1095,10 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
1045
1095
|
if (event.currentTarget.hasPointerCapture(event.pointerId))
|
|
1046
1096
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
1047
1097
|
}, onPointerCancel: () => {
|
|
1048
|
-
if (duplicate.current)
|
|
1049
|
-
|
|
1098
|
+
if (duplicate.current || nodeDrag.current)
|
|
1099
|
+
cancelledNodeDrag.current = true;
|
|
1100
|
+
if (nodeDrag.current)
|
|
1101
|
+
clearNodeDrag();
|
|
1050
1102
|
duplicate.current = null;
|
|
1051
1103
|
setDuplicatePreview(null);
|
|
1052
1104
|
setDrawing(null);
|
|
@@ -1064,8 +1116,10 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
1064
1116
|
}, onNodeDragStart: (event, node) => {
|
|
1065
1117
|
if (!node.data.node)
|
|
1066
1118
|
return;
|
|
1067
|
-
|
|
1119
|
+
cancelledNodeDrag.current = false;
|
|
1068
1120
|
editor.beginGesture(event.altKey ? "Duplicate nodes" : "Move nodes");
|
|
1121
|
+
if (!event.altKey)
|
|
1122
|
+
nodeDrag.current = new Map();
|
|
1069
1123
|
if (event.altKey) {
|
|
1070
1124
|
const selection = editor.getSnapshot().selection;
|
|
1071
1125
|
const ids = selection.nodeIds.includes(node.data.node.id)
|
|
@@ -1084,11 +1138,14 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
1084
1138
|
setDuplicatePreview(duplicate.current);
|
|
1085
1139
|
}
|
|
1086
1140
|
}, onNodeDragStop: (_event, node) => {
|
|
1141
|
+
const positions = nodeDrag.current;
|
|
1142
|
+
if (positions)
|
|
1143
|
+
clearNodeDrag();
|
|
1087
1144
|
const current = duplicate.current;
|
|
1088
1145
|
duplicate.current = null;
|
|
1089
1146
|
setDuplicatePreview(null);
|
|
1090
|
-
if (
|
|
1091
|
-
|
|
1147
|
+
if (cancelledNodeDrag.current) {
|
|
1148
|
+
cancelledNodeDrag.current = false;
|
|
1092
1149
|
editor.cancelGesture();
|
|
1093
1150
|
editor.select(editor.getSnapshot().selection);
|
|
1094
1151
|
return;
|
|
@@ -1108,6 +1165,8 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
1108
1165
|
return;
|
|
1109
1166
|
}
|
|
1110
1167
|
}
|
|
1168
|
+
if (positions?.size)
|
|
1169
|
+
editor.edit("Move nodes", "layout", graph => applyNodePositions(graph, positions));
|
|
1111
1170
|
editor.commitGesture();
|
|
1112
1171
|
}, onConnectStart: (_event, params) => {
|
|
1113
1172
|
setRevealedNodeId(params.nodeId?.startsWith("node:") ? params.nodeId.slice(5) : null);
|
|
@@ -1189,7 +1248,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
1189
1248
|
}, connectionLineType: ConnectionLineType.Bezier, connectionLineStyle: {
|
|
1190
1249
|
stroke: editor.modes.get(activeMode)?.color,
|
|
1191
1250
|
strokeWidth: 2,
|
|
1192
|
-
}, defaultViewport: props.defaultViewport, onInit: instance => props.onInit?.(instance), onMoveStart: () => {
|
|
1251
|
+
}, defaultViewport: props.defaultViewport ?? defaultGraphViewport, onInit: instance => props.onInit?.(instance), onMoveStart: () => {
|
|
1193
1252
|
viewportMoving.current = true;
|
|
1194
1253
|
}, onMove: (_, viewport) => setZoom(viewport.zoom), onMoveEnd: (_, viewport) => {
|
|
1195
1254
|
viewportMoving.current = false;
|
|
@@ -1199,12 +1258,30 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
|
|
|
1199
1258
|
transform: `translate(${Math.min(drawing.start.x, drawing.end.x)}px, ${Math.min(drawing.start.y, drawing.end.y)}px)`,
|
|
1200
1259
|
width: Math.abs(drawing.start.x - drawing.end.x),
|
|
1201
1260
|
height: Math.abs(drawing.start.y - drawing.end.y),
|
|
1202
|
-
} }) })) }), _jsx("div", { ref: setLinkOverlay, className: "spagbol-canvas-link-overlay" }), (props.toolbar ?? true) === true ? (props.controls === false ? null : (_jsx(SpagbolCanvasControls, { ...props.controls }))) : (props.toolbar), picker && (_jsxs("div", { className: "spagbol-canvas-picker nodrag nopan", role: "dialog", "aria-label": "Add node", style: { left: picker.screen.x, top: picker.screen.y }, onPointerDown: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "spagbol-canvas-picker-heading", children: [picker.linkId ? "
|
|
1261
|
+
} }) })) }), _jsx("div", { ref: setLinkOverlay, className: "spagbol-canvas-link-overlay" }), (props.toolbar ?? true) === true ? (props.controls === false ? null : (_jsx(SpagbolCanvasControls, { ...props.controls }))) : (props.toolbar), picker && (_jsxs("div", { className: "spagbol-canvas-picker nodrag nopan", role: "dialog", "aria-label": "Add node", style: { left: picker.screen.x, top: picker.screen.y }, onPointerDown: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "spagbol-canvas-picker-heading", children: [_jsx("span", { className: "spagbol-canvas-picker-title", children: picker.linkId ? "Insert a node" : "Add a node" }), _jsx("button", { "aria-label": "Cancel node creation", onClick: () => setPicker(null), children: "\u00D7" })] }), _jsx("input", { autoFocus: true, "aria-label": "Search node types", role: "combobox", "aria-expanded": "true", "aria-autocomplete": "list", "aria-controls": pickerListId, "aria-activedescendant": selectedPickerNode ? `${pickerListId}-${selectedPickerIndex}` : undefined, placeholder: "Search nodes\u2026", value: search, onChange: (event) => {
|
|
1262
|
+
setSearch(event.target.value);
|
|
1263
|
+
setPickerIndex(0);
|
|
1264
|
+
}, onKeyDown: (event) => {
|
|
1203
1265
|
event.stopPropagation();
|
|
1204
|
-
if (event.
|
|
1266
|
+
if (event.nativeEvent.isComposing)
|
|
1267
|
+
return;
|
|
1268
|
+
if (event.key === "ArrowDown") {
|
|
1269
|
+
event.preventDefault();
|
|
1270
|
+
setPickerIndex(Math.min(selectedPickerIndex + 1, Math.max(0, availableNodes.length - 1)));
|
|
1271
|
+
}
|
|
1272
|
+
else if (event.key === "ArrowUp") {
|
|
1273
|
+
event.preventDefault();
|
|
1274
|
+
setPickerIndex(Math.max(0, selectedPickerIndex - 1));
|
|
1275
|
+
}
|
|
1276
|
+
else if (event.key === "Escape") {
|
|
1277
|
+
event.preventDefault();
|
|
1205
1278
|
setPicker(null);
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1279
|
+
}
|
|
1280
|
+
else if (event.key === "Enter") {
|
|
1281
|
+
event.preventDefault();
|
|
1282
|
+
if (selectedPickerNode)
|
|
1283
|
+
chooseNode(selectedPickerNode.kind);
|
|
1284
|
+
}
|
|
1285
|
+
} }), _jsxs("div", { ref: pickerList, id: pickerListId, className: "spagbol-canvas-picker-list nowheel", role: "listbox", "aria-label": "Node types", children: [availableNodes.map((definition, index) => (_jsxs("button", { id: `${pickerListId}-${index}`, type: "button", role: "option", "aria-selected": index === selectedPickerIndex, tabIndex: -1, onPointerMove: () => setPickerIndex(index), onMouseDown: (event) => event.preventDefault(), onClick: () => chooseNode(definition.kind), children: [_jsx("span", { children: definition.label }), _jsx("small", { children: definition.category })] }, definition.kind))), !availableNodes.length && _jsx("p", { children: "No matching nodes." })] })] })), error && (_jsxs("div", { className: "spagbol-canvas-error", role: "alert", children: [error, _jsx("button", { "aria-label": "Dismiss error", onClick: () => setError(null), children: "\u00D7" })] })), props.overlay && (_jsx("div", { className: "spagbol-canvas-overlay", children: props.overlay }))] }) }));
|
|
1209
1286
|
}
|
|
1210
1287
|
//# sourceMappingURL=SpagbolCanvas.js.map
|