@pauloricca/spagbol 0.1.1 → 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.
Files changed (61) hide show
  1. package/README.md +33 -3
  2. package/dist/core/controller.js +4 -4
  3. package/dist/core/controller.js.map +1 -1
  4. package/dist/core/model.d.ts +2 -1
  5. package/dist/core/model.d.ts.map +1 -1
  6. package/dist/core/model.js.map +1 -1
  7. package/dist/editors.css +157 -0
  8. package/dist/react/SpagbolCanvas.d.ts.map +1 -1
  9. package/dist/react/SpagbolCanvas.js +53 -14
  10. package/dist/react/SpagbolCanvas.js.map +1 -1
  11. package/dist/react/SpagbolCanvasControls.d.ts.map +1 -1
  12. package/dist/react/SpagbolCanvasControls.js +5 -5
  13. package/dist/react/SpagbolCanvasControls.js.map +1 -1
  14. package/dist/react/SpagbolControlJoystick.d.ts +9 -0
  15. package/dist/react/SpagbolControlJoystick.d.ts.map +1 -0
  16. package/dist/react/SpagbolControlJoystick.js +111 -0
  17. package/dist/react/SpagbolControlJoystick.js.map +1 -0
  18. package/dist/react/SpagbolControlZoom.d.ts.map +1 -1
  19. package/dist/react/SpagbolControlZoom.js +3 -2
  20. package/dist/react/SpagbolControlZoom.js.map +1 -1
  21. package/dist/react/SpagbolCurveEditor.d.ts +45 -0
  22. package/dist/react/SpagbolCurveEditor.d.ts.map +1 -0
  23. package/dist/react/SpagbolCurveEditor.js +142 -0
  24. package/dist/react/SpagbolCurveEditor.js.map +1 -0
  25. package/dist/react/SpagbolNodeFrame.d.ts.map +1 -1
  26. package/dist/react/SpagbolNodeFrame.js +5 -5
  27. package/dist/react/SpagbolNodeFrame.js.map +1 -1
  28. package/dist/react/SpagbolNodeFrameTypePicker.js +1 -1
  29. package/dist/react/SpagbolNodeFrameTypePicker.js.map +1 -1
  30. package/dist/react/SpagbolSequencer.d.ts +35 -0
  31. package/dist/react/SpagbolSequencer.d.ts.map +1 -0
  32. package/dist/react/SpagbolSequencer.js +166 -0
  33. package/dist/react/SpagbolSequencer.js.map +1 -0
  34. package/dist/react/index.d.ts +3 -0
  35. package/dist/react/index.d.ts.map +1 -1
  36. package/dist/react/index.js +3 -0
  37. package/dist/react/index.js.map +1 -1
  38. package/dist/react/zoom.d.ts +5 -0
  39. package/dist/react/zoom.d.ts.map +1 -0
  40. package/dist/react/zoom.js +19 -0
  41. package/dist/react/zoom.js.map +1 -0
  42. package/dist/styles.css +112 -41
  43. package/docs/language/standard-node-comparison.md +92 -0
  44. package/docs/language/standard-node-contracts.md +237 -0
  45. package/docs/language/standard-nodes.md +198 -0
  46. package/docs/language/ui-components.md +79 -0
  47. package/package.json +5 -3
  48. package/src/core/controller.ts +4 -4
  49. package/src/core/model.ts +2 -1
  50. package/src/react/SpagbolCanvas.tsx +74 -18
  51. package/src/react/SpagbolCanvasControls.tsx +2 -7
  52. package/src/react/SpagbolControlJoystick.tsx +147 -0
  53. package/src/react/SpagbolControlZoom.tsx +3 -2
  54. package/src/react/SpagbolCurveEditor.tsx +166 -0
  55. package/src/react/SpagbolNodeFrame.tsx +18 -13
  56. package/src/react/SpagbolNodeFrameTypePicker.tsx +1 -1
  57. package/src/react/SpagbolSequencer.tsx +265 -0
  58. package/src/react/editors.css +157 -0
  59. package/src/react/index.ts +4 -0
  60. package/src/react/styles.css +112 -41
  61. package/src/react/zoom.ts +18 -0
@@ -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":"AAIA,OAAO,EAyBL,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;AAOD,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,+BAetD"}
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,9 +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";
2
3
  import { previewNodePositions, applyNodePositions } from "./nodeDrag.js";
3
4
  import { createPortal } from "react-dom";
4
5
  import { SpagbolCanvasLinkControls } from "./SpagbolCanvasLinkControls.js";
5
6
  import { SpagbolCanvasFlow } from "./SpagbolCanvasFlow.js";
6
- 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";
7
8
  import { memo, useCallback, useEffect, useId, useMemo, useRef, useState, } from "react";
8
9
  import { areaMembers, groupPanelLayout, spagbolGroupKinds, copyFragment, createId, portsFor, readFragment, } from "../core/index.js";
9
10
  import { GraphContext, useEditorSnapshot, useEditorSelector, useSpagbol, } from "./context.js";
@@ -14,6 +15,7 @@ import { rememberMeasurements, reuseFlowPresentations, stationaryReconnectEdge,
14
15
  import { duplicateDragPresentation, } from "./duplicateDrag.js";
15
16
  import { translateExtentForVisibleContent, clampViewportToTranslateExtent, infiniteCanvasExtent, } from "./viewport.js";
16
17
  import { anchorFlowId, areaFlowId, linkPath, nodeFlowId, parseHandle, sourceHandle, targetHandle, } from "./geometry.js";
18
+ const defaultGraphViewport = { x: 0, y: 0, zoom: SPAGBOL_ZOOM_BASELINE };
17
19
  const emptyFlowNodes = [];
18
20
  const emptyViews = Object.freeze({});
19
21
  const emptyPresentations = Object.freeze({});
@@ -151,14 +153,18 @@ function SpagbolCanvasArea({ data, selected }) {
151
153
  height: area.collapsed
152
154
  ? 34 + (area.controlHeight ?? 0)
153
155
  : area.size.height,
154
- }, 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() || 'Area' }); setTitleEditing(false); }, onKeyDown: event => { event.stopPropagation(); if (event.key === 'Enter')
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')
155
157
  event.currentTarget.blur(); if (event.key === 'Escape') {
156
158
  setTitleDraft(area.title);
157
159
  setTitleEditing(false);
158
160
  } } }) : _jsx("span", { className: "spagbol-canvas-area-title", onDoubleClick: event => { event.stopPropagation(); if (editor.editing) {
159
161
  setTitleDraft(area.title);
160
162
  setTitleEditing(true);
161
- } }, children: area.title }), _jsxs("div", { className: "spagbol-canvas-area-actions", onPointerDown: (event) => event.stopPropagation(), children: [_jsx("button", { title: area.locked ? "Unlock membership" : "Lock membership", "aria-label": area.locked ? "Unlock membership" : "Lock membership", disabled: !editor.editing, onClick: () => editor.lockArea(area.id, !area.locked), children: area.locked ? "●" : "○" }), _jsx("button", { 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 } })] }));
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 } })] }));
162
168
  }
163
169
  const SpagbolCanvasLink = memo(function SpagbolCanvasLink(props) {
164
170
  const { editor, diagnostics, linkOverlay, connecting } = useSpagbol();
@@ -215,8 +221,8 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
215
221
  // React Flow from hiding/reinitializing every node on each drag preview frame.
216
222
  const measuredSizes = useRef(new Map());
217
223
  const [measurementRevision, setMeasurementRevision] = useState(0);
218
- const [zoom, setZoom] = useState(props.defaultViewport?.zoom ?? 1);
219
- const [settledViewport, setSettledViewport] = useState(props.defaultViewport ?? { x: 0, y: 0, zoom: 1 });
224
+ const [zoom, setZoom] = useState(props.defaultViewport?.zoom ?? SPAGBOL_ZOOM_BASELINE);
225
+ const [settledViewport, setSettledViewport] = useState(props.defaultViewport ?? defaultGraphViewport);
220
226
  const [editorSize, setEditorSize] = useState({ width: 0, height: 0 });
221
227
  const viewportMoving = useRef(false);
222
228
  useEffect(() => {
@@ -239,6 +245,9 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
239
245
  const [activeMode, setActiveMode] = useState(editor.policy.modes?.[0] ?? editor.modes.list()[0]?.id ?? "set");
240
246
  const [picker, setPicker] = useState(null);
241
247
  const [search, setSearch] = useState("");
248
+ const [pickerIndex, setPickerIndex] = useState(0);
249
+ const pickerListId = useId();
250
+ const pickerList = useRef(null);
242
251
  const [error, setError] = useState(null);
243
252
  const [drawing, setDrawing] = useState(null);
244
253
  const connection = useRef(null);
@@ -785,6 +794,11 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
785
794
  const next = previewNodePositions(nodeDrag.current, positions);
786
795
  if (next !== nodeDrag.current)
787
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));
788
802
  return;
789
803
  }
790
804
  if (positions.length)
@@ -819,6 +833,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
819
833
  return;
820
834
  const bounds = root.current?.getBoundingClientRect();
821
835
  setSearch("");
836
+ setPickerIndex(0);
822
837
  setPicker({
823
838
  point: flow.screenToFlowPosition(client),
824
839
  screen: {
@@ -972,7 +987,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
972
987
  }
973
988
  if (command && key === "0") {
974
989
  event.preventDefault();
975
- void flow.zoomTo(1);
990
+ void flow.zoomTo(SPAGBOL_ZOOM_BASELINE);
976
991
  return;
977
992
  }
978
993
  if (key === "delete" || key === "backspace") {
@@ -1017,14 +1032,20 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
1017
1032
  const touch = event.changedTouches[0];
1018
1033
  return touch ? { x: touch.clientX, y: touch.clientY } : undefined;
1019
1034
  }
1020
- const availableNodes = [...editor.nodes.list().filter(definition => !props.nodeKinds || props.nodeKinds.includes(definition.kind)), ...(features.areas && !picker?.origin && !picker?.linkId ? [{ kind: 'spagbol.area', label: 'Area', createData: () => ({}), ports: () => [] }] : [])]
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: () => [] }] : [])]
1021
1036
  .filter(definition => snapshot.path.length > 0 || ![spagbolGroupKinds.inputs, spagbolGroupKinds.outputs].includes(definition.kind))
1022
1037
  .filter((definition) => `${definition.label} ${definition.category ?? ""} ${definition.keywords?.join(" ") ?? ""}`
1023
1038
  .toLowerCase()
1024
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]);
1025
1046
  return (_jsx(GraphContext.Provider, { value: context, children: _jsxs("div", { ref: root, className: `spagbol-canvas ${props.className ?? ""}`, style: {
1026
1047
  ...props.style,
1027
- "--spagbol-title-scale": zoom < 0.3 ? 1.75 : zoom < 0.5 ? 1.3 : 1,
1048
+ "--spagbol-title-scale": spagbolHeaderTitleScale(zoom),
1028
1049
  "--spagbol-canvas-height": `${editorSize.height}px`,
1029
1050
  }, tabIndex: 0, "aria-label": props.ariaLabel ?? "Graph editor", onKeyDown: keyDown, onDoubleClick: (event) => {
1030
1051
  if (event.target.classList.contains("react-flow__pane")) {
@@ -1227,7 +1248,7 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
1227
1248
  }, connectionLineType: ConnectionLineType.Bezier, connectionLineStyle: {
1228
1249
  stroke: editor.modes.get(activeMode)?.color,
1229
1250
  strokeWidth: 2,
1230
- }, defaultViewport: props.defaultViewport, onInit: instance => props.onInit?.(instance), onMoveStart: () => {
1251
+ }, defaultViewport: props.defaultViewport ?? defaultGraphViewport, onInit: instance => props.onInit?.(instance), onMoveStart: () => {
1231
1252
  viewportMoving.current = true;
1232
1253
  }, onMove: (_, viewport) => setZoom(viewport.zoom), onMoveEnd: (_, viewport) => {
1233
1254
  viewportMoving.current = false;
@@ -1237,12 +1258,30 @@ function SpagbolCanvasInner({ editor, views = emptyViews, features = defaultFeat
1237
1258
  transform: `translate(${Math.min(drawing.start.x, drawing.end.x)}px, ${Math.min(drawing.start.y, drawing.end.y)}px)`,
1238
1259
  width: Math.abs(drawing.start.x - drawing.end.x),
1239
1260
  height: Math.abs(drawing.start.y - drawing.end.y),
1240
- } }) })) }), _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 ? "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", placeholder: "Search nodes\u2026", value: search, onChange: (event) => setSearch(event.target.value), onKeyDown: (event) => {
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) => {
1241
1265
  event.stopPropagation();
1242
- if (event.key === "Escape")
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();
1243
1278
  setPicker(null);
1244
- if (event.key === "Enter" && availableNodes[0])
1245
- chooseNode(availableNodes[0].kind);
1246
- } }), _jsxs("div", { className: "spagbol-canvas-picker-list", children: [availableNodes.map((definition) => (_jsxs("button", { 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 }))] }) }));
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 }))] }) }));
1247
1286
  }
1248
1287
  //# sourceMappingURL=SpagbolCanvas.js.map