@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.
Files changed (66) hide show
  1. package/README.md +42 -4
  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 +106 -29
  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 +18 -16
  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/nodeDrag.d.ts +7 -0
  39. package/dist/react/nodeDrag.d.ts.map +1 -0
  40. package/dist/react/nodeDrag.js +27 -0
  41. package/dist/react/nodeDrag.js.map +1 -0
  42. package/dist/react/zoom.d.ts +5 -0
  43. package/dist/react/zoom.d.ts.map +1 -0
  44. package/dist/react/zoom.js +19 -0
  45. package/dist/react/zoom.js.map +1 -0
  46. package/dist/styles.css +112 -41
  47. package/docs/language/standard-node-comparison.md +92 -0
  48. package/docs/language/standard-node-contracts.md +237 -0
  49. package/docs/language/standard-nodes.md +198 -0
  50. package/docs/language/ui-components.md +79 -0
  51. package/package.json +5 -3
  52. package/src/core/controller.ts +4 -4
  53. package/src/core/model.ts +2 -1
  54. package/src/react/SpagbolCanvas.tsx +121 -31
  55. package/src/react/SpagbolCanvasControls.tsx +2 -7
  56. package/src/react/SpagbolControlJoystick.tsx +147 -0
  57. package/src/react/SpagbolControlZoom.tsx +3 -2
  58. package/src/react/SpagbolCurveEditor.tsx +166 -0
  59. package/src/react/SpagbolNodeFrame.tsx +30 -24
  60. package/src/react/SpagbolNodeFrameTypePicker.tsx +1 -1
  61. package/src/react/SpagbolSequencer.tsx +265 -0
  62. package/src/react/editors.css +157 -0
  63. package/src/react/index.ts +4 -0
  64. package/src/react/nodeDrag.ts +33 -0
  65. package/src/react/styles.css +112 -41
  66. package/src/react/zoom.ts +18 -0
@@ -1 +1 @@
1
- {"version":3,"file":"SpagbolCanvasControls.d.ts","sourceRoot":"","sources":["../../src/react/SpagbolCanvasControls.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAKzD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AACD,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,IAAI,CAAC;CACjB;AACD,MAAM,MAAM,qBAAqB,GAC7B,mBAAmB,GACnB,0BAA0B,CAAC;AAC/B,MAAM,WAAW,4BAA4B;IAC3C,6EAA6E;IAC7E,OAAO,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACzC,SAAS,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACnE;AAKD,mFAAmF;AACnF,wBAAgB,yBAAyB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,+BAUzE;AAED,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,4BAA4B,+BA+JzE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,+BAuC1E"}
1
+ {"version":3,"file":"SpagbolCanvasControls.d.ts","sourceRoot":"","sources":["../../src/react/SpagbolCanvasControls.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAKzD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AACD,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,IAAI,CAAC;CACjB;AACD,MAAM,MAAM,qBAAqB,GAC7B,mBAAmB,GACnB,0BAA0B,CAAC;AAC/B,MAAM,WAAW,4BAA4B;IAC3C,6EAA6E;IAC7E,OAAO,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACzC,SAAS,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACnE;AAKD,mFAAmF;AACnF,wBAAgB,yBAAyB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,+BAUzE;AAED,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,4BAA4B,+BA0JzE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,+BAuC1E"}
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useReactFlow } from "@xyflow/react";
3
- import { isGroupBoundary } from "../core/index.js";
3
+ import { graphAt, isGroupBoundary } from "../core/index.js";
4
4
  import { useRef, useState } from "react";
5
5
  import { SpagbolControlZoom } from "./SpagbolControlZoom.js";
6
6
  import { SpagbolCanvasMenu } from "./SpagbolCanvasMenu.js";
@@ -95,19 +95,19 @@ export function SpagbolCanvasActions(options) {
95
95
  }
96
96
  }
97
97
  function actionButton(action, fromMenu = false) {
98
- return (_jsxs("button", { type: "button", role: fromMenu
98
+ return (_jsx("button", { type: "button", role: fromMenu
99
99
  ? action.checked === undefined
100
100
  ? "menuitem"
101
101
  : "menuitemcheckbox"
102
102
  : undefined, "aria-label": labelFor(action), title: action.title ?? labelFor(action), disabled: action.disabled || busy.has(action.id), "aria-busy": busy.has(action.id) || undefined, "aria-checked": fromMenu ? action.checked : undefined, "aria-pressed": fromMenu ? undefined : action.checked, onClick: () => {
103
103
  void invoke(action, fromMenu);
104
- }, children: [action.checked !== undefined && fromMenu ? (_jsx("span", { className: "spagbol-canvas-menu-check", "aria-hidden": "true", children: action.checked ? "✓" : "" })) : null, action.label] }, action.id));
104
+ }, children: action.label }, action.id));
105
105
  }
106
106
  return (_jsxs("div", { className: "spagbol-canvas-actions nodrag nopan", role: "group", "aria-label": "Canvas actions", onPointerDown: (event) => event.stopPropagation(), onDoubleClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: [options.leading && (_jsx("div", { className: "spagbol-canvas-actions-leading", children: options.leading })), buttons.map((action) => actionButton(action)), hasMenu && (_jsx(SpagbolCanvasMenu, { label: _jsxs(_Fragment, { children: [options.menuLabel ?? "MORE", " ", _jsx("span", { "aria-hidden": "true", children: "\u02C6" })] }), children: items.map((item) => "separator" in item ? (_jsx("div", { role: "separator", className: "spagbol-canvas-menu-separator" }, item.id)) : (actionButton(item, true))) })), error && (_jsxs("div", { className: "spagbol-canvas-actions-error", role: "alert", children: [error, _jsx("button", { type: "button", "aria-label": "Dismiss action error", onClick: () => setError(null), children: "\u00D7" })] }))] }));
107
107
  }
108
108
  export function SpagbolCanvasControls(options) {
109
109
  const { editor } = useSpagbol();
110
110
  const snapshot = useEditorSnapshot(editor);
111
- return (_jsxs(_Fragment, { children: [options.showBreadcrumbs !== false && snapshot.path.length > 0 && (_jsxs("nav", { className: "spagbol-canvas-breadcrumbs nodrag nopan", "aria-label": "Graph navigation", onPointerDown: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: [_jsx("button", { type: "button", onClick: () => editor.navigate([]), children: "ROOT" }), snapshot.path.map((id, index) => (_jsx("button", { type: "button", "aria-current": index === snapshot.path.length - 1 ? "page" : undefined, onClick: () => editor.navigate(snapshot.path.slice(0, index + 1)), children: id }, `${index}:${id}`)))] })), _jsxs("div", { className: `spagbol-canvas-controls${options.showZoom === false ? " spagbol-canvas-controls-no-zoom" : ""}`, children: [options.showZoom !== false && (_jsx(SpagbolCanvasControlsZoom, { extra: options.zoomExtra })), _jsx(SpagbolCanvasActions, { ...options })] })] }));
111
+ return (_jsxs(_Fragment, { children: [options.showBreadcrumbs !== false && snapshot.path.length > 0 && (_jsxs("nav", { className: "spagbol-canvas-breadcrumbs nodrag nopan", "aria-label": "Graph navigation", onPointerDown: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: [_jsx("button", { type: "button", onClick: () => editor.navigate([]), children: "ROOT" }), snapshot.path.map((id, index) => (_jsx("button", { type: "button", "aria-current": index === snapshot.path.length - 1 ? "page" : undefined, onClick: () => editor.navigate(snapshot.path.slice(0, index + 1)), children: graphAt(snapshot.document, snapshot.path.slice(0, index)).nodes.find(node => node.id === id)?.label?.trim() || 'Group' }, `${index}:${id}`)))] })), _jsxs("div", { className: `spagbol-canvas-controls${options.showZoom === false ? " spagbol-canvas-controls-no-zoom" : ""}`, children: [options.showZoom !== false && (_jsx(SpagbolCanvasControlsZoom, { extra: options.zoomExtra })), _jsx(SpagbolCanvasActions, { ...options })] })] }));
112
112
  }
113
113
  //# sourceMappingURL=SpagbolCanvasControls.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SpagbolCanvasControls.js","sourceRoot":"","sources":["../../src/react/SpagbolCanvasControls.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useReactFlow } from \"@xyflow/react\";\nimport { isGroupBoundary } from \"../core/index.js\";\nimport { useRef, useState, type ReactNode } from \"react\";\nimport { SpagbolControlZoom } from \"./SpagbolControlZoom.js\";\nimport { SpagbolCanvasMenu } from \"./SpagbolCanvasMenu.js\";\nimport { useEditorSnapshot, useSpagbol } from \"./context.js\";\n\nexport interface SpagbolCanvasAction {\n id: string;\n label: ReactNode;\n ariaLabel?: string;\n title?: string;\n disabled?: boolean;\n checked?: boolean;\n onSelect(): void | Promise<void>;\n}\nexport interface SpagbolCanvasMenuSeparator {\n id: string;\n separator: true;\n}\nexport type SpagbolCanvasMenuItem =\n | SpagbolCanvasAction\n | SpagbolCanvasMenuSeparator;\nexport interface SpagbolCanvasControlsOptions {\n /** Host actions beside the menu; history buttons follow these by default. */\n buttons?: readonly SpagbolCanvasAction[];\n menuItems?: readonly SpagbolCanvasMenuItem[];\n menuLabel?: string;\n leading?: ReactNode;\n zoomExtra?: ReactNode;\n showZoom?: boolean;\n showHistory?: boolean;\n showLayoutLock?: boolean;\n showBreadcrumbs?: boolean;\n onActionError?(error: unknown, action: SpagbolCanvasAction): void;\n}\nconst labelFor = (action: SpagbolCanvasAction) =>\n action.ariaLabel ??\n (typeof action.label === \"string\" ? action.label : action.id);\n\n/** Standard zoom stack and percentage/reset button, independent of app actions. */\nexport function SpagbolCanvasControlsZoom({ extra }: { extra?: ReactNode }) {\n const { zoom } = useSpagbol();\n return (\n <SpagbolControlZoom\n className=\"spagbol-canvas-controls-zoom\"\n zoom={zoom}\n extra={extra}\n layout=\"corner\"\n />\n );\n}\n\n/** Teia/visual-visual corner menu. Domain actions stay in the consumer. */\nexport function SpagbolCanvasActions(options: SpagbolCanvasControlsOptions) {\n const flow = useReactFlow();\n const scale = (factor: number) => editor.scaleSelection(factor, new Map(flow.getNodes().filter(node => node.id.startsWith(\"node:\")).map(node => [node.id.slice(5), { width: node.measured?.width ?? node.width ?? 180, height: node.measured?.height ?? node.height ?? 100 }])));\n const { editor } = useSpagbol();\n const snapshot = useEditorSnapshot(editor);\n const [busy, setBusy] = useState<ReadonlySet<string>>(new Set());\n const busyRef = useRef(new Set<string>());\n const [error, setError] = useState<string | null>(null);\n const buttons: SpagbolCanvasAction[] = [\n ...(snapshot.path.length\n ? [\n {\n id: \"spagbol:exit\",\n label: \"EXIT\",\n ariaLabel: \"Exit nested graph\",\n onSelect: () => editor.exit(),\n },\n ]\n : []),\n ...(options.buttons ?? []),\n ...(options.showHistory === false\n ? []\n : [\n {\n id: \"spagbol:undo\",\n label: \"UNDO\",\n ariaLabel: \"Undo\",\n title: \"Undo (Cmd/Ctrl+Z)\",\n disabled: !snapshot.canUndo || !editor.editing,\n onSelect: () => editor.undo(),\n },\n {\n id: \"spagbol:redo\",\n label: \"REDO\",\n ariaLabel: \"Redo\",\n title: \"Redo (Cmd/Ctrl+Shift+Z)\",\n disabled: !snapshot.canRedo || !editor.editing,\n onSelect: () => editor.redo(),\n },\n ]),\n ];\n const members = editor.graph.nodes.filter(node => snapshot.selection.nodeIds.includes(node.id) && !isGroupBoundary(node));\n const exposed = editor.graph.data?.exposedNodeIds;\n const exposureChecked = members.length > 0 && Array.isArray(exposed) && members.every(node => exposed.includes(node.id));\n const items: SpagbolCanvasMenuItem[] = [\n { id: 'spagbol:group', label: 'GROUP TO SUBPATCH', ariaLabel: 'Group to subpatch', disabled: !editor.editing || !members.length, onSelect: () => { editor.groupSelection(); } },\n\n { id: 'spagbol:scale-up', label: 'SCALE UP SELECTION', disabled: !editor.editing || !snapshot.selection.nodeIds.length, onSelect: () => scale(2) },\n { id: 'spagbol:scale-down', label: 'SCALE DOWN SELECTION', disabled: !editor.editing || !snapshot.selection.nodeIds.length, onSelect: () => scale(0.5) },\n ...(options.menuItems ?? []),\n ...(options.showLayoutLock === false\n ? []\n : [\n {\n id: \"spagbol:lock\",\n label: \"LOCK UI\",\n ariaLabel: snapshot.mode === \"edit\" ? \"Lock UI\" : \"Unlock UI\",\n checked: snapshot.mode !== \"edit\",\n disabled: snapshot.mode === \"readonly\",\n onSelect: () =>\n editor.setMode(editor.editing ? \"controls\" : \"edit\"),\n },\n ]),\n ];\n const hasMenu = items.some((item) => !(\"separator\" in item));\n async function invoke(action: SpagbolCanvasAction, fromMenu: boolean) {\n if (action.disabled || busyRef.current.has(action.id)) return;\n busyRef.current.add(action.id);\n setBusy(new Set(busyRef.current));\n setError(null);\n try {\n await action.onSelect();\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n options.onActionError?.(cause, action);\n } finally {\n busyRef.current.delete(action.id);\n setBusy(new Set(busyRef.current));\n }\n }\n function actionButton(action: SpagbolCanvasAction, fromMenu = false) {\n return (\n <button\n key={action.id}\n type=\"button\"\n role={\n fromMenu\n ? action.checked === undefined\n ? \"menuitem\"\n : \"menuitemcheckbox\"\n : undefined\n }\n aria-label={labelFor(action)}\n title={action.title ?? labelFor(action)}\n disabled={action.disabled || busy.has(action.id)}\n aria-busy={busy.has(action.id) || undefined}\n aria-checked={fromMenu ? action.checked : undefined}\n aria-pressed={fromMenu ? undefined : action.checked}\n onClick={() => {\n void invoke(action, fromMenu);\n }}\n >\n {action.checked !== undefined && fromMenu ? (\n <span className=\"spagbol-canvas-menu-check\" aria-hidden=\"true\">\n {action.checked ? \"✓\" : \"\"}\n </span>\n ) : null}\n {action.label}\n </button>\n );\n }\n return (\n <div\n className=\"spagbol-canvas-actions nodrag nopan\"\n role=\"group\"\n aria-label=\"Canvas actions\"\n onPointerDown={(event) => event.stopPropagation()}\n onDoubleClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n >\n {options.leading && (\n <div className=\"spagbol-canvas-actions-leading\">{options.leading}</div>\n )}\n {buttons.map((action) => actionButton(action))}\n {hasMenu && (\n <SpagbolCanvasMenu\n label={\n <>\n {options.menuLabel ?? \"MORE\"} <span aria-hidden=\"true\">ˆ</span>\n </>\n }\n >\n {items.map((item) =>\n \"separator\" in item ? (\n <div\n key={item.id}\n role=\"separator\"\n className=\"spagbol-canvas-menu-separator\"\n />\n ) : (\n actionButton(item, true)\n ),\n )}\n </SpagbolCanvasMenu>\n )}\n {error && (\n <div className=\"spagbol-canvas-actions-error\" role=\"alert\">\n {error}\n <button\n type=\"button\"\n aria-label=\"Dismiss action error\"\n onClick={() => setError(null)}\n >\n ×\n </button>\n </div>\n )}\n </div>\n );\n}\n\nexport function SpagbolCanvasControls(options: SpagbolCanvasControlsOptions) {\n const { editor } = useSpagbol();\n const snapshot = useEditorSnapshot(editor);\n return (\n <>\n {options.showBreadcrumbs !== false && snapshot.path.length > 0 && (\n <nav\n className=\"spagbol-canvas-breadcrumbs nodrag nopan\"\n aria-label=\"Graph navigation\"\n onPointerDown={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n >\n <button type=\"button\" onClick={() => editor.navigate([])}>\n ROOT\n </button>\n {snapshot.path.map((id, index) => (\n <button\n key={`${index}:${id}`}\n type=\"button\"\n aria-current={\n index === snapshot.path.length - 1 ? \"page\" : undefined\n }\n onClick={() => editor.navigate(snapshot.path.slice(0, index + 1))}\n >\n {id}\n </button>\n ))}\n </nav>\n )}\n <div\n className={`spagbol-canvas-controls${options.showZoom === false ? \" spagbol-canvas-controls-no-zoom\" : \"\"}`}\n >\n {options.showZoom !== false && (\n <SpagbolCanvasControlsZoom extra={options.zoomExtra} />\n )}\n <SpagbolCanvasActions {...options} />\n </div>\n </>\n );\n}\n"]}
1
+ {"version":3,"file":"SpagbolCanvasControls.js","sourceRoot":"","sources":["../../src/react/SpagbolCanvasControls.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useReactFlow } from \"@xyflow/react\";\nimport { graphAt, isGroupBoundary } from \"../core/index.js\";\nimport { useRef, useState, type ReactNode } from \"react\";\nimport { SpagbolControlZoom } from \"./SpagbolControlZoom.js\";\nimport { SpagbolCanvasMenu } from \"./SpagbolCanvasMenu.js\";\nimport { useEditorSnapshot, useSpagbol } from \"./context.js\";\n\nexport interface SpagbolCanvasAction {\n id: string;\n label: ReactNode;\n ariaLabel?: string;\n title?: string;\n disabled?: boolean;\n checked?: boolean;\n onSelect(): void | Promise<void>;\n}\nexport interface SpagbolCanvasMenuSeparator {\n id: string;\n separator: true;\n}\nexport type SpagbolCanvasMenuItem =\n | SpagbolCanvasAction\n | SpagbolCanvasMenuSeparator;\nexport interface SpagbolCanvasControlsOptions {\n /** Host actions beside the menu; history buttons follow these by default. */\n buttons?: readonly SpagbolCanvasAction[];\n menuItems?: readonly SpagbolCanvasMenuItem[];\n menuLabel?: string;\n leading?: ReactNode;\n zoomExtra?: ReactNode;\n showZoom?: boolean;\n showHistory?: boolean;\n showLayoutLock?: boolean;\n showBreadcrumbs?: boolean;\n onActionError?(error: unknown, action: SpagbolCanvasAction): void;\n}\nconst labelFor = (action: SpagbolCanvasAction) =>\n action.ariaLabel ??\n (typeof action.label === \"string\" ? action.label : action.id);\n\n/** Standard zoom stack and percentage/reset button, independent of app actions. */\nexport function SpagbolCanvasControlsZoom({ extra }: { extra?: ReactNode }) {\n const { zoom } = useSpagbol();\n return (\n <SpagbolControlZoom\n className=\"spagbol-canvas-controls-zoom\"\n zoom={zoom}\n extra={extra}\n layout=\"corner\"\n />\n );\n}\n\n/** Teia/visual-visual corner menu. Domain actions stay in the consumer. */\nexport function SpagbolCanvasActions(options: SpagbolCanvasControlsOptions) {\n const flow = useReactFlow();\n const scale = (factor: number) => editor.scaleSelection(factor, new Map(flow.getNodes().filter(node => node.id.startsWith(\"node:\")).map(node => [node.id.slice(5), { width: node.measured?.width ?? node.width ?? 180, height: node.measured?.height ?? node.height ?? 100 }])));\n const { editor } = useSpagbol();\n const snapshot = useEditorSnapshot(editor);\n const [busy, setBusy] = useState<ReadonlySet<string>>(new Set());\n const busyRef = useRef(new Set<string>());\n const [error, setError] = useState<string | null>(null);\n const buttons: SpagbolCanvasAction[] = [\n ...(snapshot.path.length\n ? [\n {\n id: \"spagbol:exit\",\n label: \"EXIT\",\n ariaLabel: \"Exit nested graph\",\n onSelect: () => editor.exit(),\n },\n ]\n : []),\n ...(options.buttons ?? []),\n ...(options.showHistory === false\n ? []\n : [\n {\n id: \"spagbol:undo\",\n label: \"UNDO\",\n ariaLabel: \"Undo\",\n title: \"Undo (Cmd/Ctrl+Z)\",\n disabled: !snapshot.canUndo || !editor.editing,\n onSelect: () => editor.undo(),\n },\n {\n id: \"spagbol:redo\",\n label: \"REDO\",\n ariaLabel: \"Redo\",\n title: \"Redo (Cmd/Ctrl+Shift+Z)\",\n disabled: !snapshot.canRedo || !editor.editing,\n onSelect: () => editor.redo(),\n },\n ]),\n ];\n const members = editor.graph.nodes.filter(node => snapshot.selection.nodeIds.includes(node.id) && !isGroupBoundary(node));\n const exposed = editor.graph.data?.exposedNodeIds;\n const exposureChecked = members.length > 0 && Array.isArray(exposed) && members.every(node => exposed.includes(node.id));\n const items: SpagbolCanvasMenuItem[] = [\n { id: 'spagbol:group', label: 'GROUP TO SUBPATCH', ariaLabel: 'Group to subpatch', disabled: !editor.editing || !members.length, onSelect: () => { editor.groupSelection(); } },\n\n { id: 'spagbol:scale-up', label: 'SCALE UP SELECTION', disabled: !editor.editing || !snapshot.selection.nodeIds.length, onSelect: () => scale(2) },\n { id: 'spagbol:scale-down', label: 'SCALE DOWN SELECTION', disabled: !editor.editing || !snapshot.selection.nodeIds.length, onSelect: () => scale(0.5) },\n ...(options.menuItems ?? []),\n ...(options.showLayoutLock === false\n ? []\n : [\n {\n id: \"spagbol:lock\",\n label: \"LOCK UI\",\n ariaLabel: snapshot.mode === \"edit\" ? \"Lock UI\" : \"Unlock UI\",\n checked: snapshot.mode !== \"edit\",\n disabled: snapshot.mode === \"readonly\",\n onSelect: () =>\n editor.setMode(editor.editing ? \"controls\" : \"edit\"),\n },\n ]),\n ];\n const hasMenu = items.some((item) => !(\"separator\" in item));\n async function invoke(action: SpagbolCanvasAction, fromMenu: boolean) {\n if (action.disabled || busyRef.current.has(action.id)) return;\n busyRef.current.add(action.id);\n setBusy(new Set(busyRef.current));\n setError(null);\n try {\n await action.onSelect();\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n options.onActionError?.(cause, action);\n } finally {\n busyRef.current.delete(action.id);\n setBusy(new Set(busyRef.current));\n }\n }\n function actionButton(action: SpagbolCanvasAction, fromMenu = false) {\n return (\n <button\n key={action.id}\n type=\"button\"\n role={\n fromMenu\n ? action.checked === undefined\n ? \"menuitem\"\n : \"menuitemcheckbox\"\n : undefined\n }\n aria-label={labelFor(action)}\n title={action.title ?? labelFor(action)}\n disabled={action.disabled || busy.has(action.id)}\n aria-busy={busy.has(action.id) || undefined}\n aria-checked={fromMenu ? action.checked : undefined}\n aria-pressed={fromMenu ? undefined : action.checked}\n onClick={() => {\n void invoke(action, fromMenu);\n }}\n >\n {action.label}\n </button>\n );\n }\n return (\n <div\n className=\"spagbol-canvas-actions nodrag nopan\"\n role=\"group\"\n aria-label=\"Canvas actions\"\n onPointerDown={(event) => event.stopPropagation()}\n onDoubleClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n >\n {options.leading && (\n <div className=\"spagbol-canvas-actions-leading\">{options.leading}</div>\n )}\n {buttons.map((action) => actionButton(action))}\n {hasMenu && (\n <SpagbolCanvasMenu\n label={\n <>\n {options.menuLabel ?? \"MORE\"} <span aria-hidden=\"true\">ˆ</span>\n </>\n }\n >\n {items.map((item) =>\n \"separator\" in item ? (\n <div\n key={item.id}\n role=\"separator\"\n className=\"spagbol-canvas-menu-separator\"\n />\n ) : (\n actionButton(item, true)\n ),\n )}\n </SpagbolCanvasMenu>\n )}\n {error && (\n <div className=\"spagbol-canvas-actions-error\" role=\"alert\">\n {error}\n <button\n type=\"button\"\n aria-label=\"Dismiss action error\"\n onClick={() => setError(null)}\n >\n ×\n </button>\n </div>\n )}\n </div>\n );\n}\n\nexport function SpagbolCanvasControls(options: SpagbolCanvasControlsOptions) {\n const { editor } = useSpagbol();\n const snapshot = useEditorSnapshot(editor);\n return (\n <>\n {options.showBreadcrumbs !== false && snapshot.path.length > 0 && (\n <nav\n className=\"spagbol-canvas-breadcrumbs nodrag nopan\"\n aria-label=\"Graph navigation\"\n onPointerDown={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n >\n <button type=\"button\" onClick={() => editor.navigate([])}>\n ROOT\n </button>\n {snapshot.path.map((id, index) => (\n <button\n key={`${index}:${id}`}\n type=\"button\"\n aria-current={\n index === snapshot.path.length - 1 ? \"page\" : undefined\n }\n onClick={() => editor.navigate(snapshot.path.slice(0, index + 1))}\n >\n {graphAt(snapshot.document, snapshot.path.slice(0, index)).nodes.find(node => node.id === id)?.label?.trim() || 'Group'}\n </button>\n ))}\n </nav>\n )}\n <div\n className={`spagbol-canvas-controls${options.showZoom === false ? \" spagbol-canvas-controls-no-zoom\" : \"\"}`}\n >\n {options.showZoom !== false && (\n <SpagbolCanvasControlsZoom extra={options.zoomExtra} />\n )}\n <SpagbolCanvasActions {...options} />\n </div>\n </>\n );\n}\n"]}
@@ -0,0 +1,9 @@
1
+ export interface SpagbolControlJoystickProps {
2
+ x: number;
3
+ y: number;
4
+ elasticity?: number;
5
+ className?: string;
6
+ onChange: (x: number, y: number) => void;
7
+ }
8
+ export declare function SpagbolControlJoystick({ x, y, elasticity, onChange, className }: SpagbolControlJoystickProps): import("react").JSX.Element;
9
+ //# sourceMappingURL=SpagbolControlJoystick.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpagbolControlJoystick.d.ts","sourceRoot":"","sources":["../../src/react/SpagbolControlJoystick.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,2BAA2B;IAC1C,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C;AAED,wBAAgB,sBAAsB,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,UAAc,EAAE,QAAQ,EAAE,SAAc,EAAE,EAAE,2BAA2B,+BAqIrH"}
@@ -0,0 +1,111 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ export function SpagbolControlJoystick({ x, y, elasticity = 0, onChange, className = '' }) {
4
+ const changeRef = useRef(onChange);
5
+ changeRef.current = onChange;
6
+ const dragPointerRef = useRef(null);
7
+ const returnFrameRef = useRef(null);
8
+ const returnTimeRef = useRef(0);
9
+ const elasticityRef = useRef(elasticity);
10
+ const normalizedX = clamp(x, 0, 1);
11
+ const normalizedY = clamp(y, 0, 1);
12
+ const positionRef = useRef({ x: normalizedX, y: normalizedY });
13
+ elasticityRef.current = elasticity;
14
+ if (dragPointerRef.current === null && returnFrameRef.current === null) {
15
+ positionRef.current = { x: normalizedX, y: normalizedY };
16
+ }
17
+ useEffect(() => () => {
18
+ if (returnFrameRef.current !== null) {
19
+ window.cancelAnimationFrame(returnFrameRef.current);
20
+ }
21
+ }, []);
22
+ function stopReturn() {
23
+ if (returnFrameRef.current !== null) {
24
+ window.cancelAnimationFrame(returnFrameRef.current);
25
+ returnFrameRef.current = null;
26
+ }
27
+ }
28
+ function startReturn() {
29
+ stopReturn();
30
+ if (!(elasticityRef.current > 0))
31
+ return;
32
+ returnTimeRef.current = performance.now();
33
+ function stepReturn(timestamp) {
34
+ if (dragPointerRef.current !== null || !(elasticityRef.current > 0)) {
35
+ returnFrameRef.current = null;
36
+ return;
37
+ }
38
+ const elapsedSeconds = Math.min(0.05, Math.max(0, (timestamp - returnTimeRef.current) / 1000));
39
+ returnTimeRef.current = timestamp;
40
+ const current = positionRef.current;
41
+ const deltaX = 0.5 - current.x;
42
+ const deltaY = 0.5 - current.y;
43
+ const distance = Math.hypot(deltaX, deltaY);
44
+ const travel = elasticityRef.current * elapsedSeconds;
45
+ if (distance <= Math.max(0.0001, travel)) {
46
+ positionRef.current = { x: 0.5, y: 0.5 };
47
+ returnFrameRef.current = null;
48
+ changeRef.current(0.5, 0.5);
49
+ return;
50
+ }
51
+ const scale = travel / distance;
52
+ const next = {
53
+ x: current.x + deltaX * scale,
54
+ y: current.y + deltaY * scale,
55
+ };
56
+ positionRef.current = next;
57
+ changeRef.current(next.x, next.y);
58
+ returnFrameRef.current = window.requestAnimationFrame(stepReturn);
59
+ }
60
+ returnFrameRef.current = window.requestAnimationFrame(stepReturn);
61
+ }
62
+ function updateFromPointer(event) {
63
+ const bounds = event.currentTarget.getBoundingClientRect();
64
+ const nextX = (event.clientX - bounds.left) / Math.max(1, bounds.width);
65
+ const nextY = 1 - ((event.clientY - bounds.top) / Math.max(1, bounds.height));
66
+ const next = { x: clamp(nextX, 0, 1), y: clamp(nextY, 0, 1) };
67
+ positionRef.current = next;
68
+ onChange(next.x, next.y);
69
+ }
70
+ return (_jsx("div", { className: `spagbol-control-joystick ${className} nodrag nopan`, role: "group", "aria-label": "Joystick", "aria-valuetext": `X ${normalizedX.toFixed(3)}, Y ${normalizedY.toFixed(3)}`, onPointerDown: (event) => {
71
+ event.preventDefault();
72
+ event.stopPropagation();
73
+ if (!event.isPrimary || event.button !== 0)
74
+ return;
75
+ stopReturn();
76
+ dragPointerRef.current = event.pointerId;
77
+ event.currentTarget.setPointerCapture(event.pointerId);
78
+ updateFromPointer(event);
79
+ }, onPointerMove: (event) => {
80
+ if (dragPointerRef.current !== event.pointerId)
81
+ return;
82
+ event.preventDefault();
83
+ event.stopPropagation();
84
+ updateFromPointer(event);
85
+ }, onPointerUp: (event) => {
86
+ event.stopPropagation();
87
+ if (dragPointerRef.current === event.pointerId) {
88
+ dragPointerRef.current = null;
89
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
90
+ event.currentTarget.releasePointerCapture(event.pointerId);
91
+ }
92
+ startReturn();
93
+ }
94
+ }, onPointerCancel: (event) => {
95
+ event.stopPropagation();
96
+ if (dragPointerRef.current === event.pointerId) {
97
+ dragPointerRef.current = null;
98
+ startReturn();
99
+ }
100
+ }, onLostPointerCapture: (event) => {
101
+ if (dragPointerRef.current === event.pointerId) {
102
+ dragPointerRef.current = null;
103
+ startReturn();
104
+ }
105
+ }, onClick: (event) => event.stopPropagation(), onDoubleClick: (event) => event.stopPropagation(), children: _jsx("span", { className: "spagbol-control-joystick-handle", style: {
106
+ left: `calc(${normalizedX} * (100% - var(--joystick-handle-size)))`,
107
+ top: `calc(${1 - normalizedY} * (100% - var(--joystick-handle-size)))`,
108
+ }, "aria-hidden": "true" }) }));
109
+ }
110
+ function clamp(value, min, max) { return Math.max(min, Math.min(max, Number.isFinite(value) ? value : 0.5)); }
111
+ //# sourceMappingURL=SpagbolControlJoystick.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpagbolControlJoystick.js","sourceRoot":"","sources":["../../src/react/SpagbolControlJoystick.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, type PointerEvent } from 'react';\n\nexport interface SpagbolControlJoystickProps {\n x: number;\n y: number;\n elasticity?: number;\n className?: string;\n onChange: (x: number, y: number) => void;\n}\n\nexport function SpagbolControlJoystick({ x, y, elasticity = 0, onChange, className = '' }: SpagbolControlJoystickProps) {\n const changeRef = useRef(onChange); changeRef.current = onChange;\n const dragPointerRef = useRef<number | null>(null);\n const returnFrameRef = useRef<number | null>(null);\n const returnTimeRef = useRef(0);\n const elasticityRef = useRef(elasticity);\n const normalizedX = clamp(x, 0, 1);\n const normalizedY = clamp(y, 0, 1);\n const positionRef = useRef({ x: normalizedX, y: normalizedY });\n\n elasticityRef.current = elasticity;\n if (dragPointerRef.current === null && returnFrameRef.current === null) {\n positionRef.current = { x: normalizedX, y: normalizedY };\n }\n\n useEffect(() => () => {\n if (returnFrameRef.current !== null) {\n window.cancelAnimationFrame(returnFrameRef.current);\n }\n }, []);\n\n function stopReturn() {\n if (returnFrameRef.current !== null) {\n window.cancelAnimationFrame(returnFrameRef.current);\n returnFrameRef.current = null;\n }\n }\n\n function startReturn() {\n stopReturn();\n if (!(elasticityRef.current > 0)) return;\n returnTimeRef.current = performance.now();\n\n function stepReturn(timestamp: number) {\n if (dragPointerRef.current !== null || !(elasticityRef.current > 0)) {\n returnFrameRef.current = null;\n return;\n }\n\n const elapsedSeconds = Math.min(0.05, Math.max(0, (timestamp - returnTimeRef.current) / 1000));\n returnTimeRef.current = timestamp;\n const current = positionRef.current;\n const deltaX = 0.5 - current.x;\n const deltaY = 0.5 - current.y;\n const distance = Math.hypot(deltaX, deltaY);\n const travel = elasticityRef.current * elapsedSeconds;\n\n if (distance <= Math.max(0.0001, travel)) {\n positionRef.current = { x: 0.5, y: 0.5 };\n returnFrameRef.current = null;\n changeRef.current(0.5, 0.5);\n return;\n }\n\n const scale = travel / distance;\n const next = {\n x: current.x + deltaX * scale,\n y: current.y + deltaY * scale,\n };\n positionRef.current = next;\n changeRef.current(next.x, next.y);\n returnFrameRef.current = window.requestAnimationFrame(stepReturn);\n }\n\n returnFrameRef.current = window.requestAnimationFrame(stepReturn);\n }\n\n function updateFromPointer(event: PointerEvent<HTMLDivElement>) {\n const bounds = event.currentTarget.getBoundingClientRect();\n const nextX = (event.clientX - bounds.left) / Math.max(1, bounds.width);\n const nextY = 1 - ((event.clientY - bounds.top) / Math.max(1, bounds.height));\n const next = { x: clamp(nextX, 0, 1), y: clamp(nextY, 0, 1) };\n positionRef.current = next;\n onChange(next.x, next.y);\n }\n\n return (\n <div\n className={`spagbol-control-joystick ${className} nodrag nopan`}\n role=\"group\"\n aria-label=\"Joystick\"\n aria-valuetext={`X ${normalizedX.toFixed(3)}, Y ${normalizedY.toFixed(3)}`}\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n if (!event.isPrimary || event.button !== 0) return;\n stopReturn();\n dragPointerRef.current = event.pointerId;\n event.currentTarget.setPointerCapture(event.pointerId);\n updateFromPointer(event);\n }}\n onPointerMove={(event) => {\n if (dragPointerRef.current !== event.pointerId) return;\n event.preventDefault();\n event.stopPropagation();\n updateFromPointer(event);\n }}\n onPointerUp={(event) => {\n event.stopPropagation();\n if (dragPointerRef.current === event.pointerId) {\n dragPointerRef.current = null;\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n startReturn();\n }\n }}\n onPointerCancel={(event) => {\n event.stopPropagation();\n if (dragPointerRef.current === event.pointerId) {\n dragPointerRef.current = null;\n startReturn();\n }\n }}\n onLostPointerCapture={(event) => {\n if (dragPointerRef.current === event.pointerId) {\n dragPointerRef.current = null;\n startReturn();\n }\n }}\n onClick={(event) => event.stopPropagation()}\n onDoubleClick={(event) => event.stopPropagation()}\n >\n <span\n className=\"spagbol-control-joystick-handle\"\n style={{\n left: `calc(${normalizedX} * (100% - var(--joystick-handle-size)))`,\n top: `calc(${1 - normalizedY} * (100% - var(--joystick-handle-size)))`,\n }}\n aria-hidden=\"true\"\n />\n </div>\n );\n}\n\n\nfunction clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, Number.isFinite(value) ? value : 0.5)); }\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"SpagbolControlZoom.d.ts","sourceRoot":"","sources":["../../src/react/SpagbolControlZoom.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,IAAI,IAAI,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC5B;AACD,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,SAAS,EACT,OAAO,EACP,KAAK,EACL,MAAe,GAChB,EAAE,uBAAuB,+BAoFzB"}
1
+ {"version":3,"file":"SpagbolControlZoom.d.ts","sourceRoot":"","sources":["../../src/react/SpagbolControlZoom.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,IAAI,IAAI,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC5B;AACD,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,SAAS,EACT,OAAO,EACP,KAAK,EACL,MAAe,GAChB,EAAE,uBAAuB,+BAoFzB"}
@@ -1,14 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SPAGBOL_ZOOM_BASELINE, spagbolZoomPercentage } from "./zoom.js";
2
3
  import { ControlButton, Controls, useReactFlow } from "@xyflow/react";
3
4
  /** Zoom UI reusable with either Spagbol's controller or a host-owned flow. */
4
5
  export function SpagbolControlZoom({ zoom, className, onReset, extra, layout = "flow", }) {
5
6
  const flow = useReactFlow();
6
- const percentage = `${Math.round(zoom * 100)}%`;
7
+ const percentage = spagbolZoomPercentage(zoom);
7
8
  const reset = () => {
8
9
  if (onReset)
9
10
  onReset();
10
11
  else
11
- void flow.zoomTo(1);
12
+ void flow.zoomTo(SPAGBOL_ZOOM_BASELINE);
12
13
  };
13
14
  const label = `Reset zoom to 100% (currently ${percentage})`;
14
15
  if (layout === "flow")
@@ -1 +1 @@
1
- {"version":3,"file":"SpagbolControlZoom.js","sourceRoot":"","sources":["../../src/react/SpagbolControlZoom.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAUtE,8EAA8E;AAC9E,MAAM,UAAU,kBAAkB,CAAC,EACjC,IAAI,EACJ,SAAS,EACT,OAAO,EACP,KAAK,EACL,MAAM,GAAG,MAAM,GACS;IACxB,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;IAC5B,MAAM,UAAU,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC;IAChD,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,IAAI,OAAO;YAAE,OAAO,EAAE,CAAC;;YAClB,KAAK,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,iCAAiC,UAAU,GAAG,CAAC;IAC7D,IAAI,MAAM,KAAK,MAAM;QACnB,OAAO,CACL,MAAC,QAAQ,IACP,eAAe,EAAE,KAAK,EACtB,SAAS,EAAE,wBAAwB,SAAS,IAAI,EAAE,EAAE,aAEpD,KAAC,aAAa,IACZ,SAAS,EAAC,sCAAsC,EAChD,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,KAAK,gBACF,KAAK,EACjB,KAAK,EAAE,KAAK,YAEX,UAAU,GACG,EACf,KAAK,IACG,CACZ,CAAC;IACJ,OAAO,CACL,eACE,SAAS,EAAE,iEAAiE,SAAS,IAAI,EAAE,EAAE,EAC7F,IAAI,EAAC,OAAO,gBACD,sBAAsB,EACjC,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,EACjD,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,aAE7C,eAAK,SAAS,EAAC,4BAA4B,aACzC,iBACE,IAAI,EAAC,QAAQ,gBACF,SAAS,EACpB,KAAK,EAAC,SAAS,EACf,QAAQ,EAAE,IAAI,IAAI,CAAC,GAAG,MAAM,EAC5B,OAAO,EAAE,GAAG,EAAE;4BACZ,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;wBACrB,CAAC,kBAGM,EACT,iBACE,IAAI,EAAC,QAAQ,gBACF,UAAU,EACrB,KAAK,EAAC,UAAU,EAChB,QAAQ,EAAE,IAAI,IAAI,IAAI,GAAG,MAAM,EAC/B,OAAO,EAAE,GAAG,EAAE;4BACZ,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;wBACtB,CAAC,uBAGM,EACT,iBACE,IAAI,EAAC,QAAQ,gBACF,mBAAmB,EAC9B,KAAK,EAAC,mBAAmB,EACzB,OAAO,EAAE,GAAG,EAAE;4BACZ,KAAK,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;wBACvC,CAAC,YAED,cAAK,OAAO,EAAC,WAAW,iBAAa,MAAM,YACzC,eAAM,CAAC,EAAC,wCAAwC,GAAG,GAC/C,GACC,IACL,EACN,eAAK,SAAS,EAAC,6BAA6B,aACzC,KAAK,EACN,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,iCAAiC,gBAC/B,KAAK,EACjB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,KAAK,YAEb,UAAU,GACJ,IACL,IACF,CACP,CAAC;AACJ,CAAC","sourcesContent":["import { ControlButton, Controls, useReactFlow } from \"@xyflow/react\";\nimport type { ReactNode } from \"react\";\n\nexport interface SpagbolControlZoomProps {\n zoom: number;\n className?: string;\n onReset?(): void;\n extra?: ReactNode;\n layout?: \"flow\" | \"corner\";\n}\n/** Zoom UI reusable with either Spagbol's controller or a host-owned flow. */\nexport function SpagbolControlZoom({\n zoom,\n className,\n onReset,\n extra,\n layout = \"flow\",\n}: SpagbolControlZoomProps) {\n const flow = useReactFlow();\n const percentage = `${Math.round(zoom * 100)}%`;\n const reset = () => {\n if (onReset) onReset();\n else void flow.zoomTo(1);\n };\n const label = `Reset zoom to 100% (currently ${percentage})`;\n if (layout === \"flow\")\n return (\n <Controls\n showInteractive={false}\n className={`spagbol-control-zoom ${className ?? \"\"}`}\n >\n <ControlButton\n className=\"react-flow__controls-zoom-percentage\"\n type=\"button\"\n onClick={reset}\n aria-label={label}\n title={label}\n >\n {percentage}\n </ControlButton>\n {extra}\n </Controls>\n );\n return (\n <div\n className={`spagbol-control-zoom spagbol-control-zoom-corner nodrag nopan ${className ?? \"\"}`}\n role=\"group\"\n aria-label=\"Canvas zoom controls\"\n onPointerDown={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n >\n <div className=\"spagbol-control-zoom-stack\">\n <button\n type=\"button\"\n aria-label=\"Zoom in\"\n title=\"Zoom in\"\n disabled={zoom >= 3 - 0.0001}\n onClick={() => {\n void flow.zoomIn();\n }}\n >\n +\n </button>\n <button\n type=\"button\"\n aria-label=\"Zoom out\"\n title=\"Zoom out\"\n disabled={zoom <= 0.05 + 0.0001}\n onClick={() => {\n void flow.zoomOut();\n }}\n >\n −\n </button>\n <button\n type=\"button\"\n aria-label=\"Fit graph to view\"\n title=\"Fit graph to view\"\n onClick={() => {\n void flow.fitView({ padding: 0.18 });\n }}\n >\n <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\">\n <path d=\"M2 6V2h4M10 2h4v4M14 10v4h-4M6 14H2v-4\" />\n </svg>\n </button>\n </div>\n <div className=\"spagbol-control-zoom-extras\">\n {extra}\n <button\n type=\"button\"\n className=\"spagbol-control-zoom-percentage\"\n aria-label={label}\n title={label}\n onClick={reset}\n >\n {percentage}\n </button>\n </div>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"SpagbolControlZoom.js","sourceRoot":"","sources":["../../src/react/SpagbolControlZoom.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAUtE,8EAA8E;AAC9E,MAAM,UAAU,kBAAkB,CAAC,EACjC,IAAI,EACJ,SAAS,EACT,OAAO,EACP,KAAK,EACL,MAAM,GAAG,MAAM,GACS;IACxB,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;IAC5B,MAAM,UAAU,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,IAAI,OAAO;YAAE,OAAO,EAAE,CAAC;;YAClB,KAAK,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC;IAC/C,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,iCAAiC,UAAU,GAAG,CAAC;IAC7D,IAAI,MAAM,KAAK,MAAM;QACnB,OAAO,CACL,MAAC,QAAQ,IACP,eAAe,EAAE,KAAK,EACtB,SAAS,EAAE,wBAAwB,SAAS,IAAI,EAAE,EAAE,aAEpD,KAAC,aAAa,IACZ,SAAS,EAAC,sCAAsC,EAChD,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,KAAK,gBACF,KAAK,EACjB,KAAK,EAAE,KAAK,YAEX,UAAU,GACG,EACf,KAAK,IACG,CACZ,CAAC;IACJ,OAAO,CACL,eACE,SAAS,EAAE,iEAAiE,SAAS,IAAI,EAAE,EAAE,EAC7F,IAAI,EAAC,OAAO,gBACD,sBAAsB,EACjC,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,EACjD,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,EAAE,aAE7C,eAAK,SAAS,EAAC,4BAA4B,aACzC,iBACE,IAAI,EAAC,QAAQ,gBACF,SAAS,EACpB,KAAK,EAAC,SAAS,EACf,QAAQ,EAAE,IAAI,IAAI,CAAC,GAAG,MAAM,EAC5B,OAAO,EAAE,GAAG,EAAE;4BACZ,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;wBACrB,CAAC,kBAGM,EACT,iBACE,IAAI,EAAC,QAAQ,gBACF,UAAU,EACrB,KAAK,EAAC,UAAU,EAChB,QAAQ,EAAE,IAAI,IAAI,IAAI,GAAG,MAAM,EAC/B,OAAO,EAAE,GAAG,EAAE;4BACZ,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;wBACtB,CAAC,uBAGM,EACT,iBACE,IAAI,EAAC,QAAQ,gBACF,mBAAmB,EAC9B,KAAK,EAAC,mBAAmB,EACzB,OAAO,EAAE,GAAG,EAAE;4BACZ,KAAK,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;wBACvC,CAAC,YAED,cAAK,OAAO,EAAC,WAAW,iBAAa,MAAM,YACzC,eAAM,CAAC,EAAC,wCAAwC,GAAG,GAC/C,GACC,IACL,EACN,eAAK,SAAS,EAAC,6BAA6B,aACzC,KAAK,EACN,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,iCAAiC,gBAC/B,KAAK,EACjB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,KAAK,YAEb,UAAU,GACJ,IACL,IACF,CACP,CAAC;AACJ,CAAC","sourcesContent":["import { SPAGBOL_ZOOM_BASELINE, spagbolZoomPercentage } from \"./zoom.js\";\nimport { ControlButton, Controls, useReactFlow } from \"@xyflow/react\";\nimport type { ReactNode } from \"react\";\n\nexport interface SpagbolControlZoomProps {\n zoom: number;\n className?: string;\n onReset?(): void;\n extra?: ReactNode;\n layout?: \"flow\" | \"corner\";\n}\n/** Zoom UI reusable with either Spagbol's controller or a host-owned flow. */\nexport function SpagbolControlZoom({\n zoom,\n className,\n onReset,\n extra,\n layout = \"flow\",\n}: SpagbolControlZoomProps) {\n const flow = useReactFlow();\n const percentage = spagbolZoomPercentage(zoom);\n const reset = () => {\n if (onReset) onReset();\n else void flow.zoomTo(SPAGBOL_ZOOM_BASELINE);\n };\n const label = `Reset zoom to 100% (currently ${percentage})`;\n if (layout === \"flow\")\n return (\n <Controls\n showInteractive={false}\n className={`spagbol-control-zoom ${className ?? \"\"}`}\n >\n <ControlButton\n className=\"react-flow__controls-zoom-percentage\"\n type=\"button\"\n onClick={reset}\n aria-label={label}\n title={label}\n >\n {percentage}\n </ControlButton>\n {extra}\n </Controls>\n );\n return (\n <div\n className={`spagbol-control-zoom spagbol-control-zoom-corner nodrag nopan ${className ?? \"\"}`}\n role=\"group\"\n aria-label=\"Canvas zoom controls\"\n onPointerDown={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n >\n <div className=\"spagbol-control-zoom-stack\">\n <button\n type=\"button\"\n aria-label=\"Zoom in\"\n title=\"Zoom in\"\n disabled={zoom >= 3 - 0.0001}\n onClick={() => {\n void flow.zoomIn();\n }}\n >\n +\n </button>\n <button\n type=\"button\"\n aria-label=\"Zoom out\"\n title=\"Zoom out\"\n disabled={zoom <= 0.05 + 0.0001}\n onClick={() => {\n void flow.zoomOut();\n }}\n >\n −\n </button>\n <button\n type=\"button\"\n aria-label=\"Fit graph to view\"\n title=\"Fit graph to view\"\n onClick={() => {\n void flow.fitView({ padding: 0.18 });\n }}\n >\n <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\">\n <path d=\"M2 6V2h4M10 2h4v4M14 10v4h-4M6 14H2v-4\" />\n </svg>\n </button>\n </div>\n <div className=\"spagbol-control-zoom-extras\">\n {extra}\n <button\n type=\"button\"\n className=\"spagbol-control-zoom-percentage\"\n aria-label={label}\n title={label}\n onClick={reset}\n >\n {percentage}\n </button>\n </div>\n </div>\n );\n}\n"]}
@@ -0,0 +1,45 @@
1
+ import { type ReactNode } from 'react';
2
+ /** Normalized view coordinates, not a serialized curve or runtime data model. */
3
+ export interface SpagbolCurveEditorPoint {
4
+ x: number;
5
+ y: number;
6
+ locked?: boolean;
7
+ }
8
+ export interface SpagbolCurveEditorProps {
9
+ /** x: 0..1, y: -1..1. The host projects its data into these view coordinates. */
10
+ points: readonly SpagbolCurveEditorPoint[];
11
+ backgroundCurves?: readonly {
12
+ points: readonly SpagbolCurveEditorPoint[];
13
+ className?: string;
14
+ }[];
15
+ className?: string;
16
+ pathClassName?: string;
17
+ ariaLabel?: string;
18
+ /** Actual on-screen size, including canvas zoom, for constant-sized hit targets. */
19
+ displaySize: {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ screenEmphasis?: number;
24
+ pointScale?: number;
25
+ minPointSpacing?: number;
26
+ formatValue?: (normalizedValue: number) => string;
27
+ toolbar?: ReactNode;
28
+ underlay?: ReactNode;
29
+ /** SVG children in the fixed 300 × 128 viewBox; decorative, without pointer events. */
30
+ overlay?: ReactNode;
31
+ footer?: ReactNode;
32
+ /** Apply an insertion in host data and return the new point's index, or null to reject. */
33
+ onInsertPoint: (point: {
34
+ x: number;
35
+ y: number;
36
+ }) => number | null;
37
+ onMovePoint: (index: number, point: {
38
+ x: number;
39
+ y: number;
40
+ }) => void;
41
+ onRemovePoint: (index: number) => void;
42
+ onGestureEnd?: () => void;
43
+ }
44
+ export declare function SpagbolCurveEditor({ points, backgroundCurves, className, pathClassName, ariaLabel, displaySize, screenEmphasis, pointScale, minPointSpacing, formatValue, toolbar, underlay, overlay, footer, onInsertPoint, onMovePoint, onRemovePoint, onGestureEnd, }: SpagbolCurveEditorProps): import("react").JSX.Element;
45
+ //# sourceMappingURL=SpagbolCurveEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpagbolCurveEditor.d.ts","sourceRoot":"","sources":["../../src/react/SpagbolCurveEditor.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAsC,MAAM,OAAO,CAAC;AAExG,iFAAiF;AACjF,MAAM,WAAW,uBAAuB;IAAG,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE;AACnF,MAAM,WAAW,uBAAuB;IACtC,iFAAiF;IACjF,MAAM,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAC3C,gBAAgB,CAAC,EAAE,SAAS;QAAE,MAAM,EAAE,SAAS,uBAAuB,EAAE,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,uFAAuF;IACvF,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,2FAA2F;IAC3F,aAAa,EAAE,CAAC,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,GAAG,IAAI,CAAC;IAClE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACtE,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAcD,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EAAE,gBAAqB,EAAE,SAAc,EAAE,aAAkB,EAAE,SAA0B,EAC7F,WAAW,EAAE,cAAkB,EAAE,UAAc,EAAE,eAAuB,EACxE,WAAgE,EAChE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,GAC5F,EAAE,uBAAuB,+BAuHzB"}
@@ -0,0 +1,142 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ const width = 300, height = 128, padding = 12;
4
+ const innerWidth = width - padding * 2, innerHeight = height - padding * 2;
5
+ const clamp = (n, low, high) => Math.max(low, Math.min(high, n));
6
+ const screenPoint = (point) => ({ x: padding + point.x * innerWidth, y: padding + (1 - (point.y + 1) / 2) * innerHeight });
7
+ function path(points) {
8
+ return points.map((point, index) => {
9
+ const p = screenPoint(point);
10
+ // Preserve Teia's two-decimal SVG path precision.
11
+ return `${index ? 'L' : 'M'}${Math.round(p.x * 100) / 100} ${Math.round(p.y * 100) / 100}`;
12
+ }).join(' ');
13
+ }
14
+ export function SpagbolCurveEditor({ points, backgroundCurves = [], className = '', pathClassName = '', ariaLabel = 'Curve editor', displaySize, screenEmphasis = 1, pointScale = 1, minPointSpacing = 0.001, formatValue = (value) => String(Math.round(value * 1000) / 1000), toolbar, underlay, overlay, footer, onInsertPoint, onMovePoint, onRemovePoint, onGestureEnd, }) {
15
+ const editorRef = useRef(null);
16
+ const dragRef = useRef(null);
17
+ const removedByPress = useRef(false);
18
+ const clickRef = useRef({ index: null, time: 0 });
19
+ const [activePoint, setActivePoint] = useState(null);
20
+ const [hoveredPoint, setHoveredPoint] = useState(null);
21
+ const latest = useRef({ points, onMovePoint, onGestureEnd });
22
+ latest.current = { points, onMovePoint, onGestureEnd };
23
+ function fromPointer(event) {
24
+ const rect = editorRef.current?.getBoundingClientRect();
25
+ if (!rect || rect.width <= 0 || rect.height <= 0)
26
+ return null;
27
+ const x = clamp((event.clientX - rect.left) / rect.width * width, padding, width - padding);
28
+ const y = clamp((event.clientY - rect.top) / rect.height * height, padding, height - padding);
29
+ return { x: (x - padding) / innerWidth, y: (1 - (y - padding) / innerHeight) * 2 - 1 };
30
+ }
31
+ useEffect(() => {
32
+ const move = (event) => {
33
+ const drag = dragRef.current;
34
+ if (!drag || drag.pointerId !== event.pointerId)
35
+ return;
36
+ const current = latest.current.points;
37
+ const selected = current[drag.index];
38
+ if (!selected || selected.locked)
39
+ return;
40
+ event.preventDefault();
41
+ const point = fromPointer(event);
42
+ if (!point)
43
+ return;
44
+ const low = drag.index === 0 ? 0 : (current[drag.index - 1]?.x ?? 0) + minPointSpacing;
45
+ const high = drag.index === current.length - 1 ? 1 : (current[drag.index + 1]?.x ?? 1) - minPointSpacing;
46
+ // A host may reject a move; never cross adjacent point positions.
47
+ if (low > high)
48
+ return;
49
+ latest.current.onMovePoint(drag.index, { x: clamp(point.x, low, high), y: point.y });
50
+ };
51
+ const stop = (event) => {
52
+ if (dragRef.current?.pointerId !== event.pointerId)
53
+ return;
54
+ dragRef.current = null;
55
+ setActivePoint(null);
56
+ latest.current.onGestureEnd?.();
57
+ };
58
+ window.addEventListener('pointermove', move, { passive: false });
59
+ window.addEventListener('pointerup', stop);
60
+ window.addEventListener('pointercancel', stop);
61
+ return () => {
62
+ window.removeEventListener('pointermove', move);
63
+ window.removeEventListener('pointerup', stop);
64
+ window.removeEventListener('pointercancel', stop);
65
+ };
66
+ }, [minPointSpacing]);
67
+ const hitIndex = (target) => {
68
+ const element = target instanceof Element ? target.closest('[data-spagbol-curve-point]') : null;
69
+ return element ? Number(element.dataset.spagbolCurvePoint) : null;
70
+ };
71
+ const remove = (index) => {
72
+ if (!points[index] || points[index].locked)
73
+ return;
74
+ if (dragRef.current)
75
+ onGestureEnd?.();
76
+ dragRef.current = null;
77
+ setActivePoint(null);
78
+ onRemovePoint(index);
79
+ };
80
+ const pointerDown = (event) => {
81
+ if (event.button !== 0 || dragRef.current)
82
+ return;
83
+ event.preventDefault();
84
+ event.stopPropagation();
85
+ removedByPress.current = false;
86
+ const index = hitIndex(event.target);
87
+ if (index !== null && Number.isInteger(index)) {
88
+ const now = performance.now();
89
+ if (clickRef.current.index === index && now - clickRef.current.time < 360 && !points[index]?.locked) {
90
+ clickRef.current = { index: null, time: 0 };
91
+ removedByPress.current = true;
92
+ remove(index);
93
+ return;
94
+ }
95
+ clickRef.current = { index, time: now };
96
+ if (!points[index] || points[index].locked)
97
+ return;
98
+ dragRef.current = { pointerId: event.pointerId, index };
99
+ setActivePoint(index);
100
+ event.currentTarget.setPointerCapture(event.pointerId);
101
+ return;
102
+ }
103
+ const point = fromPointer(event);
104
+ if (!point)
105
+ return;
106
+ clickRef.current = { index: null, time: 0 };
107
+ const insertedIndex = onInsertPoint(point);
108
+ if (insertedIndex === null)
109
+ return;
110
+ dragRef.current = { pointerId: event.pointerId, index: insertedIndex };
111
+ setActivePoint(insertedIndex);
112
+ event.currentTarget.setPointerCapture(event.pointerId);
113
+ };
114
+ const doubleClick = (event) => {
115
+ event.preventDefault();
116
+ event.stopPropagation();
117
+ if (removedByPress.current) {
118
+ removedByPress.current = false;
119
+ return;
120
+ }
121
+ const index = hitIndex(event.target);
122
+ if (index !== null)
123
+ remove(index);
124
+ };
125
+ const radius = (pixels) => ({ rx: pixels / Math.max(0.001, displaySize.width / width), ry: pixels / Math.max(0.001, displaySize.height / height) });
126
+ const hitRadius = radius(15 * screenEmphasis);
127
+ const endpointRadius = radius(5 * screenEmphasis * pointScale);
128
+ const handleRadius = radius(6 * screenEmphasis * pointScale);
129
+ const valuePoint = points[activePoint ?? hoveredPoint ?? -1];
130
+ const valueScreen = valuePoint && screenPoint(valuePoint);
131
+ return _jsxs("div", { className: `spagbol-curve-editor ${className} nodrag nopan`, children: [_jsxs("div", { className: "spagbol-curve-editor-chart", children: [toolbar, underlay, _jsxs("svg", { ref: editorRef, className: "spagbol-curve-editor-canvas", viewBox: "0 0 300 128", preserveAspectRatio: "none", "aria-label": ariaLabel, role: "img", onPointerDown: pointerDown, onDoubleClick: doubleClick, onLostPointerCapture: (event) => {
132
+ if (dragRef.current?.pointerId === event.pointerId) {
133
+ dragRef.current = null;
134
+ setActivePoint(null);
135
+ onGestureEnd?.();
136
+ }
137
+ }, children: [_jsxs("g", { className: "spagbol-curve-editor-chrome", children: [backgroundCurves.map((curve, index) => _jsx("path", { className: `spagbol-curve-editor-path is-background ${curve.className ?? ''}`, d: path(curve.points) }, index)), _jsx("path", { className: `spagbol-curve-editor-path ${pathClassName}`, d: path(points) }), overlay] }), points.map((point, index) => {
138
+ const screen = screenPoint(point);
139
+ return _jsxs("g", { onPointerEnter: () => setHoveredPoint(index), onPointerLeave: () => setHoveredPoint((current) => current === index ? null : current), children: [_jsx("ellipse", { className: `spagbol-curve-editor-hit-target ${point.locked ? 'is-locked' : ''}`, "data-spagbol-curve-point": index, cx: screen.x, cy: screen.y, ...hitRadius }), _jsx("ellipse", { className: `spagbol-curve-editor-handle ${point.locked ? 'spagbol-curve-editor-endpoint is-locked' : ''}`, "data-spagbol-curve-point": index, cx: screen.x, cy: screen.y, ...(point.locked ? endpointRadius : handleRadius) })] }, index);
140
+ })] }), valuePoint && valueScreen ? _jsx("span", { className: `spagbol-curve-editor-value-label ${valuePoint.x > 0.72 ? 'is-left' : ''} ${valuePoint.y > 0.72 ? 'is-below' : ''}`, style: { left: `${valueScreen.x / width * 100}%`, top: `${valueScreen.y / height * 100}%` }, "aria-hidden": "true", children: formatValue(valuePoint.y) }) : null] }), footer] });
141
+ }
142
+ //# sourceMappingURL=SpagbolCurveEditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SpagbolCurveEditor.js","sourceRoot":"","sources":["../../src/react/SpagbolCurveEditor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState, type ReactNode, type PointerEvent, type MouseEvent } from 'react';\n\n/** Normalized view coordinates, not a serialized curve or runtime data model. */\nexport interface SpagbolCurveEditorPoint { x: number; y: number; locked?: boolean }\nexport interface SpagbolCurveEditorProps {\n /** x: 0..1, y: -1..1. The host projects its data into these view coordinates. */\n points: readonly SpagbolCurveEditorPoint[];\n backgroundCurves?: readonly { points: readonly SpagbolCurveEditorPoint[]; className?: string }[];\n className?: string;\n pathClassName?: string;\n ariaLabel?: string;\n /** Actual on-screen size, including canvas zoom, for constant-sized hit targets. */\n displaySize: { width: number; height: number };\n screenEmphasis?: number;\n pointScale?: number;\n minPointSpacing?: number;\n formatValue?: (normalizedValue: number) => string;\n toolbar?: ReactNode;\n underlay?: ReactNode;\n /** SVG children in the fixed 300 × 128 viewBox; decorative, without pointer events. */\n overlay?: ReactNode;\n footer?: ReactNode;\n /** Apply an insertion in host data and return the new point's index, or null to reject. */\n onInsertPoint: (point: { x: number; y: number }) => number | null;\n onMovePoint: (index: number, point: { x: number; y: number }) => void;\n onRemovePoint: (index: number) => void;\n onGestureEnd?: () => void;\n}\n\nconst width = 300, height = 128, padding = 12;\nconst innerWidth = width - padding * 2, innerHeight = height - padding * 2;\nconst clamp = (n: number, low: number, high: number) => Math.max(low, Math.min(high, n));\nconst screenPoint = (point: { x: number; y: number }) => ({ x: padding + point.x * innerWidth, y: padding + (1 - (point.y + 1) / 2) * innerHeight });\nfunction path(points: readonly SpagbolCurveEditorPoint[]) {\n return points.map((point, index) => {\n const p = screenPoint(point);\n // Preserve Teia's two-decimal SVG path precision.\n return `${index ? 'L' : 'M'}${Math.round(p.x * 100) / 100} ${Math.round(p.y * 100) / 100}`;\n }).join(' ');\n}\n\nexport function SpagbolCurveEditor({\n points, backgroundCurves = [], className = '', pathClassName = '', ariaLabel = 'Curve editor',\n displaySize, screenEmphasis = 1, pointScale = 1, minPointSpacing = 0.001,\n formatValue = (value) => String(Math.round(value * 1000) / 1000),\n toolbar, underlay, overlay, footer, onInsertPoint, onMovePoint, onRemovePoint, onGestureEnd,\n}: SpagbolCurveEditorProps) {\n const editorRef = useRef<SVGSVGElement | null>(null);\n const dragRef = useRef<{ pointerId: number; index: number } | null>(null);\n const removedByPress = useRef(false);\n const clickRef = useRef<{ index: number | null; time: number }>({ index: null, time: 0 });\n const [activePoint, setActivePoint] = useState<number | null>(null);\n const [hoveredPoint, setHoveredPoint] = useState<number | null>(null);\n const latest = useRef({ points, onMovePoint, onGestureEnd });\n latest.current = { points, onMovePoint, onGestureEnd };\n\n function fromPointer(event: { clientX: number; clientY: number }) {\n const rect = editorRef.current?.getBoundingClientRect();\n if (!rect || rect.width <= 0 || rect.height <= 0) return null;\n const x = clamp((event.clientX - rect.left) / rect.width * width, padding, width - padding);\n const y = clamp((event.clientY - rect.top) / rect.height * height, padding, height - padding);\n return { x: (x - padding) / innerWidth, y: (1 - (y - padding) / innerHeight) * 2 - 1 };\n }\n useEffect(() => {\n const move = (event: globalThis.PointerEvent) => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== event.pointerId) return;\n const current = latest.current.points;\n const selected = current[drag.index];\n if (!selected || selected.locked) return;\n event.preventDefault();\n const point = fromPointer(event);\n if (!point) return;\n const low = drag.index === 0 ? 0 : (current[drag.index - 1]?.x ?? 0) + minPointSpacing;\n const high = drag.index === current.length - 1 ? 1 : (current[drag.index + 1]?.x ?? 1) - minPointSpacing;\n // A host may reject a move; never cross adjacent point positions.\n if (low > high) return;\n latest.current.onMovePoint(drag.index, { x: clamp(point.x, low, high), y: point.y });\n };\n const stop = (event: globalThis.PointerEvent) => {\n if (dragRef.current?.pointerId !== event.pointerId) return;\n dragRef.current = null;\n setActivePoint(null);\n latest.current.onGestureEnd?.();\n };\n window.addEventListener('pointermove', move, { passive: false });\n window.addEventListener('pointerup', stop);\n window.addEventListener('pointercancel', stop);\n return () => {\n window.removeEventListener('pointermove', move);\n window.removeEventListener('pointerup', stop);\n window.removeEventListener('pointercancel', stop);\n };\n }, [minPointSpacing]);\n\n const hitIndex = (target: EventTarget) => {\n const element = target instanceof Element ? target.closest<SVGElement>('[data-spagbol-curve-point]') : null;\n return element ? Number(element.dataset.spagbolCurvePoint) : null;\n };\n const remove = (index: number) => {\n if (!points[index] || points[index].locked) return;\n if (dragRef.current) onGestureEnd?.();\n dragRef.current = null;\n setActivePoint(null);\n onRemovePoint(index);\n };\n const pointerDown = (event: PointerEvent<SVGSVGElement>) => {\n if (event.button !== 0 || dragRef.current) return;\n event.preventDefault(); event.stopPropagation();\n removedByPress.current = false;\n const index = hitIndex(event.target);\n if (index !== null && Number.isInteger(index)) {\n const now = performance.now();\n if (clickRef.current.index === index && now - clickRef.current.time < 360 && !points[index]?.locked) {\n clickRef.current = { index: null, time: 0 }; removedByPress.current = true; remove(index); return;\n }\n clickRef.current = { index, time: now };\n if (!points[index] || points[index].locked) return;\n dragRef.current = { pointerId: event.pointerId, index };\n setActivePoint(index); event.currentTarget.setPointerCapture(event.pointerId); return;\n }\n const point = fromPointer(event);\n if (!point) return;\n clickRef.current = { index: null, time: 0 };\n const insertedIndex = onInsertPoint(point);\n if (insertedIndex === null) return;\n dragRef.current = { pointerId: event.pointerId, index: insertedIndex };\n setActivePoint(insertedIndex); event.currentTarget.setPointerCapture(event.pointerId);\n };\n const doubleClick = (event: MouseEvent<SVGSVGElement>) => {\n event.preventDefault(); event.stopPropagation();\n if (removedByPress.current) { removedByPress.current = false; return; }\n const index = hitIndex(event.target);\n if (index !== null) remove(index);\n };\n const radius = (pixels: number) => ({ rx: pixels / Math.max(0.001, displaySize.width / width), ry: pixels / Math.max(0.001, displaySize.height / height) });\n const hitRadius = radius(15 * screenEmphasis);\n const endpointRadius = radius(5 * screenEmphasis * pointScale);\n const handleRadius = radius(6 * screenEmphasis * pointScale);\n const valuePoint = points[activePoint ?? hoveredPoint ?? -1];\n const valueScreen = valuePoint && screenPoint(valuePoint);\n return <div className={`spagbol-curve-editor ${className} nodrag nopan`}>\n <div className=\"spagbol-curve-editor-chart\">\n {toolbar}{underlay}\n <svg ref={editorRef} className=\"spagbol-curve-editor-canvas\" viewBox=\"0 0 300 128\" preserveAspectRatio=\"none\" aria-label={ariaLabel} role=\"img\" onPointerDown={pointerDown} onDoubleClick={doubleClick}\n onLostPointerCapture={(event) => {\n if (dragRef.current?.pointerId === event.pointerId) { dragRef.current = null; setActivePoint(null); onGestureEnd?.(); }\n }}>\n <g className=\"spagbol-curve-editor-chrome\">\n {backgroundCurves.map((curve, index) => <path key={index} className={`spagbol-curve-editor-path is-background ${curve.className ?? ''}`} d={path(curve.points)} />)}\n <path className={`spagbol-curve-editor-path ${pathClassName}`} d={path(points)} />\n {overlay}\n </g>\n {points.map((point, index) => {\n const screen = screenPoint(point);\n return <g key={index} onPointerEnter={() => setHoveredPoint(index)} onPointerLeave={() => setHoveredPoint((current) => current === index ? null : current)}>\n <ellipse className={`spagbol-curve-editor-hit-target ${point.locked ? 'is-locked' : ''}`} data-spagbol-curve-point={index} cx={screen.x} cy={screen.y} {...hitRadius} />\n <ellipse className={`spagbol-curve-editor-handle ${point.locked ? 'spagbol-curve-editor-endpoint is-locked' : ''}`} data-spagbol-curve-point={index} cx={screen.x} cy={screen.y} {...(point.locked ? endpointRadius : handleRadius)} />\n </g>;\n })}\n </svg>\n {valuePoint && valueScreen ? <span className={`spagbol-curve-editor-value-label ${valuePoint.x > 0.72 ? 'is-left' : ''} ${valuePoint.y > 0.72 ? 'is-below' : ''}`} style={{ left: `${valueScreen.x / width * 100}%`, top: `${valueScreen.y / height * 100}%` }} aria-hidden=\"true\">{formatValue(valuePoint.y)}</span> : null}\n </div>\n {footer}\n </div>;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"SpagbolNodeFrame.d.ts","sourceRoot":"","sources":["../../src/react/SpagbolNodeFrame.tsx"],"names":[],"mappings":"AAGA,OAAO,EAEL,QAAQ,EAIT,MAAM,eAAe,CAAC;AACvB,OAAO,EAAgF,KAAK,aAAa,EAAE,KAAK,GAAG,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACnJ,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AAKtB,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AACD,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,WAAkB,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,oBAAoB,+BAyB7G;AACD,MAAM,WAAW,qBAAsB,SAAQ,oBAAoB;IACjE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC;IAC5C,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,qFAAqF;IACrF,iBAAiB,CAAC,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC;IACpD,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AACD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,+BAwL5D;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,EACf,QAAQ,UAAQ,EAChB,SAAS,UAAQ,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,oBAAoB,CAmCtB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,sCAW/D"}
1
+ {"version":3,"file":"SpagbolNodeFrame.d.ts","sourceRoot":"","sources":["../../src/react/SpagbolNodeFrame.tsx"],"names":[],"mappings":"AAGA,OAAO,EAEL,QAAQ,EAIT,MAAM,eAAe,CAAC;AACvB,OAAO,EAAgF,KAAK,aAAa,EAAE,KAAK,GAAG,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACnJ,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AAKtB,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AACD,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,WAAkB,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,oBAAoB,+BA0B7G;AACD,MAAM,WAAW,qBAAsB,SAAQ,oBAAoB;IACjE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC;IAC5C,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,qFAAqF;IACrF,iBAAiB,CAAC,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC;IACpD,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AACD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,+BA8L5D;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,EACf,QAAQ,UAAQ,EAChB,SAAS,UAAQ,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,oBAAoB,CAkCtB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,sCAW/D"}