@signalsafe/tree-spec-editor-react 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,64 +1,140 @@
1
1
  # @signalsafe/tree-spec-editor-react
2
2
 
3
- Headless React layer for the SignalSafe TreeSpec graph editor. Renders the
4
- React Flow canvas, owns React state plumbing, and exposes the editor as a
5
- single React component — without depending on any specific UI library.
3
+ Headless **React + React Flow** layer for the SignalSafe TreeSpec graph editor: canvas component, orchestration hook, and wiring to `@signalsafe/tree-spec-editor-core`.
6
4
 
7
- This is the React-specific sibling to **[`@signalsafe/tree-spec-editor-core`](../tree-spec-editor-core/README.md)**. The core package owns model + helpers (zero UI deps); this package owns React rendering. UI shells (e.g. `@signalsafe/tree-spec-editor` for React + Bootstrap, planned `@signalsafe/tree-spec-editor-react-mui` for React + Material) layer on top.
5
+ | | |
6
+ |---|---|
7
+ | **npm** | `@signalsafe/tree-spec-editor-react` |
8
+ | **GitHub** | [SignalSafeSoftware/tree-spec-editor-react](https://github.com/SignalSafeSoftware/tree-spec-editor-react) |
9
+ | **Peer deps** | `react`, `react-dom`, `reactflow` (^18 / ^11) |
8
10
 
9
- ## What this package owns
11
+ ## What this package does
10
12
 
11
- - **`TreeSpecGraphEditor`** — the React Flow canvas (Background, Controls, MiniMap, custom node renderer, transition edges, selection wiring, choice focus highlighting, focus/fit-view).
12
- - **`useTreeSpecEditor`** stateful orchestration hook (load, autosave, validate, publish, selection, undo/redo, choice/edge helpers). Exposes `inspectorNode` (selected node or edge source node), `focusChoiceId`, and `selectedEdge` for sidebar panels.
13
- - **`TreeSpecGraphEditorProps`** props type.
13
+ - Renders **`TreeSpecGraphEditor`** (React Flow canvas, custom nodes/edges, selection).
14
+ - Exposes **`useTreeSpecEditor`** for load/validate/autosave/publish orchestration (host injects adapter callbacks).
15
+ - Re-exports core editor types used by the hook.
14
16
 
15
- ## Canvas selection behavior
17
+ ## What this package does not do
16
18
 
17
- | Selection | Inspector context | Contextual zoom (`contextualZoom`, default `true`) |
18
- |-----------|-------------------|-----------------------------------------------------|
19
- | **Node** | `selectedNode` | Fits viewport to the node |
20
- | **Edge** | `inspectorNode` = source node; `focusChoiceId` = source choice | **No pan/zoom** (viewport stays put) |
21
- | **Choice** (canvas or inspector focus) | `focusChoiceId` set; nodes list highlights via `focusNodeId` | Fits when the parent node is selected |
19
+ - Sidebar panels, modals, toolbars, or Bootstrap chrome use `@signalsafe/tree-spec-editor` or your own UI shell.
20
+ - Routing, HTTP, authentication, or persistence — host app provides adapter implementations.
21
+ - Wire compile/publish to a backend without your adapter code.
22
22
 
23
- Pass `onChoiceSelect` + `focusChoiceId` to keep canvas choice rows, inspector choice cards, and the nodes list in sync. Pass `contextualZoom={false}` to disable automatic viewport fitting on node selection.
23
+ ## Install
24
24
 
25
- ## What lives elsewhere
25
+ ```bash
26
+ npm install @signalsafe/tree-spec-editor-react @signalsafe/tree-spec-editor-core @signalsafe/tree-spec react react-dom reactflow
27
+ ```
26
28
 
27
- | Concern | Package |
28
- |--------|---------|
29
- | Editor model, tree operations, layout, autosave/keyboard helpers, choice edge hints | `@signalsafe/tree-spec-editor-core` |
30
- | Sidebar panels, inspector, modals, toolbar (Bootstrap-styled) | `@signalsafe/tree-spec-editor` |
31
- | Material-styled React shell (planned) | `@signalsafe/tree-spec-editor-react-mui` |
32
- | Angular shell + canvas (planned) | `@signalsafe/tree-spec-editor-angular` |
33
- | Vue shell + canvas (planned) | `@signalsafe/tree-spec-editor-vue` |
29
+ ### React Flow CSS (required)
34
30
 
35
- ## Maintainer notes
31
+ This package imports `reactflow/dist/style.css` from source. Bundlers treat it as a side effect (`sideEffects: ["**/*.css"]` in `package.json`).
36
32
 
37
- - **This package stays React + reactflow only.** Angular/Vue hosts will use `-core` with their own canvas packages, not `-react`.
38
- - **Material (React)** hosts should add `-react-mui` (planned), reusing this canvas unchanged.
39
- - Layer boundaries and refactor plan: [docs/ai/packages-editor-architecture.md](../../docs/ai/packages-editor-architecture.md).
33
+ Ensure your app loads React Flow styles, for example:
40
34
 
41
- ## Install
35
+ ```ts
36
+ import "reactflow/dist/style.css";
37
+ ```
38
+
39
+ If you use `@signalsafe/tree-spec-editor` (Bootstrap shell), the canvas still comes from this package — consumers may need the CSS import in the app entry when tree-shaking.
40
+
41
+ ## Quick start
42
+
43
+ ```tsx
44
+ import { useState } from "react";
45
+ import TreeSpecGraphEditor from "@signalsafe/tree-spec-editor-react";
46
+ import {
47
+ END_NODE_ID,
48
+ type EditorTree,
49
+ } from "@signalsafe/tree-spec-editor-core";
50
+
51
+ const initialTree: EditorTree = {
52
+ start_node: "start",
53
+ nodes: {
54
+ start: {
55
+ id: "start",
56
+ type: "prompt",
57
+ prompt: "Example prompt",
58
+ choices: [{ id: "done", label: "Finish" }],
59
+ position: { x: 40, y: 120 },
60
+ },
61
+ },
62
+ transitions: [
63
+ {
64
+ id: "t1",
65
+ fromNodeId: "start",
66
+ fromChoiceId: "done",
67
+ toNodeId: END_NODE_ID,
68
+ outcome: "safe",
69
+ },
70
+ ],
71
+ };
72
+
73
+ export function ExampleCanvas() {
74
+ const [tree, setTree] = useState(initialTree);
75
+ return (
76
+ <TreeSpecGraphEditor
77
+ tree={tree}
78
+ onChange={setTree}
79
+ className="h-[60vh] border rounded"
80
+ />
81
+ );
82
+ }
83
+ ```
84
+
85
+ For full authoring flows (load/save/validate), compose **`useTreeSpecEditor`** with your adapter — see tests and `@signalsafe/tree-spec-editor` for a reference shell.
86
+
87
+ ## Public exports
88
+
89
+ | Export | Purpose |
90
+ |---|---|
91
+ | `default` / `TreeSpecGraphEditor` | React Flow canvas |
92
+ | `TreeSpecGraphEditorProps` | Canvas props |
93
+ | `useTreeSpecEditor` | Stateful editor orchestration |
94
+ | `TreeSpecEditorAdapter`, `UseTreeSpecEditorResult`, … | Adapter and hook types |
95
+
96
+ Import from `@signalsafe/tree-spec-editor-react` only (no subpath exports).
97
+
98
+ ## Package boundaries
99
+
100
+ | Layer | Package |
101
+ |---|---|
102
+ | Wire | `@signalsafe/tree-spec` |
103
+ | Editor model | `@signalsafe/tree-spec-editor-core` |
104
+ | **React canvas (this package)** | `@signalsafe/tree-spec-editor-react` |
105
+ | Bootstrap shell | `@signalsafe/tree-spec-editor` |
106
+
107
+ ## Canvas selection behavior
108
+
109
+ | Selection | Inspector context | Contextual zoom (default on) |
110
+ |---|---|---|
111
+ | Node | selected node | fits node in viewport |
112
+ | Edge | source node + focused choice | viewport unchanged |
113
+ | Choice | `focusChoiceId` set | fits parent node when selected |
114
+
115
+ Pass `contextualZoom={false}` to disable automatic viewport fitting.
116
+
117
+ ## Development
118
+
119
+ `yarn build` uses `tsconfig.build.json` and resolves `@signalsafe/*` from `node_modules`. Ecosystem sibling `paths` in `tsconfig.json` apply to local typecheck/tests only.
42
120
 
43
121
  ```bash
44
- npm install @signalsafe/tree-spec-editor-react react react-dom reactflow
122
+ yarn install
123
+ yarn build
124
+ yarn test
125
+ yarn typecheck
45
126
  ```
46
127
 
47
- `reactflow` is a peer dependency; you must install it (and ship its CSS,
48
- e.g. `import 'reactflow/dist/style.css';`) in the consuming app. The
49
- package itself imports the CSS file from its source, so bundlers that
50
- resolve module references will pick it up automatically.
128
+ ## Security
51
129
 
52
- ## Why a separate package?
130
+ See [SECURITY.md](./SECURITY.md). Host applications must authenticate users, authorize edits, and validate TreeSpec JSON server-side before publish.
53
131
 
54
- This layer is React-specific but **UI-library-agnostic**. Hosts that want
55
- to ship a Material-styled editor only need to publish their own UI shell
56
- (panels, modals, toolbar) — they reuse the canvas and the editor model
57
- unchanged. This also keeps `@signalsafe/tree-spec-editor` (the
58
- Bootstrap variant) from being the sole React entry point.
132
+ ## Changelog and releases
59
133
 
60
- ## Repository
134
+ - [CHANGELOG.md](./CHANGELOG.md)
135
+ - [RELEASING.md](./RELEASING.md)
61
136
 
62
- Standalone source and releases: [SignalSafeSoftware/tree-spec-editor-react](https://github.com/SignalSafeSoftware/tree-spec-editor-react).
137
+ ## Related packages
63
138
 
64
- Published as [`@signalsafe/tree-spec-editor-react`](https://www.npmjs.com/package/@signalsafe/tree-spec-editor-react) on npm.
139
+ - [`@signalsafe/tree-spec-editor-core`](https://github.com/SignalSafeSoftware/tree-spec-editor-core) framework-agnostic editor helpers.
140
+ - [`@signalsafe/tree-spec-editor`](https://github.com/SignalSafeSoftware/tree-spec-editor) — full Bootstrap UI shell.
@@ -0,0 +1,14 @@
1
+ # Standalone export
2
+
3
+ This tree was generated by `scripts/export-standalone-npm-package.sh tree-spec-editor-react` from the DeliveryPlus monorepo.
4
+
5
+ ```bash
6
+ git init
7
+ git branch -M main
8
+ git add .
9
+ git commit -m "Sync tree-spec-editor-react from DeliveryPlus"
10
+ git remote add origin git@github.com:SignalSafeSoftware/tree-spec-editor-react.git
11
+ git push -u origin main
12
+ ```
13
+
14
+ See `RELEASING.md` for npm publish steps.
@@ -3,22 +3,22 @@ import { useCallback, useMemo, useRef, useState } from 'react';
3
3
  import ReactFlow, { Background, ConnectionMode, Controls, MiniMap, ReactFlowProvider, useReactFlow, } from 'reactflow';
4
4
  import 'reactflow/dist/style.css';
5
5
  import { GRAPH_SELECTION_KIND, choiceIdFromHandle, resolveGraphViewport, LAYOUT_SNAP_GRID, } from '@signalsafe/tree-spec-editor-core';
6
- import { GraphEditorCanvasContext, } from './GraphEditorCanvasContext';
7
- import { buildEdgeMarker, getIssueEdgeStyle, resolveSelectedEdgeStroke } from './canvas/edgeStyle';
8
- import { CANVAS_CLASS } from './canvas/constants';
9
- import { resolveCanvasFocusChoiceId } from './canvas/focusChoice';
10
- import { isChoiceRowClickTarget } from './canvas/typeGuards';
11
- import { GraphCanvasContextMenu } from './contextMenu/GraphCanvasContextMenu';
12
- import { EndNode } from './nodes/EndNode';
13
- import { PromptNode } from './nodes/PromptNode';
14
- import { joinClasses } from './utils/joinClasses';
15
- import { useCanvasContextMenu } from './hooks/useCanvasContextMenu';
16
- import { useCanvasGraphState } from './hooks/useCanvasGraphState';
17
- import { useCanvasIssueIndex } from './hooks/useCanvasIssueIndex';
18
- import { useCanvasNodeResize } from './hooks/useCanvasNodeResize';
19
- import { useCanvasViewport } from './hooks/useCanvasViewport';
20
- import { useChoiceDragDrop } from './hooks/useChoiceDragDrop';
21
- import { useGraphConnect } from './hooks/useGraphConnect';
6
+ import { GraphEditorCanvasContext, } from './GraphEditorCanvasContext.js';
7
+ import { buildEdgeMarker, getIssueEdgeStyle, resolveSelectedEdgeStroke } from './canvas/edgeStyle.js';
8
+ import { CANVAS_CLASS } from './canvas/constants.js';
9
+ import { resolveCanvasFocusChoiceId } from './canvas/focusChoice.js';
10
+ import { isChoiceRowClickTarget } from './canvas/typeGuards.js';
11
+ import { GraphCanvasContextMenu } from './contextMenu/GraphCanvasContextMenu.js';
12
+ import { EndNode } from './nodes/EndNode.js';
13
+ import { PromptNode } from './nodes/PromptNode.js';
14
+ import { joinClasses } from './utils/joinClasses.js';
15
+ import { useCanvasContextMenu } from './hooks/useCanvasContextMenu.js';
16
+ import { useCanvasGraphState } from './hooks/useCanvasGraphState.js';
17
+ import { useCanvasIssueIndex } from './hooks/useCanvasIssueIndex.js';
18
+ import { useCanvasNodeResize } from './hooks/useCanvasNodeResize.js';
19
+ import { useCanvasViewport } from './hooks/useCanvasViewport.js';
20
+ import { useChoiceDragDrop } from './hooks/useChoiceDragDrop.js';
21
+ import { useGraphConnect } from './hooks/useGraphConnect.js';
22
22
  function TreeSpecGraphInner({ tree, onChange, issues = [], selected, onSelect, onChoiceSelect, onRepositionChoice, focusNodeId, focusChoiceId = null, showMiniMap = true, fitViewNonce, className = 'h-70vh border rounded', readOnly = false, onDuplicateNode, onDeleteNode, onAutoLayout, contextualZoom = true, colorMode = 'light', }) {
23
23
  const rf = useReactFlow();
24
24
  const treeRef = useRef(tree);
@@ -1,7 +1,7 @@
1
1
  import { TERMINAL_OUTCOME } from '@signalsafe/tree-spec';
2
2
  import { END_NODE_ID, choiceIdFromHandle, resolveDefaultEdgeType, resolveEffectiveEdgeType, resolveEdgeStrokeColor, shouldShowEdgeLabel, } from '@signalsafe/tree-spec-editor-core';
3
- import { CHOICE_HANDLE_PREFIX, TARGET_HANDLE_ID, } from './constants';
4
- import { buildEdgeMarker, buildEdgeStyle, resolveEdgePathStroke } from './edgeStyle';
3
+ import { CHOICE_HANDLE_PREFIX, TARGET_HANDLE_ID, } from './constants.js';
4
+ import { buildEdgeMarker, buildEdgeStyle, resolveEdgePathStroke } from './edgeStyle.js';
5
5
  export function edgeLabelForTransition(tree, t) {
6
6
  const node = tree.nodes[t.fromNodeId];
7
7
  const choice = node?.choices?.find((c) => c.id === t.fromChoiceId);
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_CANVAS_EDGE_STROKE } from '@signalsafe/tree-spec-editor-core';
2
- import { BORDER_DANGER_CLASS, BORDER_WARNING_CLASS, EDGE_ARROW_MARKER, SELECTED_EDGE_STROKE } from './constants';
2
+ import { BORDER_DANGER_CLASS, BORDER_WARNING_CLASS, EDGE_ARROW_MARKER, SELECTED_EDGE_STROKE } from './constants.js';
3
3
  export function getPromptNodeBorderClass(hasErrors, warningCount) {
4
4
  if (hasErrors) {
5
5
  return BORDER_DANGER_CLASS;
@@ -1,4 +1,4 @@
1
- import { CHOICE_ROW_SELECTOR, REACT_FLOW_PANE_CLASS } from './constants';
1
+ import { CHOICE_ROW_SELECTOR, REACT_FLOW_PANE_CLASS } from './constants.js';
2
2
  export function isChoiceRowClickTarget(target) {
3
3
  if (target == null || typeof target !== 'object')
4
4
  return false;
@@ -1,4 +1,4 @@
1
- import type { CanvasContextMenuState } from './types';
1
+ import type { CanvasContextMenuState } from './types.js';
2
2
  export declare function GraphCanvasContextMenu({ menu, readOnly, onClose, onDuplicateNode, onDeleteNode, onAutoLayout, }: Readonly<{
3
3
  menu: CanvasContextMenuState | null;
4
4
  readOnly: boolean;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { END_NODE_ID } from '@signalsafe/tree-spec-editor-core';
3
- import { CONTEXT_MENU_CLASS } from '../canvas/constants';
4
- import { joinClasses } from '../utils/joinClasses';
3
+ import { CONTEXT_MENU_CLASS } from '../canvas/constants.js';
4
+ import { joinClasses } from '../utils/joinClasses.js';
5
5
  export function GraphCanvasContextMenu({ menu, readOnly, onClose, onDuplicateNode, onDeleteNode, onAutoLayout, }) {
6
6
  if (!menu || readOnly)
7
7
  return null;
@@ -1,6 +1,6 @@
1
1
  import { type MouseEvent } from 'react';
2
2
  import type { Node } from 'reactflow';
3
- import type { CanvasContextMenuState } from '../contextMenu/types';
3
+ import type { CanvasContextMenuState } from '../contextMenu/types.js';
4
4
  export type UseCanvasContextMenuOptions = {
5
5
  readOnly: boolean;
6
6
  onAutoLayout?: () => void;
@@ -1,8 +1,8 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from 'react';
2
2
  import { useEdgesState, useNodesState, } from 'reactflow';
3
3
  import { END_NODE_ID, getEditorHints, isNodeLocked, patchGraphEditorMeta, resolveCanvasNodeWidth, resolveEndNodePosition, snapPosition, } from '@signalsafe/tree-spec-editor-core';
4
- import { buildEdgesFromTransitions, buildTransitionsFromEdges } from '../canvas/edgeBuilders';
5
- import { NODE_DRAG_HANDLE_SELECTOR } from '../canvas/constants';
4
+ import { buildEdgesFromTransitions, buildTransitionsFromEdges } from '../canvas/edgeBuilders.js';
5
+ import { NODE_DRAG_HANDLE_SELECTOR } from '../canvas/constants.js';
6
6
  export function useCanvasGraphState(options) {
7
7
  const { tree, onChange, issues, issuesByNode, readOnly, resizeHeightByNodeId, isResizingRef, suppressViewportSaveRef, } = options;
8
8
  const isDraggingRef = useRef(false);
@@ -1,7 +1,7 @@
1
1
  import { type MutableRefObject, type RefObject } from 'react';
2
2
  import { type TreeSpecIssue, type TreeSpecWire } from '@signalsafe/tree-spec';
3
3
  import { type AutosaveStatus, type EditorTree, type TreeSpecAuditEventItem, type TreeSpecSnapshotItem } from '@signalsafe/tree-spec-editor-core';
4
- import type { GraphEditorVersionInfo, UseTreeSpecEditorActions, UseTreeSpecEditorOptions } from './types';
4
+ import type { GraphEditorVersionInfo, UseTreeSpecEditorActions, UseTreeSpecEditorOptions } from './types.js';
5
5
  export type UseEditorAdapterOptions = {
6
6
  options: UseTreeSpecEditorOptions;
7
7
  tree: EditorTree | null;
@@ -1,6 +1,6 @@
1
1
  import { type MutableRefObject, type RefObject } from 'react';
2
2
  import { type AutosaveStatus, type EditorTree } from '@signalsafe/tree-spec-editor-core';
3
- import type { UseTreeSpecEditorActions } from './types';
3
+ import type { UseTreeSpecEditorActions } from './types.js';
4
4
  export type UseEditorAutosaveOptions = {
5
5
  enableAutosave: boolean;
6
6
  autosaveDebounceMs: number;
@@ -1 +1 @@
1
- {"version":3,"file":"useEditorSelection.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorSelection.ts"],"names":[],"mappings":"AAEA,OAAO,EAIH,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,cAAc,EACtB,MAAM,mCAAmC,CAAC;AAE3C,MAAM,MAAM,wBAAwB,GAAG;IACnC,SAAS,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,YAAY,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACtC,aAAa,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,cAAc,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/C,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5C,gBAAgB,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9C,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,WAAW,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACvE,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,wBAAwB,CA6EpF"}
1
+ {"version":3,"file":"useEditorSelection.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorSelection.ts"],"names":[],"mappings":"AAEA,OAAO,EAIH,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,cAAc,EACtB,MAAM,mCAAmC,CAAC;AAE3C,MAAM,MAAM,wBAAwB,GAAG;IACnC,SAAS,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,YAAY,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACtC,aAAa,EAAE,UAAU,GAAG,IAAI,CAAC;IACjC,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,cAAc,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/C,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5C,gBAAgB,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9C,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,WAAW,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACvE,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,wBAAwB,CA4EpF"}
@@ -13,8 +13,7 @@ export function useEditorSelection(tree) {
13
13
  const applySelection = useCallback((next) => {
14
14
  setSelection(next);
15
15
  if (next.kind === GRAPH_SELECTION_KIND.EDGE && next.id && tree) {
16
- const edge = tree.transitions.find((t) => t.id === next.id);
17
- if (!edge)
16
+ if (!tree.transitions.some((t) => t.id === next.id))
18
17
  return;
19
18
  }
20
19
  const { focusNodeId: nextFocusNodeId, focusChoiceId: nextFocusChoiceId } = resolveGraphSelectionFocus(next, tree);
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useRef } from 'react';
2
2
  import { useReactFlow } from 'reactflow';
3
3
  import { GRAPH_SELECTION_KIND, applyEditorConnect, applyEditorConnectOnDrop, applyEditorReconnect, isValidEditorConnection, } from '@signalsafe/tree-spec-editor-core';
4
- import { isReactFlowPaneTarget } from '../canvas/typeGuards';
4
+ import { isReactFlowPaneTarget } from '../canvas/typeGuards.js';
5
5
  export function useGraphConnect(options) {
6
6
  const { treeRef, onChange, onSelect, readOnly } = options;
7
7
  const rf = useReactFlow();
@@ -1,4 +1,4 @@
1
- import type { UseTreeSpecEditorOptions, UseTreeSpecEditorResult } from './types';
1
+ import type { UseTreeSpecEditorOptions, UseTreeSpecEditorResult } from './types.js';
2
2
  /**
3
3
  * Headless React hook that owns the full stateful behavior of the SignalSafe
4
4
  * TreeSpec graph editor — loading, autosave, validation, publish, snapshots,
@@ -1,11 +1,11 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { compileTreeSpec, decompileTreeSpec, TERMINAL_OUTCOME, TREE_SPEC_ISSUE_SEVERITY, } from '@signalsafe/tree-spec';
3
3
  import { applyTreeTemplate, autoLayoutTree, AUTOSAVE_STATUS, deleteNode, deleteTransitionsForChoice, END_NODE_ID, getNextSpawnPosition, getTransition, GRAPH_SELECTION_KIND, moveChoiceInTree, moveNodeChoice, patchChoiceEdgeHints, patchGraphEditorMeta, renameNodeChoiceId, safeUUID, upsertTransition, } from '@signalsafe/tree-spec-editor-core';
4
- import { useEditorAdapter } from './useEditorAdapter';
5
- import { useEditorAutosave } from './useEditorAutosave';
6
- import { useEditorHistory } from './useEditorHistory';
7
- import { useEditorSelection } from './useEditorSelection';
8
- import { dispatchEditorKeyboardShortcut, resolveEditorKeyboardShortcutAction } from './keyboardShortcutDispatch';
4
+ import { useEditorAdapter } from './useEditorAdapter.js';
5
+ import { useEditorAutosave } from './useEditorAutosave.js';
6
+ import { useEditorHistory } from './useEditorHistory.js';
7
+ import { useEditorSelection } from './useEditorSelection.js';
8
+ import { dispatchEditorKeyboardShortcut, resolveEditorKeyboardShortcutAction } from './keyboardShortcutDispatch.js';
9
9
  const DEFAULT_AUTOSAVE_DEBOUNCE_MS = 2500;
10
10
  function isTextFieldTarget(target) {
11
11
  const tag = target instanceof HTMLElement ? target.tagName.toLowerCase() : '';
package/dist/index.d.ts CHANGED
@@ -10,8 +10,8 @@
10
10
  * UI shells layer on top of this package; routing is host-injected via hook
11
11
  * callbacks.
12
12
  */
13
- export { default } from './TreeSpecGraphEditor';
14
- export type { TreeSpecGraphEditorProps } from './TreeSpecGraphEditor';
15
- export { useTreeSpecEditor } from './hooks/useTreeSpecEditor';
16
- export type { AdapterValidationIssue, GraphEditorVersionInfo, TreeSpecEditorAdapter, UseTreeSpecEditorActions, UseTreeSpecEditorOptions, UseTreeSpecEditorResult, UseTreeSpecEditorState, } from './hooks/types';
13
+ export { default } from './TreeSpecGraphEditor.js';
14
+ export type { TreeSpecGraphEditorProps } from './TreeSpecGraphEditor.js';
15
+ export { useTreeSpecEditor } from './hooks/useTreeSpecEditor.js';
16
+ export type { AdapterValidationIssue, GraphEditorVersionInfo, TreeSpecEditorAdapter, UseTreeSpecEditorActions, UseTreeSpecEditorOptions, UseTreeSpecEditorResult, UseTreeSpecEditorState, } from './hooks/types.js';
17
17
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -10,5 +10,5 @@
10
10
  * UI shells layer on top of this package; routing is host-injected via hook
11
11
  * callbacks.
12
12
  */
13
- export { default } from './TreeSpecGraphEditor';
14
- export { useTreeSpecEditor } from './hooks/useTreeSpecEditor';
13
+ export { default } from './TreeSpecGraphEditor.js';
14
+ export { useTreeSpecEditor } from './hooks/useTreeSpecEditor.js';
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Handle, Position } from 'reactflow';
3
- import { CANVAS_NODE_SELECTED_CLASS, CANVAS_NODE_SELECTED_TEXT_CLASS, CANVAS_CHOICE_SELECTED_CLASS, CHOICE_DRAG_HANDLE_CLASS, CHOICE_DRAG_HANDLE_SELECTOR, CHOICE_DROP_TARGET_CLASS, CHOICE_HANDLE_CLASS, CHOICE_ROW_CLASS, CHOICE_ROW_SELECT_CLASS, CHOICE_ROW_SELECTABLE_CLASS, CHOICE_HANDLE_PREFIX, } from '../canvas/constants';
4
- import { useGraphEditorCanvas } from '../GraphEditorCanvasContext';
5
- import { joinClasses } from '../utils/joinClasses';
3
+ import { CANVAS_NODE_SELECTED_CLASS, CANVAS_NODE_SELECTED_TEXT_CLASS, CANVAS_CHOICE_SELECTED_CLASS, CHOICE_DRAG_HANDLE_CLASS, CHOICE_DRAG_HANDLE_SELECTOR, CHOICE_DROP_TARGET_CLASS, CHOICE_HANDLE_CLASS, CHOICE_ROW_CLASS, CHOICE_ROW_SELECT_CLASS, CHOICE_ROW_SELECTABLE_CLASS, CHOICE_HANDLE_PREFIX, } from '../canvas/constants.js';
4
+ import { useGraphEditorCanvas } from '../GraphEditorCanvasContext.js';
5
+ import { joinClasses } from '../utils/joinClasses.js';
6
6
  export function ChoiceCanvasRow({ nodeId, choice, choiceIndex, focusChoiceId, choiceTextClass, readOnly, }) {
7
7
  const { choiceDrag, choiceDropTarget, onSelectChoice, onChoiceDragStart, onChoiceDragEnd, onChoiceDragOver, onChoiceDrop, } = useGraphEditorCanvas();
8
8
  const isFocused = focusChoiceId === choice.id;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Handle, Position } from 'reactflow';
3
- import { BORDER_DANGER_CLASS, CANVAS_NODE_CLASS, CANVAS_NODE_SELECTED_CLASS, CANVAS_NODE_SELECTED_TEXT_CLASS, END_NODE_WIDTH_CLASS, TARGET_HANDLE_CLASS_DANGER, TARGET_HANDLE_ID, } from '../canvas/constants';
4
- import { joinClasses } from '../utils/joinClasses';
3
+ import { BORDER_DANGER_CLASS, CANVAS_NODE_CLASS, CANVAS_NODE_SELECTED_CLASS, CANVAS_NODE_SELECTED_TEXT_CLASS, END_NODE_WIDTH_CLASS, TARGET_HANDLE_CLASS_DANGER, TARGET_HANDLE_ID, } from '../canvas/constants.js';
4
+ import { joinClasses } from '../utils/joinClasses.js';
5
5
  export function EndNode({ selected }) {
6
6
  return (_jsxs("div", { className: joinClasses('card', 'rounded', CANVAS_NODE_CLASS, BORDER_DANGER_CLASS, END_NODE_WIDTH_CLASS, selected && CANVAS_NODE_SELECTED_CLASS, selected && CANVAS_NODE_SELECTED_TEXT_CLASS), children: [_jsx(Handle, { type: "target", position: Position.Left, id: TARGET_HANDLE_ID, className: TARGET_HANDLE_CLASS_DANGER }), _jsxs("div", { className: "card-body p-2 text-center", children: [_jsx("div", { className: "fw-bold text-danger", children: "END" }), _jsx("div", { className: "text-muted font-size-12", children: "Outcome required" })] })] }));
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import { type NodeProps } from 'reactflow';
2
- import type { PromptNodeData } from './types';
2
+ import type { PromptNodeData } from './types.js';
3
3
  type PromptNodeProps = Readonly<NodeProps<PromptNodeData>>;
4
4
  export declare function PromptNode({ data, selected, id }: PromptNodeProps): import("react").JSX.Element;
5
5
  export {};
@@ -2,13 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useCallback, useEffect } from 'react';
3
3
  import { Handle, NodeResizer, Position, useUpdateNodeInternals } from 'reactflow';
4
4
  import { editorHintsToStyle, getEditorHints, nodeTextWrapClassName, resolveNodeTextWrap, } from '@signalsafe/tree-spec-editor-core';
5
- import { CANVAS_NODE_BODY_CLASS, CANVAS_NODE_CLASS, CANVAS_NODE_SELECTED_CLASS, CANVAS_NODE_SELECTED_TEXT_CLASS, MIN_NODE_HEIGHT, MAX_NODE_HEIGHT, MIN_NODE_WIDTH, MAX_NODE_WIDTH, TARGET_HANDLE_CLASS_DEFAULT, TARGET_HANDLE_ID, } from '../canvas/constants';
6
- import { getPromptNodeBorderClass } from '../canvas/edgeStyle';
7
- import { useGraphEditorCanvas } from '../GraphEditorCanvasContext';
8
- import { joinClasses } from '../utils/joinClasses';
9
- import { PromptNodeChoicesList } from './PromptNodeChoicesList';
10
- import { PromptNodeHeader } from './PromptNodeHeader';
11
- import { PromptNodeToolbar } from './PromptNodeToolbar';
5
+ import { CANVAS_NODE_BODY_CLASS, CANVAS_NODE_CLASS, CANVAS_NODE_SELECTED_CLASS, CANVAS_NODE_SELECTED_TEXT_CLASS, MIN_NODE_HEIGHT, MAX_NODE_HEIGHT, MIN_NODE_WIDTH, MAX_NODE_WIDTH, TARGET_HANDLE_CLASS_DEFAULT, TARGET_HANDLE_ID, } from '../canvas/constants.js';
6
+ import { getPromptNodeBorderClass } from '../canvas/edgeStyle.js';
7
+ import { useGraphEditorCanvas } from '../GraphEditorCanvasContext.js';
8
+ import { joinClasses } from '../utils/joinClasses.js';
9
+ import { PromptNodeChoicesList } from './PromptNodeChoicesList.js';
10
+ import { PromptNodeHeader } from './PromptNodeHeader.js';
11
+ import { PromptNodeToolbar } from './PromptNodeToolbar.js';
12
12
  export function PromptNode({ data, selected, id }) {
13
13
  const n = data.node;
14
14
  const choices = n.choices ?? [];
@@ -1,5 +1,5 @@
1
1
  import { type EditorChoice } from '@signalsafe/tree-spec-editor-core';
2
- import { type GraphEditorCanvasContextValue } from '../GraphEditorCanvasContext';
2
+ import { type GraphEditorCanvasContextValue } from '../GraphEditorCanvasContext.js';
3
3
  export declare function PromptNodeChoicesList({ nodeId, choices, focusChoiceId, choiceTextClass, readOnly, choiceDrag, choiceDropTarget, onChoiceDragOver, onChoiceDrop, }: Readonly<{
4
4
  nodeId: string;
5
5
  choices: EditorChoice[];
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { CHOICE_DROP_APPEND_CLASS, CHOICE_DROP_TARGET_CLASS, } from '../canvas/constants';
3
- import { joinClasses } from '../utils/joinClasses';
4
- import { ChoiceCanvasRow } from './ChoiceCanvasRow';
2
+ import { CHOICE_DROP_APPEND_CLASS, CHOICE_DROP_TARGET_CLASS, } from '../canvas/constants.js';
3
+ import { joinClasses } from '../utils/joinClasses.js';
4
+ import { ChoiceCanvasRow } from './ChoiceCanvasRow.js';
5
5
  export function PromptNodeChoicesList({ nodeId, choices, focusChoiceId, choiceTextClass, readOnly, choiceDrag, choiceDropTarget, onChoiceDragOver, onChoiceDrop, }) {
6
6
  const handleListDragOver = (event) => {
7
7
  if (readOnly || !choiceDrag)
@@ -1,5 +1,5 @@
1
1
  import { getEditorHints, type EditorNode } from '@signalsafe/tree-spec-editor-core';
2
- import type { PromptNodeData } from './types';
2
+ import type { PromptNodeData } from './types.js';
3
3
  export declare function PromptNodeHeader({ node, data, editor, locked, readOnly, }: Readonly<{
4
4
  node: EditorNode;
5
5
  data: PromptNodeData;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { NODE_DRAG_HANDLE_CLASS } from '../canvas/constants';
3
- import { PromptNodeIssueBadges } from './PromptNodeIssueBadges';
2
+ import { NODE_DRAG_HANDLE_CLASS } from '../canvas/constants.js';
3
+ import { PromptNodeIssueBadges } from './PromptNodeIssueBadges.js';
4
4
  export function PromptNodeHeader({ node, data, editor, locked, readOnly, }) {
5
5
  return (_jsx("div", { className: "card-header bg-body-secondary py-2 px-2 min-w-0 flex-shrink-0", children: _jsxs("div", { className: "d-flex justify-content-between align-items-start gap-2 min-w-0", children: [_jsxs("div", { className: "d-flex align-items-start gap-1 min-w-0 flex-grow-1 overflow-hidden", children: [locked || readOnly ? null : (_jsx("span", { className: `${NODE_DRAG_HANDLE_CLASS} flex-shrink-0`, title: "Drag node", "aria-label": "Drag node", children: _jsx("i", { className: "bi bi-grip-vertical", "aria-hidden": true }) })), _jsx("div", { className: "min-w-0 flex-grow-1 overflow-hidden", children: _jsxs("div", { className: "fw-bold font-size-13", children: [data.isStart ? '▶ ' : '', node.type, editor.locked ? (_jsx("i", { className: "bi bi-lock-fill ms-1 text-secondary", title: "Locked", "aria-hidden": true })) : null, _jsx(PromptNodeIssueBadges, { issuesTotal: data.issuesTotal, issuesErrors: data.issuesErrors, issuesWarnings: data.issuesWarnings, issuesInfo: data.issuesInfo })] }) })] }), _jsx("div", { className: "text-muted font-size-11 flex-shrink-0", children: node.id.slice(0, 8) })] }) }));
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalsafe/tree-spec-editor-react",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Headless React canvas for the SignalSafe TreeSpec graph editor (React Flow shell, no UI library).",
6
6
  "license": "MIT",
@@ -23,7 +23,9 @@
23
23
  "typescript",
24
24
  "signalsafe"
25
25
  ],
26
- "sideEffects": false,
26
+ "sideEffects": [
27
+ "**/*.css"
28
+ ],
27
29
  "main": "./dist/index.js",
28
30
  "types": "./dist/index.d.ts",
29
31
  "exports": {
@@ -41,6 +43,7 @@
41
43
  "engines": {
42
44
  "node": ">=18"
43
45
  },
46
+ "packageManager": "yarn@1.22.22",
44
47
  "publishConfig": {
45
48
  "access": "public"
46
49
  },
@@ -48,14 +51,15 @@
48
51
  "build": "tsc -p tsconfig.build.json",
49
52
  "typecheck": "tsc --noEmit",
50
53
  "pack:local": "npm pack --pack-destination ../../dist/reusable-npm",
54
+ "smoke:package": "node scripts/smoke-package.mjs",
51
55
  "prepublishOnly": "npm run build",
52
56
  "prepare": "npm run build",
53
57
  "test": "vitest run",
54
58
  "test:monorepo": "cd ../../frontend && yarn vitest run --config vitest.tree-spec-editor-react.config.ts"
55
59
  },
56
60
  "dependencies": {
57
- "@signalsafe/tree-spec": "^0.3.1",
58
- "@signalsafe/tree-spec-editor-core": "^0.1.2"
61
+ "@signalsafe/tree-spec": "^0.3.2",
62
+ "@signalsafe/tree-spec-editor-core": "^0.1.3"
59
63
  },
60
64
  "peerDependencies": {
61
65
  "react": "^18.0.0",
@@ -71,6 +75,7 @@
71
75
  "react-test-renderer": "^18.3.1",
72
76
  "reactflow": "^11.11.4",
73
77
  "typescript": "^5.4.2",
78
+ "@vitest/coverage-v8": "^3.2.4",
74
79
  "vitest": "^3.2.4"
75
80
  }
76
81
  }