@signalsafe/tree-spec-editor 0.1.2 → 0.2.0
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 +657 -2
- package/dist/index.d.ts +48 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -5
- package/dist/lib/PanelHeaderCollapseCarets.d.ts +10 -0
- package/dist/lib/PanelHeaderCollapseCarets.d.ts.map +1 -0
- package/dist/lib/PanelHeaderCollapseCarets.js +13 -0
- package/dist/lib/buildDefaultToolbarSpec.d.ts +157 -0
- package/dist/lib/buildDefaultToolbarSpec.d.ts.map +1 -0
- package/dist/lib/buildDefaultToolbarSpec.js +202 -0
- package/dist/lib/panelHelpers.d.ts +9 -0
- package/dist/lib/panelHelpers.d.ts.map +1 -0
- package/dist/lib/panelHelpers.js +22 -0
- package/dist/modals/AuditLogModal.d.ts +21 -0
- package/dist/modals/AuditLogModal.d.ts.map +1 -0
- package/dist/modals/AuditLogModal.js +23 -0
- package/dist/modals/DraftHistoryModal.d.ts +34 -0
- package/dist/modals/DraftHistoryModal.d.ts.map +1 -0
- package/dist/modals/DraftHistoryModal.js +27 -0
- package/dist/modals/PublishReviewModal.d.ts +37 -0
- package/dist/modals/PublishReviewModal.d.ts.map +1 -0
- package/dist/modals/PublishReviewModal.js +25 -0
- package/dist/panels/AdvancedJsonPanel.d.ts +27 -0
- package/dist/panels/AdvancedJsonPanel.d.ts.map +1 -0
- package/dist/panels/AdvancedJsonPanel.js +11 -0
- package/dist/panels/GraphEditorInfoPanel.d.ts +20 -0
- package/dist/panels/GraphEditorInfoPanel.d.ts.map +1 -0
- package/dist/panels/GraphEditorInfoPanel.js +25 -0
- package/dist/panels/InspectorPanel.d.ts +79 -0
- package/dist/panels/InspectorPanel.d.ts.map +1 -0
- package/dist/panels/InspectorPanel.js +63 -0
- package/dist/panels/IssuesPanel.d.ts +33 -0
- package/dist/panels/IssuesPanel.d.ts.map +1 -0
- package/dist/panels/IssuesPanel.js +46 -0
- package/dist/panels/NodesPanel.d.ts +28 -0
- package/dist/panels/NodesPanel.d.ts.map +1 -0
- package/dist/panels/NodesPanel.js +56 -0
- package/dist/panels/SelectedEdgePanel.d.ts +18 -0
- package/dist/panels/SelectedEdgePanel.d.ts.map +1 -0
- package/dist/panels/SelectedEdgePanel.js +9 -0
- package/dist/panels/ToolbarPanel.d.ts +90 -0
- package/dist/panels/ToolbarPanel.d.ts.map +1 -0
- package/dist/panels/ToolbarPanel.js +59 -0
- package/package.json +7 -3
- package/dist/TreeSpecGraphEditor.d.ts +0 -16
- package/dist/TreeSpecGraphEditor.d.ts.map +0 -1
- package/dist/TreeSpecGraphEditor.js +0 -272
- package/dist/editorHelpers.d.ts +0 -16
- package/dist/editorHelpers.d.ts.map +0 -1
- package/dist/editorHelpers.js +0 -67
- package/dist/model.d.ts +0 -23
- package/dist/model.d.ts.map +0 -1
- package/dist/model.js +0 -1
- package/dist/treeLayout.d.ts +0 -11
- package/dist/treeLayout.d.ts.map +0 -1
- package/dist/treeLayout.js +0 -63
package/README.md
CHANGED
|
@@ -7,9 +7,40 @@ This package gives you a ready-to-embed editor component plus a small set of edi
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install @signalsafe/tree-spec-editor react react-dom reactflow
|
|
10
|
+
npm install @signalsafe/tree-spec-editor react react-dom reactflow react-bootstrap
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
The package ships React components that use `react-bootstrap` primitives (cards, buttons, form controls). The consuming app must install `react-bootstrap@^2.10.0` and load Bootstrap CSS in the usual way.
|
|
14
|
+
|
|
15
|
+
## Source layout (maintainers)
|
|
16
|
+
|
|
17
|
+
Published consumers import only the package root (`@signalsafe/tree-spec-editor`). This package is the **React + Bootstrap UI shell** in the layered editor stack:
|
|
18
|
+
|
|
19
|
+
| Layer | Package | Owns |
|
|
20
|
+
|-------|---------|------|
|
|
21
|
+
| Wire | [`@signalsafe/tree-spec`](../tree-spec/README.md) | TreeSpec wire types, constants, compile/lint. Pure TS. |
|
|
22
|
+
| Core | [`@signalsafe/tree-spec-editor-core`](../tree-spec-editor-core/README.md) | Editor model, tree operations, layout/autosave/keyboard helpers, constants. Pure TS. |
|
|
23
|
+
| React | [`@signalsafe/tree-spec-editor-react`](../tree-spec-editor-react/README.md) | React Flow canvas (`TreeSpecGraphEditor`). React-specific, UI-library-agnostic. |
|
|
24
|
+
| Shell | **`@signalsafe/tree-spec-editor`** (this package) | React + Bootstrap panels, modals, toolbar, Bootstrap badge helper. Re-exports the canvas from `-react` and the framework-agnostic surface from `-core` for one-stop import. |
|
|
25
|
+
|
|
26
|
+
| Directory | Contents |
|
|
27
|
+
|-----------|----------|
|
|
28
|
+
| `src/panels/` | Sidebar, toolbar, inspector, and JSON chrome components (React + `react-bootstrap`) |
|
|
29
|
+
| `src/modals/` | Publish review, draft history, and audit log dialogs (React + `react-bootstrap`) |
|
|
30
|
+
| `src/lib/panelHelpers.ts` | `getIssueSeverityBadgeClass` (Bootstrap-flavored — Material / Angular shells ship their own equivalent) |
|
|
31
|
+
| `src/index.ts` | Public barrel — **do not** import from subpaths in apps. Re-exports the canvas from `@signalsafe/tree-spec-editor-react` and the framework-agnostic surface from `@signalsafe/tree-spec-editor-core` |
|
|
32
|
+
|
|
33
|
+
Tests live under `tests/panels/`, `tests/modals/`, plus `tests/index.test.ts` for the barrel. The canvas test lives in **`packages/tree-spec-editor-react/tests/`** and the framework-agnostic helper tests live in **`packages/tree-spec-editor-core/tests/`**.
|
|
34
|
+
|
|
35
|
+
## Choosing the right package
|
|
36
|
+
|
|
37
|
+
| You want… | Import from |
|
|
38
|
+
|----------|-------------|
|
|
39
|
+
| The full React + Bootstrap editor (canvas + panels + modals + toolbar) | `@signalsafe/tree-spec-editor` (this package) |
|
|
40
|
+
| Just the canvas, with your own UI library (Material / Tailwind / custom) | `@signalsafe/tree-spec-editor-react` |
|
|
41
|
+
| Just the framework-agnostic helpers (CLI, server-side preview, Angular shell) | `@signalsafe/tree-spec-editor-core` |
|
|
42
|
+
| Just the wire contract (validation, compile, lint) | `@signalsafe/tree-spec` |
|
|
43
|
+
|
|
13
44
|
## Happy Path
|
|
14
45
|
|
|
15
46
|
The smallest useful integration is:
|
|
@@ -90,10 +121,22 @@ export function ExampleTreeSpecEditor(): JSX.Element {
|
|
|
90
121
|
## What You Get
|
|
91
122
|
|
|
92
123
|
- `TreeSpecGraphEditor`: the default React editor component.
|
|
124
|
+
- `IssuesPanel`, `NodesPanel`: presentational sidebar panels for issues and node search/selection.
|
|
125
|
+
- `AdvancedJsonPanel`: presentational card chrome for displaying a host-owned JSON renderer (e.g. the compiled `tree_spec`).
|
|
126
|
+
- `InspectorPanel`: right-rail node inspector (Type / Prompt / Choices with Label / Next / Outcome / Delete) with optional render-prop slots and overridable outcome options for project-specific extensions.
|
|
127
|
+
- `SelectedEdgePanel`: compact read-only card showing the currently selected transition.
|
|
128
|
+
- `PublishReviewModal`, `DraftHistoryModal`, `AuditLogModal`: presentational modals for the publish-review / draft-snapshot / audit-log authoring flows; all strings are overridable.
|
|
129
|
+
- `ToolbarPanel`: data-driven toolbar that takes an `items: ToolbarItem[]` array (`button` / `dropdown` / `badge` / `text` / `custom` kinds) and renders Bootstrap chrome in a single flex wrapper.
|
|
130
|
+
- `duplicateNode`, `deleteNode`, `computeTreeDiffSummary`, `applyTreeTemplate`: project-agnostic tree-operation helpers (clone a node, delete a node with cascade, summarize a structural diff, insert a reusable subgraph template).
|
|
131
|
+
- `getAutosaveStatusLabel`, `getKeyboardShortcutAction`, `shouldQueueInitialValidation`: editor-lifecycle helpers (default autosave label, default keyboard-shortcut mapper, "should we validate on load?" tri-state policy).
|
|
132
|
+
- `AUTOSAVE_STATUS`, `KEYBOARD_SHORTCUT_ACTION`, `GRAPH_SELECTION_KIND`, `TOOLBAR_ITEM_KIND`: runtime constant objects for the matching string-union types. Use these instead of bare string literals (`AUTOSAVE_STATUS.SAVING`, `KEYBOARD_SHORTCUT_ACTION.SAVE`, `GRAPH_SELECTION_KIND.NODE`, `TOOLBAR_ITEM_KIND.BUTTON`). The types (`AutosaveStatus`, `KeyboardShortcutAction`, `GraphSelectionKind`, `ToolbarItemKind`) are derived from these constants.
|
|
133
|
+
- `DEFAULT_OUTCOME_OPTIONS`: the canonical TerminalOutcome list used by `InspectorPanel`'s outcome `<select>` when no override is supplied.
|
|
134
|
+
- `TreeSpecSnapshotItem`, `TreeSpecAuditEventItem`: canonical data shapes consumed by the draft-history and audit-log modals.
|
|
93
135
|
- `EditorTree`, `EditorNode`, `EditorTransition`, `EditorChoice`: editor model types.
|
|
94
136
|
- `GraphSelection` and `GraphEditorIssue`: optional selection and issue-display types.
|
|
95
137
|
- `autoLayoutTree` and `getNextSpawnPosition`: layout helpers for placing nodes.
|
|
96
138
|
- `lintEditorTree`, `getTransition`, `upsertTransition`, `deleteTransitionsForChoice`: editor helpers for validation and transition updates.
|
|
139
|
+
- `buildStableEntries`, `getIssueSeverityBadgeClass`: small helpers reused by the sidebar panels and available for hosts that compose their own UI.
|
|
97
140
|
|
|
98
141
|
## Feature Examples
|
|
99
142
|
|
|
@@ -257,6 +300,618 @@ export function EditorWithFitView({ initialTree }: Props): JSX.Element {
|
|
|
257
300
|
}
|
|
258
301
|
```
|
|
259
302
|
|
|
303
|
+
## Sidebar Panels
|
|
304
|
+
|
|
305
|
+
The package also ships two presentational components for the editor's left rail. They are purely view-only: the host owns all state (issues, selection, search query, minimap toggle) and passes callbacks back in.
|
|
306
|
+
|
|
307
|
+
### `IssuesPanel`
|
|
308
|
+
|
|
309
|
+
Renders a Bootstrap **card** matching **`NodesPanel`** chrome (`card-header` + `bg-body-secondary`): the body is **`card-body p-0`** with **`list-group list-group-flush`**. Each issue row mirrors the **Nodes** header rhythm: **truncated `node id, type`** on the left (type resolved from the optional **`tree`** prop), **severity** as a **`badge`** on the right, then the **message** (monospace) and optional **choice id** line. The empty state is a single flush list item. Clicking a row invokes `onSelectIssue`.
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
import { useMemo, useState, type JSX } from 'react';
|
|
313
|
+
import TreeSpecGraphEditor, {
|
|
314
|
+
IssuesPanel,
|
|
315
|
+
lintEditorTree,
|
|
316
|
+
type EditorTree,
|
|
317
|
+
type TreeSpecIssue,
|
|
318
|
+
} from '@signalsafe/tree-spec-editor';
|
|
319
|
+
|
|
320
|
+
type Props = { initialTree: EditorTree };
|
|
321
|
+
|
|
322
|
+
export function EditorWithIssuesPanel({ initialTree }: Props): JSX.Element {
|
|
323
|
+
const [tree, setTree] = useState<EditorTree>(initialTree);
|
|
324
|
+
const issues = useMemo<TreeSpecIssue[]>(() => lintEditorTree(tree), [tree]);
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div className="row g-3">
|
|
328
|
+
<div className="col-3">
|
|
329
|
+
<IssuesPanel
|
|
330
|
+
issues={issues}
|
|
331
|
+
tree={tree}
|
|
332
|
+
lastValidatedAt={null}
|
|
333
|
+
onSelectIssue={(issue) => console.log('focus', issue)}
|
|
334
|
+
/>
|
|
335
|
+
</div>
|
|
336
|
+
<div className="col-9">
|
|
337
|
+
<TreeSpecGraphEditor tree={tree} onChange={setTree} issues={issues} />
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`IssuesPanel` accepts optional overrides:
|
|
345
|
+
|
|
346
|
+
- `tree`: when passed, each issue row resolves **node type** from `tree.nodes[issue.node_id]` for the header (same **id, type** pattern as **NodesPanel**).
|
|
347
|
+
- `title`: card header text (default `"Issues"`).
|
|
348
|
+
- `formatTimestamp(iso)`: override how `lastValidatedAt` is displayed in the header (default `new Date(iso).toLocaleString()`).
|
|
349
|
+
- `showValidationSummaryInHeader`: when `false`, the validated / not validated line is omitted from the header (default `true`).
|
|
350
|
+
|
|
351
|
+
### `NodesPanel`
|
|
352
|
+
|
|
353
|
+
Renders a search input plus the filtered node list, with a minimap toggle. Each row header is one line: **node id**, a comma, **type** (truncated on narrow widths), with an optional **delete** icon on the right when `onDeleteNode` is set; below that are the prompt preview and choice count. Rows are keyboard-activatable (`role="button"`). Use this when you want consistent left-rail navigation alongside the canvas.
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
import { useState, type JSX } from 'react';
|
|
357
|
+
import TreeSpecGraphEditor, {
|
|
358
|
+
NodesPanel,
|
|
359
|
+
type EditorTree,
|
|
360
|
+
type GraphSelection,
|
|
361
|
+
} from '@signalsafe/tree-spec-editor';
|
|
362
|
+
|
|
363
|
+
type Props = { initialTree: EditorTree };
|
|
364
|
+
|
|
365
|
+
export function EditorWithNodesPanel({ initialTree }: Props): JSX.Element {
|
|
366
|
+
const [tree, setTree] = useState<EditorTree>(initialTree);
|
|
367
|
+
const [selection, setSelection] = useState<GraphSelection>({ kind: null, id: null });
|
|
368
|
+
const [nodeSearch, setNodeSearch] = useState('');
|
|
369
|
+
const [showMiniMap, setShowMiniMap] = useState(true);
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<div className="row g-3">
|
|
373
|
+
<div className="col-3">
|
|
374
|
+
<NodesPanel
|
|
375
|
+
tree={tree}
|
|
376
|
+
nodeSearch={nodeSearch}
|
|
377
|
+
selection={selection}
|
|
378
|
+
showMiniMap={showMiniMap}
|
|
379
|
+
onNodeSearchChange={setNodeSearch}
|
|
380
|
+
onNodeSelect={(nodeId) => setSelection({ kind: 'node', id: nodeId })}
|
|
381
|
+
onShowMiniMapChange={setShowMiniMap}
|
|
382
|
+
/>
|
|
383
|
+
</div>
|
|
384
|
+
<div className="col-9">
|
|
385
|
+
<TreeSpecGraphEditor
|
|
386
|
+
tree={tree}
|
|
387
|
+
onChange={setTree}
|
|
388
|
+
selected={selection}
|
|
389
|
+
onSelect={setSelection}
|
|
390
|
+
showMiniMap={showMiniMap}
|
|
391
|
+
/>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`NodesPanel` accepts optional overrides:
|
|
399
|
+
|
|
400
|
+
- `searchPlaceholder`: override the search input placeholder text.
|
|
401
|
+
- `tipText`: override the muted helper text below the node list.
|
|
402
|
+
- `onAddNode`: when set, shows an add-node icon in the card header (same pattern as grid action icons); invoke from `useTreeSpecEditor().actions.addNodeOfType`, etc.
|
|
403
|
+
- `onDeleteNode(nodeId)`: when set, each row shows a delete icon on the right; wire to `deleteNodeById` (or equivalent). Omit to hide per-row delete.
|
|
404
|
+
- `isPublished`: when true, per-row delete icons are disabled.
|
|
405
|
+
|
|
406
|
+
### `AdvancedJsonPanel`
|
|
407
|
+
|
|
408
|
+
Renders a card with a header (title + optional subtitle + optional Expand/Compress button group) and a body slot that hosts your own JSON renderer. The package intentionally does **not** ship a JSON editor — pass any component (e.g. an existing `JsonEditor`) as `children` so the package stays independent of CDN-loaded jsoneditor libraries.
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
import { useRef, type JSX } from 'react';
|
|
412
|
+
import { compileTreeSpec } from '@signalsafe/tree-spec';
|
|
413
|
+
import { AdvancedJsonPanel, type EditorTree } from '@signalsafe/tree-spec-editor';
|
|
414
|
+
import JsonEditor, { type JsonEditorHandle } from '@/components/JsonEditor';
|
|
415
|
+
|
|
416
|
+
type Props = { tree: EditorTree };
|
|
417
|
+
|
|
418
|
+
export function CompiledTreeSpecPanel({ tree }: Props): JSX.Element {
|
|
419
|
+
const ref = useRef<JsonEditorHandle>(null);
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<AdvancedJsonPanel
|
|
423
|
+
subtitle="This is the compiled tree_spec that will be saved/published."
|
|
424
|
+
onExpandAll={() => ref.current?.expandAll()}
|
|
425
|
+
onCollapseAll={() => ref.current?.collapseAll()}
|
|
426
|
+
>
|
|
427
|
+
<JsonEditor
|
|
428
|
+
ref={ref}
|
|
429
|
+
value={compileTreeSpec(tree) as Record<string, unknown>}
|
|
430
|
+
readOnly
|
|
431
|
+
height="280px"
|
|
432
|
+
/>
|
|
433
|
+
</AdvancedJsonPanel>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
`AdvancedJsonPanel` accepts these optional props:
|
|
439
|
+
|
|
440
|
+
- `title`: header title text (default `"Advanced JSON (read-only)"`).
|
|
441
|
+
- `subtitle`: muted line below the title; the line is hidden when omitted.
|
|
442
|
+
- `onExpandAll`, `onCollapseAll`: when provided, the matching button renders in the header. Omit both to hide the button group entirely.
|
|
443
|
+
- `expandAllLabel`, `collapseAllLabel`: override the button labels (defaults `"Expand all"` and `"Compress all"`).
|
|
444
|
+
- `className`: extra classes applied to the outer `card mt-3` element.
|
|
445
|
+
|
|
446
|
+
### `InspectorPanel`
|
|
447
|
+
|
|
448
|
+
The right-rail node inspector. When a node is selected, it renders two Bootstrap cards: **Required** (Type, Prompt, and `renderExtraNodeFields`) and **Choices** (list-group rows). The universal fields the package owns:
|
|
449
|
+
|
|
450
|
+
- **Type** `<select>` (with the package's `TREE_SPEC_NODE_TYPE_PRESETS` + custom-value input).
|
|
451
|
+
- **Prompt** `<textarea>`.
|
|
452
|
+
- **Choices** list with **Label**, **Next** (target-node `<select>`), and a **delete** control per choice.
|
|
453
|
+
- **Outcome** `<select>` per choice — only shown when the choice targets `END_NODE_ID`. Default options are the canonical `TerminalOutcome` values from `@signalsafe/tree-spec` (`safe` / `at_risk` / `compromised`).
|
|
454
|
+
|
|
455
|
+
For project-specific UI, `InspectorPanel` exposes **render-prop slots** so the package stays generic while hosts can extend it without forking. The package owns nothing app-specific.
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
import {
|
|
459
|
+
InspectorPanel,
|
|
460
|
+
SelectedEdgePanel,
|
|
461
|
+
type EditorNode,
|
|
462
|
+
type EditorTree,
|
|
463
|
+
type EditorTransition,
|
|
464
|
+
} from '@signalsafe/tree-spec-editor';
|
|
465
|
+
|
|
466
|
+
function RightRail({
|
|
467
|
+
tree,
|
|
468
|
+
selectedNode,
|
|
469
|
+
selectedEdge,
|
|
470
|
+
focusChoiceId,
|
|
471
|
+
isPublished,
|
|
472
|
+
onUpdateSelectedNode,
|
|
473
|
+
onAddChoice,
|
|
474
|
+
onDeleteChoice,
|
|
475
|
+
onSetChoiceTarget,
|
|
476
|
+
onSetChoiceOutcome,
|
|
477
|
+
}: {
|
|
478
|
+
tree: EditorTree;
|
|
479
|
+
selectedNode: EditorNode | null;
|
|
480
|
+
selectedEdge: EditorTransition | null;
|
|
481
|
+
focusChoiceId: string | null;
|
|
482
|
+
isPublished: boolean;
|
|
483
|
+
onUpdateSelectedNode: (patch: Partial<EditorNode>) => void;
|
|
484
|
+
onAddChoice: () => void;
|
|
485
|
+
onDeleteChoice: (choiceId: string) => void;
|
|
486
|
+
onSetChoiceTarget: (choiceId: string, targetNodeId: string) => void;
|
|
487
|
+
onSetChoiceOutcome: (choiceId: string, outcome: string) => void;
|
|
488
|
+
}) {
|
|
489
|
+
return (
|
|
490
|
+
<>
|
|
491
|
+
<InspectorPanel
|
|
492
|
+
tree={tree}
|
|
493
|
+
selectedNode={selectedNode}
|
|
494
|
+
focusChoiceId={focusChoiceId}
|
|
495
|
+
isPublished={isPublished}
|
|
496
|
+
onUpdateSelectedNode={onUpdateSelectedNode}
|
|
497
|
+
onAddChoice={onAddChoice}
|
|
498
|
+
onDeleteChoice={onDeleteChoice}
|
|
499
|
+
onSetChoiceTarget={onSetChoiceTarget}
|
|
500
|
+
onSetChoiceOutcome={onSetChoiceOutcome}
|
|
501
|
+
renderExtraNodeFields={({ node, onUpdateNode }) => (
|
|
502
|
+
<YourProjectNodeMetadataFields node={node} onChange={onUpdateNode} />
|
|
503
|
+
)}
|
|
504
|
+
renderExtraChoiceFields={({ choice, onUpdateNode }) => (
|
|
505
|
+
<YourProjectChoiceMetadataFields choice={choice} onChange={onUpdateNode} />
|
|
506
|
+
)}
|
|
507
|
+
/>
|
|
508
|
+
<SelectedEdgePanel selectedEdge={selectedEdge} />
|
|
509
|
+
</>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
`InspectorPanel` accepts these optional extension points:
|
|
515
|
+
|
|
516
|
+
- `outcomeOptions`: override the END-transition outcome `<select>` options (e.g. `[{ value: 'pass', label: 'Pass' }, { value: 'fail', label: 'Fail' }]`). Defaults to the canonical `TerminalOutcome` values exported as `DEFAULT_OUTCOME_OPTIONS`.
|
|
517
|
+
- `hideOutcomeField`: hide the outcome `<select>` entirely (useful when the host manages outcomes via `renderExtraChoiceFields` or stores them outside the wire format).
|
|
518
|
+
- `renderExtraNodeFields({ tree, node, isPublished, onUpdateNode })`: render extra UI below the Prompt textarea, scoped to the currently-selected node. Use for project-specific node metadata (render hints, scoring, classification, etc.).
|
|
519
|
+
- `renderExtraChoiceFields({ tree, node, choice, transition, isPublished, onUpdateNode })`: render extra UI below each choice card (after Next/Outcome). Use for project-specific per-choice metadata.
|
|
520
|
+
- `onDeleteSelectedNode`: when set, shows a delete-node icon in the Required card header; wire to `useTreeSpecEditor().actions.deleteSelectedNode` (or equivalent). Disabled automatically when `isPublished` is true.
|
|
521
|
+
- `title`: Required card header text (default `"Required"`).
|
|
522
|
+
- `emptyStateText`: text shown when no node is selected (default `"Select a node to edit it."`).
|
|
523
|
+
- `typeHelperText`: muted helper text under the Type field (default explains the open `type` string).
|
|
524
|
+
|
|
525
|
+
### `SelectedEdgePanel`
|
|
526
|
+
|
|
527
|
+
The compact read-only card that surfaces the currently selected transition. Returns `null` when `selectedEdge` is `null`, so hosts can render it unconditionally.
|
|
528
|
+
|
|
529
|
+
`SelectedEdgePanel` accepts these optional props:
|
|
530
|
+
|
|
531
|
+
- `title`: header title text (default `"Selected Edge"`).
|
|
532
|
+
- `renderDetails(edge)`: replace the default `from · choice → to` line with a custom renderer.
|
|
533
|
+
|
|
534
|
+
## Modals
|
|
535
|
+
|
|
536
|
+
Three presentational modals cover the standard TreeSpec authoring flows. Each renders Bootstrap-styled chrome via a native `<dialog>` element, owns its list/empty/loading rendering, and exposes overridable `title` / `subtitle` / `emptyStateText` / button-label props so the package stays free of project-specific wording. Hosts wire the data, fetch on `show`, and provide callbacks.
|
|
537
|
+
|
|
538
|
+
### `PublishReviewModal`
|
|
539
|
+
|
|
540
|
+
A confirmation modal for the publish flow: shows a summary of what's about to be published (diff lines), a validation-error alert when any issue has `severity === 'error'`, and a Publish button that is disabled while `publishing` is `true` or any error issue exists. The package does not own the diff logic — pass `computeDiffSummary(baseline, current) => { lines, hasChanges }` so the host can compute it in whatever way it likes.
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
import {
|
|
544
|
+
PublishReviewModal,
|
|
545
|
+
type EditorTree,
|
|
546
|
+
type TreeSpecIssue,
|
|
547
|
+
} from '@signalsafe/tree-spec-editor';
|
|
548
|
+
|
|
549
|
+
function PublishModalContainer({
|
|
550
|
+
show,
|
|
551
|
+
tree,
|
|
552
|
+
baselineTree,
|
|
553
|
+
issues,
|
|
554
|
+
publishing,
|
|
555
|
+
onClose,
|
|
556
|
+
onPublish,
|
|
557
|
+
computeDiffSummary,
|
|
558
|
+
}: {
|
|
559
|
+
show: boolean;
|
|
560
|
+
tree: EditorTree | null;
|
|
561
|
+
baselineTree: EditorTree | null;
|
|
562
|
+
issues: TreeSpecIssue[];
|
|
563
|
+
publishing: boolean;
|
|
564
|
+
onClose: () => void;
|
|
565
|
+
onPublish: () => void;
|
|
566
|
+
computeDiffSummary: (
|
|
567
|
+
before: EditorTree | null,
|
|
568
|
+
after: EditorTree,
|
|
569
|
+
) => { lines: string[]; hasChanges: boolean };
|
|
570
|
+
}) {
|
|
571
|
+
return (
|
|
572
|
+
<PublishReviewModal
|
|
573
|
+
show={show}
|
|
574
|
+
tree={tree}
|
|
575
|
+
baselineTree={baselineTree}
|
|
576
|
+
issues={issues}
|
|
577
|
+
publishing={publishing}
|
|
578
|
+
onClose={onClose}
|
|
579
|
+
onPublish={onPublish}
|
|
580
|
+
computeDiffSummary={computeDiffSummary}
|
|
581
|
+
/>
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
`PublishReviewModal` accepts these optional props:
|
|
587
|
+
|
|
588
|
+
- `title`: modal title (default `"Review changes before publishing"`).
|
|
589
|
+
- `summaryText`: info alert text shown above the diff list.
|
|
590
|
+
- `errorAlertText`: red alert shown when `issues` contains any `severity === 'error'` item.
|
|
591
|
+
- `cancelLabel`, `publishLabel`, `publishingLabel`: button labels.
|
|
592
|
+
|
|
593
|
+
### `DraftHistoryModal`
|
|
594
|
+
|
|
595
|
+
A modal that lists draft snapshots and offers per-row "Restore" plus a top-right "Create snapshot". The host owns the `snapshots: TreeSpecSnapshotItem[]` array, the `creatingSnapshot` / `restoringSnapshotId` busy flags, and the `onCreateSnapshot` / `onRestoreSnapshot` callbacks. "Create snapshot" is disabled when `tree` is `null` (e.g. while loading) or `creatingSnapshot` is `true`.
|
|
596
|
+
|
|
597
|
+
```tsx
|
|
598
|
+
import {
|
|
599
|
+
DraftHistoryModal,
|
|
600
|
+
type EditorTree,
|
|
601
|
+
type TreeSpecSnapshotItem,
|
|
602
|
+
} from '@signalsafe/tree-spec-editor';
|
|
603
|
+
|
|
604
|
+
function DraftHistoryModalContainer({
|
|
605
|
+
show,
|
|
606
|
+
snapshots,
|
|
607
|
+
loadingSnapshots,
|
|
608
|
+
creatingSnapshot,
|
|
609
|
+
restoringSnapshotId,
|
|
610
|
+
tree,
|
|
611
|
+
onClose,
|
|
612
|
+
onCreateSnapshot,
|
|
613
|
+
onRestoreSnapshot,
|
|
614
|
+
}: {
|
|
615
|
+
show: boolean;
|
|
616
|
+
snapshots: TreeSpecSnapshotItem[];
|
|
617
|
+
loadingSnapshots: boolean;
|
|
618
|
+
creatingSnapshot: boolean;
|
|
619
|
+
restoringSnapshotId: string | null;
|
|
620
|
+
tree: EditorTree | null;
|
|
621
|
+
onClose: () => void;
|
|
622
|
+
onCreateSnapshot: () => void;
|
|
623
|
+
onRestoreSnapshot: (snapshotId: string) => void;
|
|
624
|
+
}) {
|
|
625
|
+
return (
|
|
626
|
+
<DraftHistoryModal
|
|
627
|
+
show={show}
|
|
628
|
+
snapshots={snapshots}
|
|
629
|
+
loadingSnapshots={loadingSnapshots}
|
|
630
|
+
creatingSnapshot={creatingSnapshot}
|
|
631
|
+
restoringSnapshotId={restoringSnapshotId}
|
|
632
|
+
tree={tree}
|
|
633
|
+
onClose={onClose}
|
|
634
|
+
onCreateSnapshot={onCreateSnapshot}
|
|
635
|
+
onRestoreSnapshot={onRestoreSnapshot}
|
|
636
|
+
/>
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
`DraftHistoryModal` accepts these optional props:
|
|
642
|
+
|
|
643
|
+
- `title`, `subtitle`, `listCaption`: header / instructions / list caption.
|
|
644
|
+
- `createSnapshotLabel`, `creatingSnapshotLabel`, `restoreLabel`, `restoringLabel`, `closeLabel`: button labels.
|
|
645
|
+
- `loadingText`, `emptyStateText`: state text overrides.
|
|
646
|
+
|
|
647
|
+
### `AuditLogModal`
|
|
648
|
+
|
|
649
|
+
A modal that lists authoring audit events for the current tree_spec version. Each row shows the `action` as a Bootstrap badge, the formatted `created_at` timestamp, the optional `actor` (numeric id or string), and the optional `detail` JSON. The host owns the `auditEvents: TreeSpecAuditEventItem[]` array and the `loadingAudit` flag.
|
|
650
|
+
|
|
651
|
+
```tsx
|
|
652
|
+
import { AuditLogModal, type TreeSpecAuditEventItem } from '@signalsafe/tree-spec-editor';
|
|
653
|
+
|
|
654
|
+
function AuditLogModalContainer({
|
|
655
|
+
show,
|
|
656
|
+
auditEvents,
|
|
657
|
+
loadingAudit,
|
|
658
|
+
onClose,
|
|
659
|
+
}: {
|
|
660
|
+
show: boolean;
|
|
661
|
+
auditEvents: TreeSpecAuditEventItem[];
|
|
662
|
+
loadingAudit: boolean;
|
|
663
|
+
onClose: () => void;
|
|
664
|
+
}) {
|
|
665
|
+
return (
|
|
666
|
+
<AuditLogModal
|
|
667
|
+
show={show}
|
|
668
|
+
auditEvents={auditEvents}
|
|
669
|
+
loadingAudit={loadingAudit}
|
|
670
|
+
onClose={onClose}
|
|
671
|
+
/>
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
`AuditLogModal` accepts these optional props:
|
|
677
|
+
|
|
678
|
+
- `title`, `subtitle`: header text.
|
|
679
|
+
- `loadingText`, `emptyStateText`: state text overrides.
|
|
680
|
+
- `actorLabel`: prefix shown before the actor id (default `"Actor ID:"`).
|
|
681
|
+
- `closeLabel`: footer Close button label.
|
|
682
|
+
|
|
683
|
+
## Toolbar
|
|
684
|
+
|
|
685
|
+
### `ToolbarPanel`
|
|
686
|
+
|
|
687
|
+
A data-driven toolbar for editor pages. Hosts pass an `items: ToolbarItem[]`
|
|
688
|
+
array; the package renders Bootstrap chrome (`Button`, `Dropdown`, badge,
|
|
689
|
+
muted text) inside a single flex wrapper. All project-specific concerns —
|
|
690
|
+
adapter capability gates, dropdown vocabularies, button labels, disabled
|
|
691
|
+
rules, and router-aware links — stay in the host as closures inside each
|
|
692
|
+
item.
|
|
693
|
+
|
|
694
|
+
```tsx
|
|
695
|
+
import {
|
|
696
|
+
ToolbarPanel,
|
|
697
|
+
type ToolbarItem,
|
|
698
|
+
} from '@signalsafe/tree-spec-editor';
|
|
699
|
+
import { Link } from 'react-router-dom';
|
|
700
|
+
|
|
701
|
+
const items: ToolbarItem[] = [
|
|
702
|
+
{
|
|
703
|
+
kind: 'custom',
|
|
704
|
+
id: 'back',
|
|
705
|
+
render: () => (
|
|
706
|
+
<Link className="btn btn-outline-secondary" to="/scenarios">
|
|
707
|
+
Back
|
|
708
|
+
</Link>
|
|
709
|
+
),
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
kind: 'dropdown',
|
|
713
|
+
id: 'add',
|
|
714
|
+
label: '+ Add',
|
|
715
|
+
disabled: isPublished,
|
|
716
|
+
entries: [
|
|
717
|
+
{ id: 'prompt', label: 'Prompt', onClick: () => addNode('prompt') },
|
|
718
|
+
{ id: 'email', label: 'Email', onClick: () => addNode('email') },
|
|
719
|
+
{ id: 'div', label: '', divider: true },
|
|
720
|
+
{ id: 'outcome', label: 'Outcome node', onClick: () => addNode('outcome') },
|
|
721
|
+
],
|
|
722
|
+
},
|
|
723
|
+
{ kind: 'button', id: 'reset', label: 'Reset view', onClick: onReset, title: 'Reset view to fit graph' },
|
|
724
|
+
{ kind: 'button', id: 'save', label: saving ? 'Saving…' : 'Save Draft', variant: 'primary', onClick: onSave, disabled: saving },
|
|
725
|
+
{ kind: 'badge', id: 'status', label: isPublished ? 'Published (read-only)' : 'Draft', variant: isPublished ? 'success' : 'secondary' },
|
|
726
|
+
{ kind: 'text', id: 'autosave', content: autosaveLabel },
|
|
727
|
+
];
|
|
728
|
+
|
|
729
|
+
<ToolbarPanel items={items} />;
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
**Item kinds.** Each `ToolbarItem` is one of:
|
|
733
|
+
|
|
734
|
+
- `kind: 'button'`: `label`, `onClick`, optional `variant` (Bootstrap variant string, default `outline-secondary`), `disabled`, `title`, `className`.
|
|
735
|
+
- `kind: 'dropdown'`: `label`, `entries` (array of `{ id?, label, onClick?, disabled?, divider? }`), optional `variant` (toggle variant, default `outline-secondary`), `disabled`, `className`. Entries with `divider: true` render a `<Dropdown.Divider />`.
|
|
736
|
+
- `kind: 'badge'`: `label`, optional `variant` (Bootstrap badge variant, default `secondary`) or full `className` override.
|
|
737
|
+
- `kind: 'text'`: `content`, optional `className` (default `"ms-2 text-muted font-size-12"`).
|
|
738
|
+
- `kind: 'custom'`: `render: () => ReactNode` for one-offs the package shouldn't know about (e.g. `<Link>` from `react-router-dom`).
|
|
739
|
+
|
|
740
|
+
Every item supports an optional `id` used as the React key. `ToolbarPanel` also accepts an optional `className` to override the outer wrapper styling.
|
|
741
|
+
|
|
742
|
+
## Tree Operations
|
|
743
|
+
|
|
744
|
+
Project-agnostic helpers for the things every TreeSpec authoring UI needs.
|
|
745
|
+
|
|
746
|
+
### `duplicateNode(tree, nodeId)`
|
|
747
|
+
|
|
748
|
+
Returns `{ nextTree, nextNodeId } | null`. The clone gets a fresh node id, fresh choice ids, and a `(+40, +40)` position offset from the source. Transitions are intentionally **not** copied — the new node starts orphaned. Returns `null` for the synthetic `END_NODE_ID` and unknown ids.
|
|
749
|
+
|
|
750
|
+
```tsx
|
|
751
|
+
import { duplicateNode } from '@signalsafe/tree-spec-editor';
|
|
752
|
+
|
|
753
|
+
const result = duplicateNode(tree, selectedNodeId);
|
|
754
|
+
if (result) {
|
|
755
|
+
setTree(result.nextTree);
|
|
756
|
+
setSelection({ kind: 'node', id: result.nextNodeId });
|
|
757
|
+
}
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### `deleteNode(tree, nodeId)`
|
|
761
|
+
|
|
762
|
+
Returns the next `EditorTree | null`. Removes the node and any transition with that node on either endpoint. When the deleted node was `tree.start_node`, picks the first remaining node (insertion order) as the new start. Returns `null` for the synthetic `END_NODE_ID` and unknown ids.
|
|
763
|
+
|
|
764
|
+
```tsx
|
|
765
|
+
import { deleteNode } from '@signalsafe/tree-spec-editor';
|
|
766
|
+
|
|
767
|
+
const nextTree = deleteNode(tree, selectedNodeId);
|
|
768
|
+
if (nextTree) setTree(nextTree);
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### `computeTreeDiffSummary(before, after)`
|
|
772
|
+
|
|
773
|
+
Returns `{ lines: string[]; hasChanges: boolean }`. Produces a short human-readable diff (added/removed nodes, changed type-or-prompt counts, changed choice counts, added/removed transition keys). Suitable as the default `computeDiffSummary` callback for `PublishReviewModal`.
|
|
774
|
+
|
|
775
|
+
```tsx
|
|
776
|
+
import { computeTreeDiffSummary, PublishReviewModal } from '@signalsafe/tree-spec-editor';
|
|
777
|
+
|
|
778
|
+
<PublishReviewModal
|
|
779
|
+
show={showPublish}
|
|
780
|
+
onClose={() => setShowPublish(false)}
|
|
781
|
+
onPublish={publish}
|
|
782
|
+
currentTree={tree}
|
|
783
|
+
baselineTree={publishedTree}
|
|
784
|
+
computeDiffSummary={computeTreeDiffSummary}
|
|
785
|
+
/>;
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### `applyTreeTemplate(tree, spec, options?)`
|
|
789
|
+
|
|
790
|
+
Inserts a reusable sub-graph (`TreeTemplateSpec`) into `tree`. The spec uses **slot names** (host-defined strings like `"focus"`, `"verify"`) to wire its internal transitions; the helper generates a fresh node id for each slot at insertion time so the same template can be inserted multiple times without collisions. Transitions reference either a slot name or the literal `END_NODE_ID`.
|
|
791
|
+
|
|
792
|
+
Returns `{ nextTree, focusNodeId }`. The original tree's `start_node` is preserved when set; otherwise the focus node id is used.
|
|
793
|
+
|
|
794
|
+
```tsx
|
|
795
|
+
import {
|
|
796
|
+
applyTreeTemplate,
|
|
797
|
+
END_NODE_ID,
|
|
798
|
+
type TreeTemplateSpec,
|
|
799
|
+
} from '@signalsafe/tree-spec-editor';
|
|
800
|
+
|
|
801
|
+
const CEO_CALL: TreeTemplateSpec = {
|
|
802
|
+
focusSlot: 'focus',
|
|
803
|
+
nodes: {
|
|
804
|
+
focus: {
|
|
805
|
+
type: 'call',
|
|
806
|
+
prompt: 'You receive an urgent call from your CEO...',
|
|
807
|
+
choices: [
|
|
808
|
+
{ id: 'comply', label: 'Comply immediately' },
|
|
809
|
+
{ id: 'verify', label: 'Verify identity out-of-band' },
|
|
810
|
+
],
|
|
811
|
+
offset: { x: 60, y: 0 },
|
|
812
|
+
},
|
|
813
|
+
verify: {
|
|
814
|
+
type: 'prompt',
|
|
815
|
+
prompt: 'What is the safest way to verify the request?',
|
|
816
|
+
choices: [
|
|
817
|
+
{ id: 'callback', label: 'Call a known number / assistant' },
|
|
818
|
+
{ id: 'reply', label: 'Ask follow-ups on the same call' },
|
|
819
|
+
],
|
|
820
|
+
offset: { x: 420, y: 0 },
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
transitions: [
|
|
824
|
+
{ fromSlot: 'focus', fromChoiceId: 'comply', toSlot: END_NODE_ID, outcome: 'compromised' },
|
|
825
|
+
{ fromSlot: 'focus', fromChoiceId: 'verify', toSlot: 'verify' },
|
|
826
|
+
{ fromSlot: 'verify', fromChoiceId: 'callback', toSlot: END_NODE_ID, outcome: 'safe' },
|
|
827
|
+
{ fromSlot: 'verify', fromChoiceId: 'reply', toSlot: END_NODE_ID, outcome: 'at_risk' },
|
|
828
|
+
],
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const { nextTree, focusNodeId } = applyTreeTemplate(tree, CEO_CALL);
|
|
832
|
+
setTree(nextTree);
|
|
833
|
+
setSelection({ kind: 'node', id: focusNodeId });
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
**Positioning.** Each node spec's `offset` is added to a base position. The default base is `{ x: 0, y: getNextSpawnPosition(tree).y }`, which places the cluster at the next vertical slot while letting nodes declare absolute X coordinates. Override via `options.basePosition` to anchor elsewhere.
|
|
837
|
+
|
|
838
|
+
**Validation.** `applyTreeTemplate` throws when `spec.focusSlot` is missing from `spec.nodes`, or when any transition references an unknown slot.
|
|
839
|
+
|
|
840
|
+
## Editor Lifecycle Helpers
|
|
841
|
+
|
|
842
|
+
Small pure helpers for the parts of an editor page that aren't graph-shaped: the autosave indicator, keyboard shortcuts, and the "is this a draft?" load policy. Hosts can use them as-is, wrap them for i18n, or replace them entirely — they hold no editor state.
|
|
843
|
+
|
|
844
|
+
### `getAutosaveStatusLabel(status)`
|
|
845
|
+
|
|
846
|
+
Maps an `AutosaveStatus` (`'idle' | 'dirty' | 'saving' | 'saved'`) to a default toolbar label. Returns the empty string for `'idle'` so the toolbar can render nothing between editing sessions.
|
|
847
|
+
|
|
848
|
+
```tsx
|
|
849
|
+
import {
|
|
850
|
+
getAutosaveStatusLabel,
|
|
851
|
+
ToolbarPanel,
|
|
852
|
+
type AutosaveStatus,
|
|
853
|
+
type ToolbarItem,
|
|
854
|
+
} from '@signalsafe/tree-spec-editor';
|
|
855
|
+
|
|
856
|
+
function buildToolbar(autosaveStatus: AutosaveStatus): ToolbarItem[] {
|
|
857
|
+
return [
|
|
858
|
+
{ kind: 'text', id: 'autosave', content: getAutosaveStatusLabel(autosaveStatus) },
|
|
859
|
+
];
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
### `getKeyboardShortcutAction(params)`
|
|
864
|
+
|
|
865
|
+
Pure key-event-to-action mapper. Accepts the modifier flags and `key` string directly (not a `KeyboardEvent`) so tests don't need a DOM event.
|
|
866
|
+
|
|
867
|
+
Default bindings:
|
|
868
|
+
|
|
869
|
+
| Combo | Action | Notes |
|
|
870
|
+
|-----------------------------|-------------|-------|
|
|
871
|
+
| `Ctrl/Cmd + S` | `save` | — |
|
|
872
|
+
| `Ctrl/Cmd + Shift + V` | `validate` | — |
|
|
873
|
+
| `Ctrl/Cmd + P` | `preview` | — |
|
|
874
|
+
| `Ctrl/Cmd + D` | `duplicate` | Only when `hasSelectedNode === true` |
|
|
875
|
+
| `Delete` or `Backspace` | `delete` | Only when `hasSelectedNode === true` |
|
|
876
|
+
|
|
877
|
+
```tsx
|
|
878
|
+
import { getKeyboardShortcutAction } from '@signalsafe/tree-spec-editor';
|
|
879
|
+
|
|
880
|
+
useEffect(() => {
|
|
881
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
882
|
+
const action = getKeyboardShortcutAction({
|
|
883
|
+
ctrlKey: event.ctrlKey,
|
|
884
|
+
metaKey: event.metaKey,
|
|
885
|
+
shiftKey: event.shiftKey,
|
|
886
|
+
key: event.key,
|
|
887
|
+
hasSelectedNode: selection.kind === 'node',
|
|
888
|
+
});
|
|
889
|
+
if (action) {
|
|
890
|
+
event.preventDefault();
|
|
891
|
+
dispatch(action);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
window.addEventListener('keydown', onKeyDown);
|
|
895
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
896
|
+
}, [selection.kind, dispatch]);
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
If a host needs different bindings, write your own dispatcher and skip this helper — the package does not require it.
|
|
900
|
+
|
|
901
|
+
### `shouldQueueInitialValidation(isPublished)`
|
|
902
|
+
|
|
903
|
+
Returns `true` when `isPublished` is anything other than strictly `true` (i.e. `false`, `null`, or `undefined`). Lets editors open drafts in a "validate on load" state without treating unknown-status loads as published.
|
|
904
|
+
|
|
905
|
+
```tsx
|
|
906
|
+
import { shouldQueueInitialValidation } from '@signalsafe/tree-spec-editor';
|
|
907
|
+
|
|
908
|
+
useEffect(() => {
|
|
909
|
+
if (shouldQueueInitialValidation(rawVersion?.is_published)) {
|
|
910
|
+
queueValidate();
|
|
911
|
+
}
|
|
912
|
+
}, [rawVersion?.is_published, queueValidate]);
|
|
913
|
+
```
|
|
914
|
+
|
|
260
915
|
## Helper Examples
|
|
261
916
|
|
|
262
917
|
Use `autoLayoutTree` before first render when you want a readable default layout.
|
|
@@ -369,5 +1024,5 @@ function addPromptNode(tree: EditorTree): EditorTree {
|
|
|
369
1024
|
## Notes
|
|
370
1025
|
|
|
371
1026
|
- Import from the package root only.
|
|
372
|
-
- Install `react`, `react-dom`, and `
|
|
1027
|
+
- Install `react`, `react-dom`, `reactflow`, and `react-bootstrap` in the consuming app, and load Bootstrap CSS as usual.
|
|
373
1028
|
- If you need wire-format compile/decompile helpers, use `@signalsafe/tree-spec`.
|