@signalsafe/tree-spec-editor-react 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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -28
  3. package/README.standalone.md +14 -0
  4. package/dist/GraphEditorCanvasContext.d.ts +26 -0
  5. package/dist/GraphEditorCanvasContext.d.ts.map +1 -0
  6. package/dist/GraphEditorCanvasContext.js +20 -0
  7. package/dist/TreeSpecGraphEditor.d.ts +22 -1
  8. package/dist/TreeSpecGraphEditor.d.ts.map +1 -1
  9. package/dist/TreeSpecGraphEditor.js +133 -260
  10. package/dist/canvas/constants.d.ts +41 -0
  11. package/dist/canvas/constants.d.ts.map +1 -0
  12. package/dist/canvas/constants.js +40 -0
  13. package/dist/canvas/edgeBuilders.d.ts +6 -0
  14. package/dist/canvas/edgeBuilders.d.ts.map +1 -0
  15. package/dist/canvas/edgeBuilders.js +57 -0
  16. package/dist/canvas/edgeStyle.d.ts +16 -0
  17. package/dist/canvas/edgeStyle.d.ts.map +1 -0
  18. package/dist/canvas/edgeStyle.js +44 -0
  19. package/dist/canvas/focusChoice.d.ts +3 -0
  20. package/dist/canvas/focusChoice.d.ts.map +1 -0
  21. package/dist/canvas/focusChoice.js +12 -0
  22. package/dist/canvas/typeGuards.d.ts +3 -0
  23. package/dist/canvas/typeGuards.d.ts.map +1 -0
  24. package/dist/canvas/typeGuards.js +16 -0
  25. package/dist/contextMenu/GraphCanvasContextMenu.d.ts +10 -0
  26. package/dist/contextMenu/GraphCanvasContextMenu.d.ts.map +1 -0
  27. package/dist/contextMenu/GraphCanvasContextMenu.js +39 -0
  28. package/dist/contextMenu/types.d.ts +11 -0
  29. package/dist/contextMenu/types.d.ts.map +1 -0
  30. package/dist/contextMenu/types.js +1 -0
  31. package/dist/hooks/keyboardShortcutDispatch.d.ts +30 -0
  32. package/dist/hooks/keyboardShortcutDispatch.d.ts.map +1 -0
  33. package/dist/hooks/keyboardShortcutDispatch.js +88 -0
  34. package/dist/hooks/types.d.ts +32 -2
  35. package/dist/hooks/types.d.ts.map +1 -1
  36. package/dist/hooks/useCanvasContextMenu.d.ts +15 -0
  37. package/dist/hooks/useCanvasContextMenu.d.ts.map +1 -0
  38. package/dist/hooks/useCanvasContextMenu.js +50 -0
  39. package/dist/hooks/useCanvasGraphState.d.ts +29 -0
  40. package/dist/hooks/useCanvasGraphState.d.ts.map +1 -0
  41. package/dist/hooks/useCanvasGraphState.js +150 -0
  42. package/dist/hooks/useCanvasIssueIndex.d.ts +12 -0
  43. package/dist/hooks/useCanvasIssueIndex.d.ts.map +1 -0
  44. package/dist/hooks/useCanvasIssueIndex.js +32 -0
  45. package/dist/hooks/useCanvasNodeResize.d.ts +17 -0
  46. package/dist/hooks/useCanvasNodeResize.d.ts.map +1 -0
  47. package/dist/hooks/useCanvasNodeResize.js +53 -0
  48. package/dist/hooks/useCanvasViewport.d.ts +12 -0
  49. package/dist/hooks/useCanvasViewport.d.ts.map +1 -0
  50. package/dist/hooks/useCanvasViewport.js +84 -0
  51. package/dist/hooks/useChoiceDragDrop.d.ts +25 -0
  52. package/dist/hooks/useChoiceDragDrop.d.ts.map +1 -0
  53. package/dist/hooks/useChoiceDragDrop.js +40 -0
  54. package/dist/hooks/useEditorAdapter.d.ts +46 -0
  55. package/dist/hooks/useEditorAdapter.d.ts.map +1 -0
  56. package/dist/hooks/useEditorAdapter.js +281 -0
  57. package/dist/hooks/useEditorAutosave.d.ts +18 -0
  58. package/dist/hooks/useEditorAutosave.d.ts.map +1 -0
  59. package/dist/hooks/useEditorAutosave.js +37 -0
  60. package/dist/hooks/useEditorHistory.d.ts +16 -0
  61. package/dist/hooks/useEditorHistory.d.ts.map +1 -0
  62. package/dist/hooks/useEditorHistory.js +103 -0
  63. package/dist/hooks/useEditorSelection.d.ts +22 -0
  64. package/dist/hooks/useEditorSelection.d.ts.map +1 -0
  65. package/dist/hooks/useEditorSelection.js +75 -0
  66. package/dist/hooks/useGraphConnect.d.ts +22 -0
  67. package/dist/hooks/useGraphConnect.d.ts.map +1 -0
  68. package/dist/hooks/useGraphConnect.js +75 -0
  69. package/dist/hooks/useTreeSpecEditor.d.ts +1 -9
  70. package/dist/hooks/useTreeSpecEditor.d.ts.map +1 -1
  71. package/dist/hooks/useTreeSpecEditor.js +231 -462
  72. package/dist/index.d.ts +4 -4
  73. package/dist/index.js +2 -2
  74. package/dist/nodes/ChoiceCanvasRow.d.ts +10 -0
  75. package/dist/nodes/ChoiceCanvasRow.d.ts.map +1 -0
  76. package/dist/nodes/ChoiceCanvasRow.js +55 -0
  77. package/dist/nodes/EndNode.d.ts +3 -0
  78. package/dist/nodes/EndNode.d.ts.map +1 -0
  79. package/dist/nodes/EndNode.js +7 -0
  80. package/dist/nodes/PromptNode.d.ts +6 -0
  81. package/dist/nodes/PromptNode.d.ts.map +1 -0
  82. package/dist/nodes/PromptNode.js +51 -0
  83. package/dist/nodes/PromptNodeChoicesList.d.ts +14 -0
  84. package/dist/nodes/PromptNodeChoicesList.d.ts.map +1 -0
  85. package/dist/nodes/PromptNodeChoicesList.js +24 -0
  86. package/dist/nodes/PromptNodeHeader.d.ts +10 -0
  87. package/dist/nodes/PromptNodeHeader.d.ts.map +1 -0
  88. package/dist/nodes/PromptNodeHeader.js +6 -0
  89. package/dist/nodes/PromptNodeIssueBadges.d.ts +7 -0
  90. package/dist/nodes/PromptNodeIssueBadges.d.ts.map +1 -0
  91. package/dist/nodes/PromptNodeIssueBadges.js +6 -0
  92. package/dist/nodes/PromptNodeToolbar.d.ts +6 -0
  93. package/dist/nodes/PromptNodeToolbar.d.ts.map +1 -0
  94. package/dist/nodes/PromptNodeToolbar.js +5 -0
  95. package/dist/nodes/types.d.ts +13 -0
  96. package/dist/nodes/types.d.ts.map +1 -0
  97. package/dist/nodes/types.js +1 -0
  98. package/dist/utils/joinClasses.d.ts +2 -0
  99. package/dist/utils/joinClasses.d.ts.map +1 -0
  100. package/dist/utils/joinClasses.js +3 -0
  101. package/package.json +36 -13
@@ -0,0 +1,32 @@
1
+ import { useMemo } from 'react';
2
+ import { TREE_SPEC_ISSUE_SEVERITY } from '@signalsafe/tree-spec';
3
+ export function useCanvasIssueIndex(issues) {
4
+ const issuesByNode = useMemo(() => {
5
+ const m = new Map();
6
+ for (const i of issues) {
7
+ if (!i.node_id)
8
+ continue;
9
+ const cur = m.get(i.node_id) ?? { total: 0, errors: 0, warnings: 0, info: 0 };
10
+ cur.total += 1;
11
+ const sev = String(i.severity ?? '').toLowerCase();
12
+ if (sev === TREE_SPEC_ISSUE_SEVERITY.WARNING)
13
+ cur.warnings += 1;
14
+ else if (sev === TREE_SPEC_ISSUE_SEVERITY.INFO)
15
+ cur.info += 1;
16
+ else
17
+ cur.errors += 1;
18
+ m.set(i.node_id, cur);
19
+ }
20
+ return m;
21
+ }, [issues]);
22
+ const issueKeySet = useMemo(() => {
23
+ const s = new Set();
24
+ for (const i of issues) {
25
+ if (!i.node_id || !i.choice_id)
26
+ continue;
27
+ s.add(`${i.node_id}::${i.choice_id}`);
28
+ }
29
+ return s;
30
+ }, [issues]);
31
+ return { issuesByNode, issueKeySet };
32
+ }
@@ -0,0 +1,17 @@
1
+ import type { Node } from 'reactflow';
2
+ import { type EditorTree } from '@signalsafe/tree-spec-editor-core';
3
+ export type UseCanvasNodeResizeOptions = {
4
+ readOnly: boolean;
5
+ treeRef: React.MutableRefObject<EditorTree>;
6
+ onChange: (next: EditorTree) => void;
7
+ setNodes: (value: Node[] | ((nodes: Node[]) => Node[])) => void;
8
+ isResizingRef: React.MutableRefObject<boolean>;
9
+ resizeHeightByNodeId: Record<string, number>;
10
+ setResizeHeightByNodeId: React.Dispatch<React.SetStateAction<Record<string, number>>>;
11
+ };
12
+ export type UseCanvasNodeResizeResult = {
13
+ handleResizeNodeStart: (nodeId: string, width: number, height: number) => void;
14
+ handleResizeNode: (nodeId: string, width: number, height: number) => void;
15
+ };
16
+ export declare function useCanvasNodeResize(options: UseCanvasNodeResizeOptions): UseCanvasNodeResizeResult;
17
+ //# sourceMappingURL=useCanvasNodeResize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCanvasNodeResize.d.ts","sourceRoot":"","sources":["../../src/hooks/useCanvasNodeResize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,EAAkC,KAAK,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAEpG,MAAM,MAAM,0BAA0B,GAAG;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,KAAK,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC5C,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IAChE,aAAa,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC/C,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,uBAAuB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CACzF,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACpC,qBAAqB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/E,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7E,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,yBAAyB,CAmElG"}
@@ -0,0 +1,53 @@
1
+ import { useCallback } from 'react';
2
+ import { isNodeLocked, patchEditorHints } from '@signalsafe/tree-spec-editor-core';
3
+ export function useCanvasNodeResize(options) {
4
+ const { readOnly, treeRef, onChange, setNodes, isResizingRef, setResizeHeightByNodeId, } = options;
5
+ const handleResizeNodeStart = useCallback((nodeId, width, height) => {
6
+ if (readOnly)
7
+ return;
8
+ isResizingRef.current = true;
9
+ const lockedHeight = Math.round(height);
10
+ setResizeHeightByNodeId((prev) => ({ ...prev, [nodeId]: lockedHeight }));
11
+ setNodes((current) => current.map((node) => node.id === nodeId
12
+ ? {
13
+ ...node,
14
+ style: {
15
+ ...node.style,
16
+ width: Math.round(width),
17
+ height: lockedHeight,
18
+ },
19
+ data: {
20
+ ...node.data,
21
+ lockedResizeHeight: lockedHeight,
22
+ },
23
+ }
24
+ : node));
25
+ }, [readOnly, setNodes, isResizingRef, setResizeHeightByNodeId]);
26
+ const handleResizeNode = useCallback((nodeId, width, height) => {
27
+ const node = treeRef.current.nodes[nodeId];
28
+ if (!node || isNodeLocked(node) || readOnly)
29
+ return;
30
+ isResizingRef.current = false;
31
+ setResizeHeightByNodeId((prev) => {
32
+ if (!(nodeId in prev))
33
+ return prev;
34
+ const next = { ...prev };
35
+ delete next[nodeId];
36
+ return next;
37
+ });
38
+ onChange({
39
+ ...treeRef.current,
40
+ nodes: {
41
+ ...treeRef.current.nodes,
42
+ [nodeId]: patchEditorHints(node, {
43
+ width: Math.round(width),
44
+ height: Math.round(height),
45
+ }),
46
+ },
47
+ });
48
+ }, [onChange, readOnly, treeRef, isResizingRef, setResizeHeightByNodeId]);
49
+ return {
50
+ handleResizeNodeStart,
51
+ handleResizeNode,
52
+ };
53
+ }
@@ -0,0 +1,12 @@
1
+ import type { ReactFlowInstance } from 'reactflow';
2
+ import { type GraphSelection } from '@signalsafe/tree-spec-editor-core';
3
+ export type UseCanvasViewportOptions = {
4
+ rf: ReactFlowInstance;
5
+ focusNodeId?: string | null;
6
+ fitViewNonce?: number;
7
+ contextualZoom?: boolean;
8
+ selected?: GraphSelection;
9
+ suppressViewportSaveRef: React.MutableRefObject<boolean>;
10
+ };
11
+ export declare function useCanvasViewport(options: UseCanvasViewportOptions): void;
12
+ //# sourceMappingURL=useCanvasViewport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCanvasViewport.d.ts","sourceRoot":"","sources":["../../src/hooks/useCanvasViewport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD,OAAO,EAAqC,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAE3G,MAAM,MAAM,wBAAwB,GAAG;IACnC,EAAE,EAAE,iBAAiB,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,uBAAuB,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;CAC5D,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAqFzE"}
@@ -0,0 +1,84 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { END_NODE_ID, GRAPH_SELECTION_KIND } from '@signalsafe/tree-spec-editor-core';
3
+ export function useCanvasViewport(options) {
4
+ const { rf, focusNodeId, fitViewNonce, contextualZoom = true, selected, suppressViewportSaveRef, } = options;
5
+ const lastContextualSelectionRef = useRef(null);
6
+ useEffect(() => {
7
+ if (!focusNodeId)
8
+ return;
9
+ suppressViewportSaveRef.current = true;
10
+ try {
11
+ const n = rf.getNode(focusNodeId);
12
+ if (!n)
13
+ return;
14
+ rf.setCenter(n.position.x + 150, n.position.y + 60, { zoom: 1, duration: 300 });
15
+ }
16
+ catch {
17
+ // ignore
18
+ }
19
+ const releaseId = globalThis.setTimeout(() => {
20
+ suppressViewportSaveRef.current = false;
21
+ }, 350);
22
+ return () => globalThis.clearTimeout(releaseId);
23
+ }, [focusNodeId, rf, suppressViewportSaveRef]);
24
+ useEffect(() => {
25
+ if (!fitViewNonce)
26
+ return;
27
+ suppressViewportSaveRef.current = true;
28
+ const id = globalThis.setTimeout(() => {
29
+ try {
30
+ rf.fitView({ padding: 0.2, duration: 300 });
31
+ }
32
+ catch {
33
+ // ignore
34
+ }
35
+ }, 100);
36
+ const releaseId = globalThis.setTimeout(() => {
37
+ suppressViewportSaveRef.current = false;
38
+ }, 450);
39
+ return () => {
40
+ globalThis.clearTimeout(id);
41
+ globalThis.clearTimeout(releaseId);
42
+ };
43
+ }, [fitViewNonce, rf, suppressViewportSaveRef]);
44
+ useEffect(() => {
45
+ if (!contextualZoom || !selected?.id)
46
+ return;
47
+ if (selected.kind === GRAPH_SELECTION_KIND.EDGE)
48
+ return;
49
+ if (focusNodeId && selected.kind === GRAPH_SELECTION_KIND.NODE && focusNodeId === selected.id) {
50
+ return;
51
+ }
52
+ const selectionKey = `${selected.kind}:${selected.id}`;
53
+ if (lastContextualSelectionRef.current === selectionKey)
54
+ return;
55
+ lastContextualSelectionRef.current = selectionKey;
56
+ const nodeIds = [];
57
+ if (selected.kind === GRAPH_SELECTION_KIND.NODE && selected.id !== END_NODE_ID) {
58
+ nodeIds.push(selected.id);
59
+ }
60
+ if (nodeIds.length === 0)
61
+ return;
62
+ suppressViewportSaveRef.current = true;
63
+ const id = globalThis.setTimeout(() => {
64
+ try {
65
+ rf.fitView({
66
+ nodes: nodeIds.map((nodeId) => ({ id: nodeId })),
67
+ padding: 0.35,
68
+ duration: 250,
69
+ maxZoom: 1.25,
70
+ });
71
+ }
72
+ catch {
73
+ // ignore
74
+ }
75
+ }, 0);
76
+ const releaseId = globalThis.setTimeout(() => {
77
+ suppressViewportSaveRef.current = false;
78
+ }, 300);
79
+ return () => {
80
+ globalThis.clearTimeout(id);
81
+ globalThis.clearTimeout(releaseId);
82
+ };
83
+ }, [contextualZoom, focusNodeId, rf, selected, suppressViewportSaveRef]);
84
+ }
@@ -0,0 +1,25 @@
1
+ import { type GraphSelection } from '@signalsafe/tree-spec-editor-core';
2
+ export type ChoiceDragState = {
3
+ sourceNodeId: string;
4
+ choiceId: string;
5
+ };
6
+ export type ChoiceDropTarget = {
7
+ nodeId: string;
8
+ index: number;
9
+ };
10
+ export type UseChoiceDragDropOptions = {
11
+ readOnly: boolean;
12
+ onChoiceSelect?: (nodeId: string, choiceId: string) => void;
13
+ onSelect?: (sel: GraphSelection) => void;
14
+ onRepositionChoice?: (fromNodeId: string, choiceId: string, toNodeId: string, toIndex: number) => void;
15
+ };
16
+ export type UseChoiceDragDropResult = {
17
+ choiceDrag: ChoiceDragState | null;
18
+ choiceDropTarget: ChoiceDropTarget | null;
19
+ handleChoiceDragStart: (nodeId: string, choiceId: string) => void;
20
+ handleChoiceDragEnd: () => void;
21
+ handleChoiceDragOver: (nodeId: string, index: number) => void;
22
+ handleChoiceDrop: (targetNodeId: string, targetIndex: number) => void;
23
+ };
24
+ export declare function useChoiceDragDrop(options: UseChoiceDragDropOptions): UseChoiceDragDropResult;
25
+ //# sourceMappingURL=useChoiceDragDrop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useChoiceDragDrop.d.ts","sourceRoot":"","sources":["../../src/hooks/useChoiceDragDrop.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAE9F,MAAM,MAAM,eAAe,GAAG;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACnC,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,kBAAkB,CAAC,EAAE,CACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,KACd,IAAI,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IAClC,UAAU,EAAE,eAAe,GAAG,IAAI,CAAC;IACnC,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC1C,qBAAqB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAClE,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9D,gBAAgB,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACzE,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,uBAAuB,CAmD5F"}
@@ -0,0 +1,40 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { GRAPH_SELECTION_KIND } from '@signalsafe/tree-spec-editor-core';
3
+ export function useChoiceDragDrop(options) {
4
+ const { readOnly, onChoiceSelect, onSelect, onRepositionChoice } = options;
5
+ const [choiceDrag, setChoiceDrag] = useState(null);
6
+ const [choiceDropTarget, setChoiceDropTarget] = useState(null);
7
+ const handleChoiceDragStart = useCallback((nodeId, choiceId) => {
8
+ if (readOnly)
9
+ return;
10
+ setChoiceDrag({ sourceNodeId: nodeId, choiceId });
11
+ if (onChoiceSelect) {
12
+ onChoiceSelect(nodeId, choiceId);
13
+ }
14
+ else {
15
+ onSelect?.({ kind: GRAPH_SELECTION_KIND.NODE, id: nodeId });
16
+ }
17
+ }, [readOnly, onChoiceSelect, onSelect]);
18
+ const handleChoiceDragEnd = useCallback(() => {
19
+ setChoiceDrag(null);
20
+ setChoiceDropTarget(null);
21
+ }, []);
22
+ const handleChoiceDragOver = useCallback((nodeId, index) => {
23
+ setChoiceDropTarget({ nodeId, index });
24
+ }, []);
25
+ const handleChoiceDrop = useCallback((targetNodeId, targetIndex) => {
26
+ if (readOnly || !choiceDrag)
27
+ return;
28
+ onRepositionChoice?.(choiceDrag.sourceNodeId, choiceDrag.choiceId, targetNodeId, targetIndex);
29
+ setChoiceDrag(null);
30
+ setChoiceDropTarget(null);
31
+ }, [readOnly, choiceDrag, onRepositionChoice]);
32
+ return {
33
+ choiceDrag,
34
+ choiceDropTarget,
35
+ handleChoiceDragStart,
36
+ handleChoiceDragEnd,
37
+ handleChoiceDragOver,
38
+ handleChoiceDrop,
39
+ };
40
+ }
@@ -0,0 +1,46 @@
1
+ import { type MutableRefObject, type RefObject } from 'react';
2
+ import { type TreeSpecIssue, type TreeSpecWire } from '@signalsafe/tree-spec';
3
+ import { type AutosaveStatus, type EditorTree, type TreeSpecAuditEventItem, type TreeSpecSnapshotItem } from '@signalsafe/tree-spec-editor-core';
4
+ import type { GraphEditorVersionInfo, UseTreeSpecEditorActions, UseTreeSpecEditorOptions } from './types.js';
5
+ export type UseEditorAdapterOptions = {
6
+ options: UseTreeSpecEditorOptions;
7
+ tree: EditorTree | null;
8
+ isPublished: boolean;
9
+ setIsPublished: (next: boolean) => void;
10
+ replaceTreeWithoutHistory: (next: EditorTree | null) => void;
11
+ lastSavedKeyRef: MutableRefObject<string>;
12
+ setAutosaveStatus: (status: AutosaveStatus) => void;
13
+ };
14
+ export type UseEditorAdapterResult = {
15
+ loading: boolean;
16
+ saving: boolean;
17
+ publishing: boolean;
18
+ setPublishing: (next: boolean) => void;
19
+ creatingSnapshot: boolean;
20
+ cloning: boolean;
21
+ restoringSnapshotId: string | null;
22
+ lastValidatedAt: string | null;
23
+ rawTreeSpec: TreeSpecWire | null;
24
+ versionInfo: GraphEditorVersionInfo | null;
25
+ localIssues: TreeSpecIssue[];
26
+ serverIssues: TreeSpecIssue[];
27
+ snapshots: TreeSpecSnapshotItem[];
28
+ auditEvents: TreeSpecAuditEventItem[];
29
+ loadingSnapshots: boolean;
30
+ loadingAudit: boolean;
31
+ showDraftHistory: boolean;
32
+ setShowDraftHistory: (next: boolean) => void;
33
+ showAudit: boolean;
34
+ setShowAudit: (next: boolean) => void;
35
+ showPublishModal: boolean;
36
+ setShowPublishModal: (next: boolean) => void;
37
+ validate: UseTreeSpecEditorActions['validate'];
38
+ saveDraft: UseTreeSpecEditorActions['saveDraft'];
39
+ createSnapshot: UseTreeSpecEditorActions['createSnapshot'];
40
+ restoreSnapshot: UseTreeSpecEditorActions['restoreSnapshot'];
41
+ cloneToDraft: UseTreeSpecEditorActions['cloneToDraft'];
42
+ validateRef: RefObject<UseTreeSpecEditorActions['validate']>;
43
+ saveDraftRef: RefObject<UseTreeSpecEditorActions['saveDraft']>;
44
+ };
45
+ export declare function useEditorAdapter(adapterOptions: UseEditorAdapterOptions): UseEditorAdapterResult;
46
+ //# sourceMappingURL=useEditorAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEditorAdapter.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqD,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEjH,OAAO,EAKH,KAAK,aAAa,EAClB,KAAK,YAAY,EACpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAQH,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EAC5B,MAAM,mCAAmC,CAAC;AAE3C,OAAO,KAAK,EAER,sBAAsB,EACtB,wBAAwB,EACxB,wBAAwB,EAC3B,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,uBAAuB,GAAG;IAClC,OAAO,EAAE,wBAAwB,CAAC;IAClC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,yBAAyB,EAAE,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7D,eAAe,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,iBAAiB,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,YAAY,GAAG,IAAI,CAAC;IACjC,WAAW,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC3C,WAAW,EAAE,aAAa,EAAE,CAAC;IAC7B,YAAY,EAAE,aAAa,EAAE,CAAC;IAC9B,SAAS,EAAE,oBAAoB,EAAE,CAAC;IAClC,WAAW,EAAE,sBAAsB,EAAE,CAAC;IACtC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,mBAAmB,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,mBAAmB,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,QAAQ,EAAE,wBAAwB,CAAC,UAAU,CAAC,CAAC;IAC/C,SAAS,EAAE,wBAAwB,CAAC,WAAW,CAAC,CAAC;IACjD,cAAc,EAAE,wBAAwB,CAAC,gBAAgB,CAAC,CAAC;IAC3D,eAAe,EAAE,wBAAwB,CAAC,iBAAiB,CAAC,CAAC;IAC7D,YAAY,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAC;IACvD,WAAW,EAAE,SAAS,CAAC,wBAAwB,CAAC,UAAU,CAAC,CAAC,CAAC;IAC7D,YAAY,EAAE,SAAS,CAAC,wBAAwB,CAAC,WAAW,CAAC,CAAC,CAAC;CAClE,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,cAAc,EAAE,uBAAuB,GAAG,sBAAsB,CAsShG"}
@@ -0,0 +1,281 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { compileTreeSpec, decompileTreeSpec, lintTreeSpecWire, TREE_SPEC_ISSUE_SEVERITY, } from '@signalsafe/tree-spec';
3
+ import { autoLayoutTree, AUTOSAVE_STATUS, coerceTreeSpecWireForEditor as defaultCoerceRawSpec, needsInitialLayout, lintEditorTree, parsePydanticOutcomeErrors as defaultParseServerErrorMessage, shouldQueueInitialValidation as defaultShouldQueueInitialValidation, } from '@signalsafe/tree-spec-editor-core';
4
+ export function useEditorAdapter(adapterOptions) {
5
+ const { options, tree, isPublished, setIsPublished, replaceTreeWithoutHistory, lastSavedKeyRef, setAutosaveStatus, } = adapterOptions;
6
+ const { adapter, entityId, coerceRawSpec = defaultCoerceRawSpec, onCloneNavigate, parseServerErrorMessage = defaultParseServerErrorMessage, shouldQueueInitialValidation = defaultShouldQueueInitialValidation, } = options;
7
+ const [loading, setLoading] = useState(true);
8
+ const [saving, setSaving] = useState(false);
9
+ const [publishing, setPublishing] = useState(false);
10
+ const [rawTreeSpec, setRawTreeSpec] = useState(null);
11
+ const [versionInfo, setVersionInfo] = useState(null);
12
+ const [localIssues, setLocalIssues] = useState([]);
13
+ const [serverIssues, setServerIssues] = useState([]);
14
+ const [lastValidatedAt, setLastValidatedAt] = useState(null);
15
+ const [showDraftHistory, setShowDraftHistory] = useState(false);
16
+ const [showAudit, setShowAudit] = useState(false);
17
+ const [showPublishModal, setShowPublishModal] = useState(false);
18
+ const [snapshots, setSnapshots] = useState([]);
19
+ const [auditEvents, setAuditEvents] = useState([]);
20
+ const [loadingSnapshots, setLoadingSnapshots] = useState(false);
21
+ const [loadingAudit, setLoadingAudit] = useState(false);
22
+ const [restoringSnapshotId, setRestoringSnapshotId] = useState(null);
23
+ const [creatingSnapshot, setCreatingSnapshot] = useState(false);
24
+ const [cloning, setCloning] = useState(false);
25
+ const validateRef = useRef(async () => undefined);
26
+ const saveDraftRef = useRef(async () => undefined);
27
+ const compiledTreeSpec = useMemo(() => (tree ? compileTreeSpec(tree) : null), [tree]);
28
+ useEffect(() => {
29
+ let cancelled = false;
30
+ async function load() {
31
+ if (!entityId)
32
+ return;
33
+ setLoading(true);
34
+ setVersionInfo(null);
35
+ try {
36
+ const raw = await adapter.getVersion(entityId);
37
+ if (cancelled)
38
+ return;
39
+ setIsPublished(Boolean(raw?.is_published));
40
+ setVersionInfo(raw?.info ?? null);
41
+ const rawSpec = raw?.tree_spec;
42
+ const spec = coerceRawSpec(rawSpec);
43
+ if (spec == null) {
44
+ setRawTreeSpec(null);
45
+ replaceTreeWithoutHistory(null);
46
+ setVersionInfo(raw?.info ?? null);
47
+ }
48
+ else {
49
+ setRawTreeSpec(spec);
50
+ let nextTree = decompileTreeSpec(spec);
51
+ if (needsInitialLayout(nextTree)) {
52
+ nextTree = autoLayoutTree(nextTree);
53
+ }
54
+ replaceTreeWithoutHistory(nextTree);
55
+ setLocalIssues([...lintTreeSpecWire(spec), ...lintEditorTree(nextTree)]);
56
+ if (shouldQueueInitialValidation(raw?.is_published)) {
57
+ queueMicrotask(() => {
58
+ void validateRef.current(spec);
59
+ });
60
+ }
61
+ lastSavedKeyRef.current = JSON.stringify(spec);
62
+ setAutosaveStatus(AUTOSAVE_STATUS.IDLE);
63
+ }
64
+ }
65
+ catch {
66
+ if (cancelled)
67
+ return;
68
+ setRawTreeSpec(null);
69
+ replaceTreeWithoutHistory(null);
70
+ setVersionInfo(null);
71
+ }
72
+ finally {
73
+ if (!cancelled)
74
+ setLoading(false);
75
+ }
76
+ }
77
+ if (entityId)
78
+ void load();
79
+ return () => {
80
+ cancelled = true;
81
+ };
82
+ }, [
83
+ entityId,
84
+ adapter,
85
+ coerceRawSpec,
86
+ shouldQueueInitialValidation,
87
+ replaceTreeWithoutHistory,
88
+ lastSavedKeyRef,
89
+ setAutosaveStatus,
90
+ setIsPublished,
91
+ ]);
92
+ useEffect(() => {
93
+ if (!tree || !compiledTreeSpec)
94
+ return;
95
+ setLocalIssues([...lintTreeSpecWire(compiledTreeSpec), ...lintEditorTree(tree)]);
96
+ }, [tree, compiledTreeSpec]);
97
+ const validate = useCallback(async (specOverride) => {
98
+ if (!entityId)
99
+ return undefined;
100
+ const spec = specOverride ?? (tree ? compileTreeSpec(tree) : rawTreeSpec);
101
+ if (!spec)
102
+ return undefined;
103
+ if (!adapter.validate) {
104
+ setLastValidatedAt(new Date().toISOString());
105
+ return { valid: true };
106
+ }
107
+ try {
108
+ const payload = await adapter.validate(entityId, spec);
109
+ const nextIssues = (payload?.issues ?? []).map((i) => {
110
+ const severity = i.severity ?? i.level ?? TREE_SPEC_ISSUE_SEVERITY.ERROR;
111
+ return {
112
+ severity,
113
+ message: String(i.message ?? 'Issue'),
114
+ node_id: i.node_id ?? undefined,
115
+ choice_id: i.choice_id ?? undefined,
116
+ };
117
+ });
118
+ setServerIssues(nextIssues);
119
+ setLastValidatedAt(new Date().toISOString());
120
+ return payload;
121
+ }
122
+ catch (err) {
123
+ const ex = err;
124
+ const errData = ex?.response?.data;
125
+ const msg = errData?.detail ??
126
+ errData?.error ??
127
+ err?.message ??
128
+ 'Validation failed';
129
+ const parsed = parseServerErrorMessage(String(msg));
130
+ setServerIssues(parsed ?? [{ severity: TREE_SPEC_ISSUE_SEVERITY.ERROR, message: String(msg) }]);
131
+ setLastValidatedAt(new Date().toISOString());
132
+ return {
133
+ valid: false,
134
+ issues: [{ severity: TREE_SPEC_ISSUE_SEVERITY.ERROR, message: String(msg) }],
135
+ };
136
+ }
137
+ }, [adapter, entityId, parseServerErrorMessage, rawTreeSpec, tree]);
138
+ useEffect(() => {
139
+ validateRef.current = validate;
140
+ }, [validate]);
141
+ const saveDraft = useCallback(async () => {
142
+ if (!entityId || !tree)
143
+ return;
144
+ if (isPublished)
145
+ return;
146
+ setSaving(true);
147
+ setAutosaveStatus(AUTOSAVE_STATUS.SAVING);
148
+ try {
149
+ const compiled = compileTreeSpec(tree);
150
+ await adapter.updateVersion(entityId, { tree_spec: compiled });
151
+ setRawTreeSpec(compiled);
152
+ lastSavedKeyRef.current = JSON.stringify(compiled);
153
+ setAutosaveStatus(AUTOSAVE_STATUS.SAVED);
154
+ }
155
+ finally {
156
+ setSaving(false);
157
+ }
158
+ }, [adapter, entityId, isPublished, tree, lastSavedKeyRef, setAutosaveStatus]);
159
+ useEffect(() => {
160
+ saveDraftRef.current = saveDraft;
161
+ }, [saveDraft]);
162
+ useEffect(() => {
163
+ const listSnapshots = adapter.listSnapshots;
164
+ if (!listSnapshots || !showDraftHistory || !entityId)
165
+ return;
166
+ let cancelled = false;
167
+ setLoadingSnapshots(true);
168
+ void (async () => {
169
+ try {
170
+ const list = await listSnapshots(entityId);
171
+ if (!cancelled)
172
+ setSnapshots(list);
173
+ }
174
+ finally {
175
+ if (!cancelled)
176
+ setLoadingSnapshots(false);
177
+ }
178
+ })();
179
+ return () => {
180
+ cancelled = true;
181
+ };
182
+ }, [adapter, showDraftHistory, entityId]);
183
+ const createSnapshot = useCallback(async () => {
184
+ if (!entityId || !tree || !adapter.createSnapshot || !adapter.listSnapshots)
185
+ return;
186
+ setCreatingSnapshot(true);
187
+ try {
188
+ const compiled = compileTreeSpec(tree);
189
+ await adapter.createSnapshot(entityId, { label: '', tree_spec: compiled });
190
+ const list = await adapter.listSnapshots(entityId);
191
+ setSnapshots(list);
192
+ }
193
+ finally {
194
+ setCreatingSnapshot(false);
195
+ }
196
+ }, [adapter, entityId, tree]);
197
+ const restoreSnapshot = useCallback(async (snapshotId) => {
198
+ if (!entityId || !adapter.restoreSnapshot)
199
+ return;
200
+ setRestoringSnapshotId(snapshotId);
201
+ try {
202
+ const { tree_spec: rawSpec } = await adapter.restoreSnapshot(entityId, snapshotId);
203
+ const spec = coerceRawSpec(rawSpec);
204
+ if (spec != null) {
205
+ setRawTreeSpec(spec);
206
+ replaceTreeWithoutHistory(decompileTreeSpec(spec));
207
+ lastSavedKeyRef.current = JSON.stringify(spec);
208
+ setAutosaveStatus(AUTOSAVE_STATUS.IDLE);
209
+ }
210
+ setShowDraftHistory(false);
211
+ }
212
+ finally {
213
+ setRestoringSnapshotId(null);
214
+ }
215
+ }, [adapter, coerceRawSpec, entityId, replaceTreeWithoutHistory, lastSavedKeyRef, setAutosaveStatus]);
216
+ useEffect(() => {
217
+ const listAudit = adapter.listAudit;
218
+ if (!listAudit || !showAudit || !entityId)
219
+ return;
220
+ let cancelled = false;
221
+ setLoadingAudit(true);
222
+ void (async () => {
223
+ try {
224
+ const list = await listAudit(entityId);
225
+ if (!cancelled)
226
+ setAuditEvents(list);
227
+ }
228
+ finally {
229
+ if (!cancelled)
230
+ setLoadingAudit(false);
231
+ }
232
+ })();
233
+ return () => {
234
+ cancelled = true;
235
+ };
236
+ }, [adapter, showAudit, entityId]);
237
+ const cloneToDraft = useCallback(async () => {
238
+ if (!entityId || !adapter.cloneToDraft)
239
+ return;
240
+ setCloning(true);
241
+ try {
242
+ const { id: newId } = await adapter.cloneToDraft(entityId);
243
+ if (newId)
244
+ onCloneNavigate?.(newId);
245
+ }
246
+ finally {
247
+ setCloning(false);
248
+ }
249
+ }, [adapter, entityId, onCloneNavigate]);
250
+ return {
251
+ loading,
252
+ saving,
253
+ publishing,
254
+ setPublishing,
255
+ creatingSnapshot,
256
+ cloning,
257
+ restoringSnapshotId,
258
+ lastValidatedAt,
259
+ rawTreeSpec,
260
+ versionInfo,
261
+ localIssues,
262
+ serverIssues,
263
+ snapshots,
264
+ auditEvents,
265
+ loadingSnapshots,
266
+ loadingAudit,
267
+ showDraftHistory,
268
+ setShowDraftHistory,
269
+ showAudit,
270
+ setShowAudit,
271
+ showPublishModal,
272
+ setShowPublishModal,
273
+ validate,
274
+ saveDraft,
275
+ createSnapshot,
276
+ restoreSnapshot,
277
+ cloneToDraft,
278
+ validateRef,
279
+ saveDraftRef,
280
+ };
281
+ }
@@ -0,0 +1,18 @@
1
+ import { type MutableRefObject, type RefObject } from 'react';
2
+ import { type AutosaveStatus, type EditorTree } from '@signalsafe/tree-spec-editor-core';
3
+ import type { UseTreeSpecEditorActions } from './types.js';
4
+ export type UseEditorAutosaveOptions = {
5
+ enableAutosave: boolean;
6
+ autosaveDebounceMs: number;
7
+ entityId: string | undefined;
8
+ tree: EditorTree | null;
9
+ isPublished: boolean;
10
+ saving: boolean;
11
+ publishing: boolean;
12
+ autosaveStatus: AutosaveStatus;
13
+ setAutosaveStatus: (status: AutosaveStatus) => void;
14
+ lastSavedKeyRef: MutableRefObject<string>;
15
+ saveDraftRef: RefObject<UseTreeSpecEditorActions['saveDraft']>;
16
+ };
17
+ export declare function useEditorAutosave(options: UseEditorAutosaveOptions): void;
18
+ //# sourceMappingURL=useEditorAutosave.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEditorAutosave.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorAutosave.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIjF,OAAO,EAAmB,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAE1G,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAExD,MAAM,MAAM,wBAAwB,GAAG;IACnC,cAAc,EAAE,OAAO,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IACpD,eAAe,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,YAAY,EAAE,SAAS,CAAC,wBAAwB,CAAC,WAAW,CAAC,CAAC,CAAC;CAClE,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CA2CzE"}