@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
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,GAGd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,gBAAgB,GAGjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EACpB,YAAY,GAEb,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,eAAe,EACf,UAAU,GAMX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EACL,iBAAiB,GAElB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,kBAAkB,GAEnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,iBAAiB,GAElB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,gCAAgC,EAChC,8BAA8B,EAC9B,oBAAoB,EACpB,oBAAoB,GAIrB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,yBAAyB,GAK1B,MAAM,4BAA4B,CAAC","sourcesContent":["export {\n SpagbolCanvas,\n type SpagbolCanvasProps,\n type SpagbolLinkPresentation,\n} from \"./SpagbolCanvas.js\";\nexport {\n SpagbolNodeFrame,\n SpagbolNodePort,\n SpagbolNodePanel,\n type SpagbolNodeFrameProps,\n type SpagbolNodePortProps,\n} from \"./SpagbolNodeFrame.js\";\nexport {\n SpagbolControlNumber,\n formatNumber,\n type SpagbolControlNumberProps,\n} from \"./SpagbolControlNumber.js\";\nexport {\n defineNodeView,\n useEditorSnapshot,\n useGraphEditor,\n useObservation,\n useRenderTarget,\n useSpagbol,\n type SpagbolNodeView,\n type SpagbolNodeViewProps,\n type SpagbolCanvasFeatures,\n type SpagbolTheme,\n type SpagbolCanvasSlots,\n} from \"./context.js\";\nexport { linkPath } from \"./geometry.js\";\nexport {\n SpagbolCanvasFlow,\n type SpagbolCanvasFlowProps,\n} from \"./SpagbolCanvasFlow.js\";\nexport {\n SpagbolControlZoom,\n type SpagbolControlZoomProps,\n} from \"./SpagbolControlZoom.js\";\nexport {\n SpagbolCanvasMenu,\n type SpagbolCanvasMenuProps,\n} from \"./SpagbolCanvasMenu.js\";\nexport {\n translateExtentForVisibleContent,\n clampViewportToTranslateExtent,\n visibleContentBounds,\n infiniteCanvasExtent,\n type CanvasBoundsItem,\n type CanvasViewport,\n type CanvasExtent,\n} from \"./viewport.js\";\nexport {\n SpagbolCanvasControls,\n SpagbolCanvasActions,\n SpagbolCanvasControlsZoom,\n type SpagbolCanvasControlsOptions,\n type SpagbolCanvasAction,\n type SpagbolCanvasMenuItem,\n type SpagbolCanvasMenuSeparator,\n} from \"./SpagbolCanvasControls.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,GAGd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,gBAAgB,GAGjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EACpB,YAAY,GAEb,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,eAAe,EACf,UAAU,GAMX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EACL,iBAAiB,GAElB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,kBAAkB,GAEnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,iBAAiB,GAElB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,gCAAgC,EAChC,8BAA8B,EAC9B,oBAAoB,EACpB,oBAAoB,GAIrB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,yBAAyB,GAK1B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,kBAAkB,EAA8D,MAAM,yBAAyB,CAAC;AACzH,OAAO,EAAE,gBAAgB,EAAyD,MAAM,uBAAuB,CAAC;AAChH,OAAO,EAAE,sBAAsB,EAAoC,MAAM,6BAA6B,CAAC","sourcesContent":["export {\n SpagbolCanvas,\n type SpagbolCanvasProps,\n type SpagbolLinkPresentation,\n} from \"./SpagbolCanvas.js\";\nexport {\n SpagbolNodeFrame,\n SpagbolNodePort,\n SpagbolNodePanel,\n type SpagbolNodeFrameProps,\n type SpagbolNodePortProps,\n} from \"./SpagbolNodeFrame.js\";\nexport {\n SpagbolControlNumber,\n formatNumber,\n type SpagbolControlNumberProps,\n} from \"./SpagbolControlNumber.js\";\nexport {\n defineNodeView,\n useEditorSnapshot,\n useGraphEditor,\n useObservation,\n useRenderTarget,\n useSpagbol,\n type SpagbolNodeView,\n type SpagbolNodeViewProps,\n type SpagbolCanvasFeatures,\n type SpagbolTheme,\n type SpagbolCanvasSlots,\n} from \"./context.js\";\nexport { linkPath } from \"./geometry.js\";\nexport {\n SpagbolCanvasFlow,\n type SpagbolCanvasFlowProps,\n} from \"./SpagbolCanvasFlow.js\";\nexport {\n SpagbolControlZoom,\n type SpagbolControlZoomProps,\n} from \"./SpagbolControlZoom.js\";\nexport {\n SpagbolCanvasMenu,\n type SpagbolCanvasMenuProps,\n} from \"./SpagbolCanvasMenu.js\";\nexport {\n translateExtentForVisibleContent,\n clampViewportToTranslateExtent,\n visibleContentBounds,\n infiniteCanvasExtent,\n type CanvasBoundsItem,\n type CanvasViewport,\n type CanvasExtent,\n} from \"./viewport.js\";\nexport {\n SpagbolCanvasControls,\n SpagbolCanvasActions,\n SpagbolCanvasControlsZoom,\n type SpagbolCanvasControlsOptions,\n type SpagbolCanvasAction,\n type SpagbolCanvasMenuItem,\n type SpagbolCanvasMenuSeparator,\n} from \"./SpagbolCanvasControls.js\";\n\nexport { SpagbolCurveEditor, type SpagbolCurveEditorProps, type SpagbolCurveEditorPoint } from \"./SpagbolCurveEditor.js\";\nexport { SpagbolSequencer, type SpagbolSequencerProps, type SpagbolSequencerItem } from \"./SpagbolSequencer.js\";\nexport { SpagbolControlJoystick, type SpagbolControlJoystickProps } from \"./SpagbolControlJoystick.js\";\n"]}
@@ -0,0 +1,5 @@
1
+ /** Teia's user-facing 100% corresponds to a half-scale graph viewport. */
2
+ export declare const SPAGBOL_ZOOM_BASELINE = 0.5;
3
+ export declare function spagbolZoomPercentage(zoom: number): string;
4
+ export declare function spagbolHeaderTitleScale(zoom: number): number;
5
+ //# sourceMappingURL=zoom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zoom.d.ts","sourceRoot":"","sources":["../../src/react/zoom.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAMzC,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM5D"}
@@ -0,0 +1,19 @@
1
+ /** Teia's user-facing 100% corresponds to a half-scale graph viewport. */
2
+ export const SPAGBOL_ZOOM_BASELINE = 0.5;
3
+ function normalizedZoom(zoom) {
4
+ return Number.isFinite(zoom) && zoom > 0 ? zoom : SPAGBOL_ZOOM_BASELINE;
5
+ }
6
+ export function spagbolZoomPercentage(zoom) {
7
+ return `${Math.round((normalizedZoom(zoom) / SPAGBOL_ZOOM_BASELINE) * 100)}%`;
8
+ }
9
+ export function spagbolHeaderTitleScale(zoom) {
10
+ const userZoom = normalizedZoom(zoom) / SPAGBOL_ZOOM_BASELINE;
11
+ if (userZoom <= 0.3)
12
+ return 2.2;
13
+ if (userZoom <= 0.5)
14
+ return 1.75;
15
+ if (userZoom < 0.7)
16
+ return 1.3;
17
+ return 1;
18
+ }
19
+ //# sourceMappingURL=zoom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zoom.js","sourceRoot":"","sources":["../../src/react/zoom.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC,SAAS,cAAc,CAAC,IAAY;IAClC,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,qBAAqB,CAAC;AAC1E,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,IAAY;IAClD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG,qBAAqB,CAAC;IAC9D,IAAI,QAAQ,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC;IAChC,IAAI,QAAQ,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,QAAQ,GAAG,GAAG;QAAE,OAAO,GAAG,CAAC;IAC/B,OAAO,CAAC,CAAC;AACX,CAAC","sourcesContent":["/** Teia's user-facing 100% corresponds to a half-scale graph viewport. */\nexport const SPAGBOL_ZOOM_BASELINE = 0.5;\n\nfunction normalizedZoom(zoom: number): number {\n return Number.isFinite(zoom) && zoom > 0 ? zoom : SPAGBOL_ZOOM_BASELINE;\n}\n\nexport function spagbolZoomPercentage(zoom: number): string {\n return `${Math.round((normalizedZoom(zoom) / SPAGBOL_ZOOM_BASELINE) * 100)}%`;\n}\n\nexport function spagbolHeaderTitleScale(zoom: number): number {\n const userZoom = normalizedZoom(zoom) / SPAGBOL_ZOOM_BASELINE;\n if (userZoom <= 0.3) return 2.2;\n if (userZoom <= 0.5) return 1.75;\n if (userZoom < 0.7) return 1.3;\n return 1;\n}\n"]}
package/dist/styles.css CHANGED
@@ -1,3 +1,4 @@
1
+ @import "./editors.css";
1
2
  @import "./link-controls.css";
2
3
  @import "./controls.css";
3
4
  @import "@xyflow/react/dist/style.css";
@@ -19,6 +20,7 @@
19
20
  --spagbol-control: rgba(0, 0, 0, 0.62);
20
21
  --spagbol-link-underlay: #0b0b0b;
21
22
  --spagbol-control-size: 28px;
23
+ --spagbol-pin-size: 8px;
22
24
  --spagbol-font:
23
25
  ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
24
26
  monospace;
@@ -81,6 +83,17 @@
81
83
  .spagbol-canvas .react-flow__edgeupdater { fill: transparent; stroke: transparent; stroke-width: 0; cursor: grab; pointer-events: all; }
82
84
  .spagbol-canvas .react-flow__edgeupdater-source { transform: translateX(-12px); }
83
85
  .spagbol-canvas .react-flow__edgeupdater-target { transform: translateX(12px); }
86
+ /* Match Teia's square pins; selected endpoints only change colour. */
87
+ .spagbol-canvas .react-flow__handle.spagbol-node-port,
88
+ .spagbol-canvas .react-flow__handle.spagbol-property-port {
89
+ width: var(--spagbol-pin-size);
90
+ height: var(--spagbol-pin-size);
91
+ min-width: var(--spagbol-pin-size);
92
+ min-height: var(--spagbol-pin-size);
93
+ border: 1px solid var(--spagbol-foreground);
94
+ border-radius: 0;
95
+ background: var(--spagbol-control);
96
+ }
84
97
  .spagbol-canvas .spagbol-node-port.spagbol-node-port-selected-link { background: var(--spagbol-accent); border-color: var(--spagbol-accent); box-shadow: 0 0 0 2px var(--spagbol-link-underlay); pointer-events: none; }
85
98
  .spagbol-canvas-link-overlay { position: absolute; inset: 0; z-index: 25; pointer-events: none; }
86
99
 
@@ -124,7 +137,8 @@
124
137
  opacity: 0.48;
125
138
  border-style: dashed;
126
139
  }
127
- .spagbol-node-frame-header {
140
+ .spagbol-node-frame-header,
141
+ .spagbol-canvas-picker-heading {
128
142
  min-height: 31px;
129
143
  padding: 4px 8px;
130
144
  border-bottom: 1px solid var(--spagbol-border);
@@ -136,7 +150,8 @@
136
150
  cursor: grab;
137
151
  user-select: none;
138
152
  }
139
- .spagbol-node-frame-title {
153
+ .spagbol-node-frame-title,
154
+ .spagbol-canvas-picker-title {
140
155
  flex: 1;
141
156
  min-width: 0;
142
157
  overflow: visible;
@@ -145,7 +160,8 @@
145
160
  font-size: calc(13px * var(--spagbol-title-scale, 1));
146
161
  font-weight: 700;
147
162
  }
148
- .spagbol-canvas .spagbol-node-frame-header-button {
163
+ .spagbol-canvas .spagbol-node-frame-header-button,
164
+ .spagbol-canvas .spagbol-canvas-picker-heading button {
149
165
  border: 0;
150
166
  background: transparent;
151
167
  color: inherit;
@@ -220,11 +236,11 @@
220
236
  height: 1px;
221
237
  pointer-events: none;
222
238
  }
223
- .spagbol-canvas .spagbol-property-port {
239
+ .spagbol-canvas .react-flow__handle.spagbol-property-port {
224
240
  top: 0;
225
241
  left: 0;
226
- width: 9px;
227
- height: 9px;
242
+ width: var(--spagbol-pin-size);
243
+ height: var(--spagbol-pin-size);
228
244
  border: 1px solid var(--spagbol-accent);
229
245
  background: var(--spagbol-bg);
230
246
  pointer-events: auto;
@@ -232,11 +248,17 @@
232
248
  .spagbol-canvas-area {
233
249
  position: relative;
234
250
  border: 1px solid var(--spagbol-border);
235
- background: #ffffff03;
251
+ background:
252
+ linear-gradient(color-mix(in srgb, var(--spagbol-foreground) 8%, transparent), color-mix(in srgb, var(--spagbol-foreground) 8%, transparent)),
253
+ color-mix(in srgb, var(--spagbol-bg) 42%, transparent);
236
254
  pointer-events: none;
237
255
  }
238
256
  .spagbol-canvas-area-selected {
239
- border-color: var(--spagbol-accent);
257
+ outline: 1px dashed var(--spagbol-foreground);
258
+ outline-offset: 3px;
259
+ }
260
+ .spagbol-canvas-area-collapsed {
261
+ overflow: hidden;
240
262
  }
241
263
  .spagbol-canvas-area-header {
242
264
  position: absolute;
@@ -244,29 +266,84 @@
244
266
  left: 0;
245
267
  right: 0;
246
268
  height: 32px;
247
- padding: 5px 10px;
248
- background: var(--spagbol-surface-raised);
249
- color: var(--spagbol-muted);
250
269
  display: flex;
251
- justify-content: space-between;
252
- cursor: move;
270
+ align-items: center;
271
+ padding: 0 8px;
272
+ border-bottom: 1px solid color-mix(in srgb, var(--spagbol-foreground) 28%, transparent);
273
+ color: var(--spagbol-foreground);
274
+ cursor: grab;
253
275
  pointer-events: auto;
254
276
  user-select: none;
255
277
  touch-action: none;
256
278
  }
257
- .spagbol-canvas-area-header > span {
279
+ .spagbol-canvas-area-header:active { cursor: grabbing; }
280
+ .spagbol-canvas-area-title,
281
+ .spagbol-canvas-area-title-input {
258
282
  flex: 1;
259
- font-size: calc(11px * var(--spagbol-title-scale, 1));
283
+ min-width: 0;
284
+ max-width: 100%;
285
+ border: 0;
286
+ padding: 0;
287
+ outline: 0;
288
+ background: transparent;
289
+ color: var(--spagbol-foreground);
290
+ font: inherit;
291
+ font-size: calc(13px * var(--spagbol-title-scale, 1));
292
+ font-weight: 700;
293
+ }
294
+ .spagbol-canvas-area-title {
295
+ overflow: hidden;
296
+ text-overflow: ellipsis;
297
+ white-space: nowrap;
298
+ cursor: text;
260
299
  }
300
+ .spagbol-canvas-area-title-input { width: auto; min-width: 4ch; }
261
301
  .spagbol-canvas-area-actions {
262
302
  display: flex;
263
- gap: 8px;
303
+ flex: 0 0 auto;
304
+ align-items: center;
305
+ gap: 6px;
306
+ margin-left: auto;
264
307
  }
265
- .spagbol-canvas-area-actions button {
266
- border: none;
308
+ .spagbol-canvas .spagbol-canvas-area-actions button {
309
+ display: inline-flex;
310
+ flex: 0 0 auto;
311
+ width: 20px;
312
+ height: 20px;
313
+ align-items: center;
314
+ justify-content: center;
315
+ border: 0;
316
+ padding: 0;
267
317
  background: transparent;
268
- padding: 0 2px;
318
+ color: currentColor;
319
+ cursor: pointer;
320
+ }
321
+ .spagbol-canvas .spagbol-canvas-area-actions button:hover,
322
+ .spagbol-canvas .spagbol-canvas-area-actions button:focus-visible {
323
+ background: var(--spagbol-bg);
324
+ color: var(--spagbol-foreground);
325
+ outline: 0;
269
326
  }
327
+ .spagbol-canvas-area-lock-icon {
328
+ position: relative;
329
+ display: block;
330
+ width: 10px;
331
+ height: 8px;
332
+ border: 1px solid currentColor;
333
+ }
334
+ .spagbol-canvas-area-lock-icon::before {
335
+ position: absolute;
336
+ bottom: 7px;
337
+ left: 1px;
338
+ width: 6px;
339
+ height: 6px;
340
+ border: 1px solid currentColor;
341
+ border-bottom: 0;
342
+ border-radius: 5px 5px 0 0;
343
+ content: '';
344
+ }
345
+ .spagbol-canvas-area-lock:not([aria-pressed="true"]) .spagbol-canvas-area-lock-icon::before { left: 6px; }
346
+ .spagbol-canvas-area-lock[aria-pressed="true"] .spagbol-canvas-area-lock-icon { background: currentColor; }
270
347
  .spagbol-canvas-area-resize {
271
348
  position: absolute;
272
349
  bottom: 0;
@@ -426,10 +503,10 @@
426
503
  gap: 8px;
427
504
  text-align: left;
428
505
  }
429
- .spagbol-canvas-menu-check {
430
- display: inline-block;
431
- width: 12px;
432
- flex-shrink: 0;
506
+ .spagbol-canvas-menu-items > button[aria-checked="true"],
507
+ .spagbol-canvas-actions > button[aria-pressed="true"] {
508
+ background: var(--spagbol-header);
509
+ color: var(--spagbol-header-text);
433
510
  }
434
511
  .spagbol-canvas-menu-separator {
435
512
  height: 1px;
@@ -472,19 +549,7 @@
472
549
  box-shadow: 0 10px 40px #0007;
473
550
  }
474
551
  .spagbol-canvas-picker-heading {
475
- padding: 9px 10px;
476
- display: flex;
477
- align-items: center;
478
- justify-content: space-between;
479
- background: var(--spagbol-header);
480
- color: var(--spagbol-header-text);
481
- }
482
- .spagbol-canvas-picker-heading button {
483
- border: 0;
484
- background: none;
485
- color: inherit;
486
- font-size: 18px;
487
- line-height: 16px;
552
+ cursor: default;
488
553
  }
489
554
  .spagbol-canvas-picker input {
490
555
  margin: 8px;
@@ -509,9 +574,12 @@
509
574
  border: 0;
510
575
  background: transparent;
511
576
  }
512
- .spagbol-canvas-picker-list button:hover {
513
- background: var(--spagbol-surface-raised);
514
- color: var(--spagbol-accent);
577
+ .spagbol-canvas-picker-list button[aria-selected="true"] {
578
+ background: var(--spagbol-header);
579
+ color: var(--spagbol-header-text);
580
+ }
581
+ .spagbol-canvas-picker-list button[aria-selected="true"] small {
582
+ color: inherit;
515
583
  }
516
584
  .spagbol-canvas-picker-list small {
517
585
  color: var(--spagbol-muted);
@@ -739,10 +807,13 @@
739
807
  }
740
808
 
741
809
 
742
- .spagbol-canvas-area-title, .spagbol-canvas-area-title-input { flex: 1; min-width: 0; }
743
- .spagbol-canvas-area-title-input { border: 0; padding: 0; color: inherit; background: transparent; font: inherit; outline: none; }
744
810
 
745
811
  .spagbol-node-frame-projected > .spagbol-node-frame-header { background: var(--spagbol-bg); color: var(--spagbol-foreground); cursor: pointer; }
746
812
  .spagbol-node-panel { border-bottom: 1px dashed var(--spagbol-foreground); background: var(--spagbol-bg); }
747
813
 
748
814
  .spagbol-node-frame-auto-compact { min-width: 0; }
815
+
816
+ .spagbol-canvas .spagbol-node-frame-enter { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 30px; width: 30px; height: 30px; padding: 0; cursor: pointer; }
817
+ .spagbol-node-frame-enter svg { display: block; flex: none; }
818
+ .spagbol-node-frame-enter + .spagbol-node-frame-enabled { margin-left: 0; }
819
+ .spagbol-node-frame-enter:hover { background: var(--spagbol-bg); color: var(--spagbol-foreground); }
@@ -0,0 +1,92 @@
1
+ # Standard nodes: Teia and Visual Visual comparison
2
+
3
+ **Source review:** 2026-11-09, against the [baseline 0.4 contracts](standard-node-contracts.md). This is the phase 1 comparison and decision record for phases 2–3. It is not an execution, audio, browser or hardware conformance report.
4
+
5
+ ## Evidence boundary
6
+
7
+ Reviewed sibling checkouts:
8
+
9
+ | Repository | HEAD at review | Working-tree qualification |
10
+ | --- | --- | --- |
11
+ | Spagbol | `43e613cb685533c6812f97cc22b35c9ea9d1ed44` | Clean before this documentation task. Core/UI, not an evaluator. |
12
+ | Teia | `7f8e655539ad185bd58ee08117780be4a5b32573` | Clean at review. Definitions, compiler and Rust source inspected; no app execution. |
13
+ | Visual Visual | `0e409ecfa6614ff0bc27bde8bffab6484aaf5ed3` | Existing uncommitted editor, preview and persistence work. Observations describe the inspected working tree, not a claim that every detail is in HEAD. No files changed by this task. |
14
+
15
+ Source paths below are repository-relative evidence locations; function names identify the reviewed implementation without depending on shifting line numbers. HEAD hashes record provenance, not a promise about future versions.
16
+
17
+ | Ref | Repository and evidence |
18
+ | --- | --- |
19
+ | S1 | Spagbol `src/core/model.ts`: `PortDefinition`, `GraphNode`, `GraphPolicy`, `standardLinkModes`, `NodeRegistry`; `src/core/groups.ts`: `normalizeGroupPortLabel`, stable port creation. |
20
+ | S2 | Spagbol `README.md`: rich links, nested graphs, shared slider/button controls and host execution ownership; `src/react/Controls.tsx`. |
21
+ | T1 | Teia `editor/src/graph/nodeTypes.ts`: `NODE_DEFINITIONS`, `getNodeDefinition`, `sequencerShape` and pattern/gate helpers. |
22
+ | T2 | Teia `editor/src/audio/dspProgram.ts`: `ExpressionParser`, `EXPRESSION_FUNCTIONS`, `compileSelector`, `compileSequencer`, `resolveSliderUnitValue`, `applySliderCurve`, `resolveJoystickUnitValue` and per-type lowering. |
23
+ | T3 | Teia `rust/teia-kernel/src/lib.rs`: `render_dsp_selector`, `render_dsp_accumulator`, `advance_accumulator`, `render_dsp_random`, `render_dsp_function`, `sign_preserving_pow`, `DSP_OP_MAP` and repeat/Spawn scheduling in `render_dsp_ops`. |
24
+ | T4 | Teia `editor/src/graph/spread.ts`: `expandSpreads`, `expandOneSpread`, membership checks; `editor/src/audio/dspProgram.ts` block-latch lowering; `README.md` Spreads / Conditional Execution / Spawns. |
25
+ | T5 | Teia `editor/src/graph/customWave.ts`: `DEFAULT_CUSTOM_WAVE`, normalization, banks, base-level mapping and curve evaluation helpers; `editor/src/editor/TeiaNodeContent.tsx`: curve, sequencer, joystick and parameter editing. |
26
+ | T6 | Teia `firmware/fm1/support.json` and generated `firmware/fm1/SUPPORT.md`: authoritative firmware capability/verification contract. This document is not a second firmware support list. |
27
+ | V1 | Visual Visual `web/src/graph/nodeTypes.ts`: `NODE_DEFINITIONS`, unary/binary helpers and dynamic Expression ports. |
28
+ | V2 | Visual Visual `web/src/graph/glsl.ts`: `emitExpression`, `emitMap`, output resolution cases for maths/Noise, `emitEnvelope`, `collectEnvelopeSlots` and GLSL noise helpers. |
29
+ | V3 | Visual Visual `web/src/graph/expression.ts`: symbol extraction/replacement and float-literal normalization; `web/src/editor/WebGLPreview.tsx`: frame scheduling, uniforms and feedback textures. |
30
+
31
+ ## Current infrastructure versus language intent
32
+
33
+ The proposal's float-signal language is a design contract, not a description of type enforcement already implemented in Spagbol. S1 exposes an optional arbitrary `dataType` string, numeric parameter storage, JSON node data, custom mode descriptors and host connection policy. S2 deliberately leaves numeric mode arithmetic, feedback and runtime scheduling to applications. Group/Ins/Outs editing is shared; Spread/Spawn/Conditional execution is not implemented by the library.
34
+
35
+ Keep numeric links and non-pin configuration distinct when adopting the conventions. Do not infer that an editor control, mode descriptor or nested graph already implements a node evaluator. The initial phases 1–3 review made no API/runtime changes. Phase 4 adds controlled UI exports and local packed consumption, leaving patch schemas and evaluators unchanged; see [UI components](ui-components.md).
36
+
37
+ ## All 26 recommended nodes
38
+
39
+ “Absent” means no named entry in the inspected application catalogue (T1/V1), not proof the behaviour is impossible to express in a graph. Similar arithmetic does not establish matching numeric edge cases or scheduler behaviour. Labels shown in this table are existing app names/IDs, not new recommended labels.
40
+
41
+ | Node | Teia source behaviour | Visual Visual source behaviour | Baseline decision / remaining difference |
42
+ | --- | --- | --- | --- |
43
+ | Expression | Dynamic inputs → `value`; parsed scalar dialect, arithmetic/comparisons/logical operators and fixed function table; compiler diagnostics for unsupported syntax/functions (T1/T2). | Dynamic inputs → `value`; source substituted into GLSL after input discovery and float-literal normalization (V1–V3). | Specify a small portable arithmetic/function subset and explicit dialects. GLSL availability, syntax, boolean results and Teia's guarded functions are not automatically portable. |
44
+ | Constant | `value=1` → `signal` (T1/T2). | `value=1` → `value` (V1/V2). | Same basic role/default. Output label differs; no rename required now. |
45
+ | Pass | `signal` → `signal` (T1/T2). | Absent (V1). | Recommend transparent float routing; no new node installed. |
46
+ | Abs | `signal` → `signal` (T1–T3). | `value` → `value`, GLSL `abs` (V1/V2). | Same finite arithmetic, different labels; non-finite parity not established. |
47
+ | Add | No named Add catalogue entry; Expression and Add links can express addition (T1/T2). | `a=0`, `b=0` → `value`, scalar addition (V1/V2). | Recommend `signal=0`, `amount=0` → `signal`. Names differ; no Teia node is installed by this specification. |
48
+ | Subtract | No named catalogue entry; Expression supports subtraction (T1/T2). | `a=0`, `b=0` → `value` (V1/V2). | Same finite arithmetic; preserve left-minus-right operand order. |
49
+ | Divide | No named catalogue entry; Expression supports guarded division (T1/T2). | `a=0`, `b=1` → `value`, raw GLSL division (V1/V2). | Baseline exact-zero fallback is explicit; raw GPU zero/overflow results are not portable. |
50
+ | Modulo | No named catalogue entry (T1). | `Mod`, `a=0`, `b=1` → `value`, GLSL `mod` (V1/V2). | Choose floor-based float modulo, not JavaScript `%` remainder. Explicit zero fallback still requires an app adapter. |
51
+ | Min | No named catalogue entry; Expression has `min` (T1/T2). | `a=0`, `b=1` → `value` (V1/V2). | Baseline recommends both inputs default 0; Visual Visual's second default is 1. |
52
+ | Max | No named catalogue entry; Expression has `max` (T1/T2). | `a=0`, `b=1` → `value` (V1/V2). | Same default difference as Min. Finite comparison intent aligns. |
53
+ | Multiply | `signal=0`, `factor=1` → `signal` (T1/T2). | `a=0`, `b=1` → `value` (V1/V2). | Equivalent finite multiplication; labels differ. Link mode remains separate. |
54
+ | Pow | `signal=0`, `exponent=1`; `sign(base) * abs(base)^exponent`, with zero base forced to 0 (T1–T3). | `a=1`, `b=1`; raw GLSL `pow(a,b)` (V1/V2). | Baseline chooses mathematical real power with explicit invalid-domain fallback. Teia `(-2)^2=-4` and `0^0=0` differ from baseline 4 and 1. GLSL negative-base behaviour cannot be assumed to match either. Teia sign-preserving behaviour remains an app profile. |
55
+ | Map | `signal`, `srcMin`, `srcMax`, `trgtMin`, `trgtMax`; source span magnitude floored to `1e-6` with sign retained (T1/T3). | Same interval parameter IDs with `value` input; span below `1e-6` in magnitude replaced by **positive** `1e-6` (V1/V2). | Neither clamps. Baseline equal source bounds → target min; other finite nonzero spans use actual span. Both epsilon guards differ; a tiny reversed interval can even reverse direction differently between apps. |
56
+ | Clamp | `signal`, `min=0`, `max=1`; sorts bounds in scalar function (T1–T3). | `value`, `min=0`, `max=1`; direct GLSL `clamp` with unsorted arguments (V1/V2). | Baseline sorts reversed bounds as Teia does. Visual Visual does not encode this guarantee. |
57
+ | Selector | One-based numeric input names; floors selection, zero/out-of-range retains active selection; temporal `slide` in seconds (T1–T3). | Absent (V1). | Adopt latching intent, use human-readable labels/stable identities. Baseline switch tick starts slide elapsed at 0; Teia advances by one sample during rendering. Sparse numeric port indices, removal/reorder migration and dynamic slide edits need profile-specific checks. |
58
+ | Conditional Execution | `gate=0`; `>=0.5` executes per sample; closed scope skips instructions, retains state/resources, zeroes outgoing registers (T1/T3/T4). | Absent (V1). Numeric `Gate` passes/suppresses a value and does not establish execution suppression (V2). | Adopt execution semantics, not a numeric conditional. Record external side effects, scope restrictions and entity visibility in each profile. |
59
+ | Spread | `count=1`, one-based `item index`; nonnegative floored count, compiler block-latched; independent states/resources; shrinking resizes/truncates active storage (T1–T4). | Absent (V1). | Baseline explicitly retains suspended indices on shrink for stable persistent instances. Current dynamic Teia destroys removed state/resources and regrowth is clean; this is a recorded deviation, not a claim that Teia already retains it. Block versus tick count sampling also differs. |
60
+ | Spawn | Trigger/release edges at 0.5, per-instance gate/kill, independent state/resources. Scalar scheduler releases existing tags **before** creation, matching with absolute tolerance `1e-6`, and evaluates/collects before kill disposal (T1/T3/T4). | Absent (V1). | Baseline creates before release and uses exact tag equality; these are proposed explicit choices. Teia simultaneous matching trigger/release leaves the new instance gated on. Killing after final evaluation aligns in the inspected scalar path; other execution profiles need their own evidence. |
61
+ | Accumulator | Trigger or continuous mode; rising reset wins; sorted bounds; overshoot jumps to opposite endpoint (T1–T3). State begins as zero and is clamped to bounds. | Absent (V1). | Preserve edge/reset/endpoint-wrap semantics. Baseline initializes at low: for bounds -1..1 and no trigger its first value is -1; Teia's initially zero state remains 0. Continuous mode is per sample in Teia and per declared tick in the baseline. |
62
+ | Random | Initial draw then hold until trigger rise; per-node generator state; live range mapping, defaults -1..1; transport restart reseeds (T1–T3, README Random). | Absent (V1). | Adopt held normalized draw plus live mapping. Algorithm/seed/restart interoperability is not established. No cross-app identical sequence promised. |
63
+ | Noise | Raw temporal noise, default range -1..1 (T1–T3). | `x`, `y`, `scale=12`, `seed=0`, `octaves=4`; spatial coherent noise through GLSL helpers, plus separate 3D variants (V1/V2). | Baseline names temporal uniform noise; Visual Visual's coherent coordinate field is a different profile, not identical white-noise semantics. Pin names, units, ranges and distributions must be explicit. |
64
+ | Slider | `value=0.5`, curve shaping, range mapping/inverses, optional routed input signals, orientation and MIDI (T1/T2/T5). | No Slider catalogue node; scalar parameter widgets do not establish this node contract (V1). | Baseline specifies linear mapping and inverse. Curve, routing and MIDI are app extensions. Shared Spagbol slider is UI only (S2). |
65
+ | Button | Gate/toggle/trigger-style saved UI state, complementary signal routing, smoothing and MIDI (T1/T2/T5). | Absent (V1). | Baseline distinguishes numeric temporary/toggle/pulse transitions and event delivery. Shared UI's 120ms flash is not DSP pulse width; host adapters and scheduling remain necessary (S2). |
66
+ | Joystick | Saved normalized x/y default 0.5; min/max and inverse per axis, bottom-to-top Y, optional elastic centre return and MIDI (T1/T2/T5). | Absent (V1). | Adopt position/range intent; retain elasticity and MIDI as extensions. Joystick now uses the shared controlled component; saved positions, ranges and MIDI remain Teia-owned. |
67
+ | Curve | `CustomWave` / “Custom Wave”: waveform timing, frequency, phase, trigger, mode, multiple curves, base level, range and end triggers. Existing normalized model fixes endpoint values and supports sustain regions (T1/T5). | Absent (V1). | Standard is external `position` → `value`, no internal clock. Baseline generic curve allows arbitrary finite values; Teia normalizes stored points to -1..1. Preserve banks, endpoints and advanced editing with a lossless extraction adapter; do not rename/remove the musical wrapper. |
68
+ | Sequence | `Sequencer`: signal/reset-driven musical trigger or gate rows, beat length, row count, labels, fractional positions, velocities and `trigger index`; dynamic numeric row ports (T1/T2/T5). | Absent (V1). | Standard is normalized external `position` lookup of numeric cells, not a clock or event engine. Teia's event model is not already this evaluator. Specify baseline step boundaries now; use app-owned serialization adapters in phase 4 without losing richer Teia data. |
69
+
70
+ ## App-specific exclusions
71
+
72
+ Envelope, Playhead and Time were removed from the recommended set at the user's request. Their existing nodes remain untouched. Teia's gate/trigger ADSR envelope and Visual Visual's attack/release frame follower already illustrate why the name alone is insufficient to standardize them. Visual Visual's `System.time` and Teia's sample-advanced Time have different clock ownership; neither is made a shared clock by this work. Teia's normalized Playhead remains part of its own catalogue. These observations do not establish new shared contracts for the excluded nodes.
73
+
74
+ Group, Ins and Outs remain shared editing/interface constructs. Teia Params still needs a separate interface review. Visual scene entities, shaders, audio/MIDI and domain-specific DSP remain application-owned. The proposal's Sphere/Light/Painty/Image/Video examples are possible scene vocabulary, not a claim that the inspected Visual Visual catalogue already contains them.
75
+
76
+ ## Execution profiles still required
77
+
78
+ - **Teia web:** sample-driven Rust/WASM state, buffer-latched Spread counts, parameter smoothing and compiler-defined link accumulation. The inspected scalar path and README establish source intent; this task has not exercised state migration, side effects or timing. Existing nesting/overlap restrictions in T4 must be documented by an adopting profile; visual nesting alone does not establish executable nested containers.
79
+ - **Visual Visual:** GLSL evaluation per fragment, frame feedback via textures and UI-clock uniforms (V2/V3). Pure expressions can be inlined; persistent effects use explicit state textures. There is no evidence here for general execution-area or independently lived scene-instance support.
80
+ - **FM-1 / hosted / bounded policies:** consult T6 and their own source/evidence. Fixed pools, trigger drops, capped counts, reload resets and firmware support cannot be inferred from Teia web behaviour. This document intentionally adds no independent capability claims or version promises.
81
+
82
+ The proposed finite-value fallback, arithmetic error policy, low-bound Accumulator initialization, slide timing, shrink retention, exact Spawn matching, simultaneous lifecycle order and UI pulse queue are **design choices**. They are not source-discovered consensus. Applications may keep their existing behaviour by declaring deviations; any future migration must assess saved patches and performance first.
83
+
84
+ ## Phases 1–3 completion and follow-up boundary
85
+
86
+ 1. Proposal published in Spagbol, linked from README; all 26 nodes compared against both actual catalogues and relevant compiler/runtime source.
87
+ 2. Six anchor contracts specify pins, configuration, execution/state/lifecycle order, errors, extension points and examples.
88
+ 3. Twenty remaining contracts specify defaults, formulas, ranges, numeric edge cases and examples; Curve/Sequence logical position semantics are established without prematurely freezing their editor wire formats.
89
+
90
+ Phase 4 now provides controlled UI editors, used by Teia. There is no shared persistence model or model-edit engine. Visual Visual adoption is still future work. Phase 5 remains optional executable conformance tooling; evaluators remain app-owned. Phase 6 remains functional cross-application validation and separate performance measurements. Nothing in this documentation task certifies conformance or authorizes a runtime migration.
91
+
92
+ The initial phases 1–3 documentation did not touch app source. Phase 4 updates Teia UI imports/adapters and styles, with no changes to ports, saved models, compiler, DSP or firmware. Teia README and reviewed source fingerprints are updated for that extraction. FM-1 feature statuses and implementation/verification claims remain unchanged: this is a UI ownership refactor, not a web-engine capability change.