@signalsafe/tree-spec-editor 0.1.2 → 0.2.1

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 (56) hide show
  1. package/README.md +657 -2
  2. package/dist/index.d.ts +48 -7
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +39 -5
  5. package/dist/lib/PanelHeaderCollapseCarets.d.ts +7 -0
  6. package/dist/lib/PanelHeaderCollapseCarets.d.ts.map +1 -0
  7. package/dist/lib/PanelHeaderCollapseCarets.js +5 -0
  8. package/dist/lib/buildDefaultToolbarSpec.d.ts +83 -0
  9. package/dist/lib/buildDefaultToolbarSpec.d.ts.map +1 -0
  10. package/dist/lib/buildDefaultToolbarSpec.js +167 -0
  11. package/dist/lib/panelHelpers.d.ts +3 -0
  12. package/dist/lib/panelHelpers.d.ts.map +1 -0
  13. package/dist/lib/panelHelpers.js +14 -0
  14. package/dist/modals/AuditLogModal.d.ts +21 -0
  15. package/dist/modals/AuditLogModal.d.ts.map +1 -0
  16. package/dist/modals/AuditLogModal.js +23 -0
  17. package/dist/modals/DraftHistoryModal.d.ts +34 -0
  18. package/dist/modals/DraftHistoryModal.d.ts.map +1 -0
  19. package/dist/modals/DraftHistoryModal.js +27 -0
  20. package/dist/modals/PublishReviewModal.d.ts +37 -0
  21. package/dist/modals/PublishReviewModal.d.ts.map +1 -0
  22. package/dist/modals/PublishReviewModal.js +25 -0
  23. package/dist/panels/AdvancedJsonPanel.d.ts +27 -0
  24. package/dist/panels/AdvancedJsonPanel.d.ts.map +1 -0
  25. package/dist/panels/AdvancedJsonPanel.js +11 -0
  26. package/dist/panels/GraphEditorInfoPanel.d.ts +20 -0
  27. package/dist/panels/GraphEditorInfoPanel.d.ts.map +1 -0
  28. package/dist/panels/GraphEditorInfoPanel.js +25 -0
  29. package/dist/panels/InspectorPanel.d.ts +79 -0
  30. package/dist/panels/InspectorPanel.d.ts.map +1 -0
  31. package/dist/panels/InspectorPanel.js +63 -0
  32. package/dist/panels/IssuesPanel.d.ts +33 -0
  33. package/dist/panels/IssuesPanel.d.ts.map +1 -0
  34. package/dist/panels/IssuesPanel.js +41 -0
  35. package/dist/panels/NodesPanel.d.ts +28 -0
  36. package/dist/panels/NodesPanel.d.ts.map +1 -0
  37. package/dist/panels/NodesPanel.js +48 -0
  38. package/dist/panels/SelectedEdgePanel.d.ts +18 -0
  39. package/dist/panels/SelectedEdgePanel.d.ts.map +1 -0
  40. package/dist/panels/SelectedEdgePanel.js +9 -0
  41. package/dist/panels/ToolbarPanel.d.ts +88 -0
  42. package/dist/panels/ToolbarPanel.d.ts.map +1 -0
  43. package/dist/panels/ToolbarPanel.js +59 -0
  44. package/package.json +7 -3
  45. package/dist/TreeSpecGraphEditor.d.ts +0 -16
  46. package/dist/TreeSpecGraphEditor.d.ts.map +0 -1
  47. package/dist/TreeSpecGraphEditor.js +0 -272
  48. package/dist/editorHelpers.d.ts +0 -16
  49. package/dist/editorHelpers.d.ts.map +0 -1
  50. package/dist/editorHelpers.js +0 -67
  51. package/dist/model.d.ts +0 -23
  52. package/dist/model.d.ts.map +0 -1
  53. package/dist/model.js +0 -1
  54. package/dist/treeLayout.d.ts +0 -11
  55. package/dist/treeLayout.d.ts.map +0 -1
  56. package/dist/treeLayout.js +0 -63
@@ -0,0 +1,27 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface AdvancedJsonPanelProps {
3
+ /** The JSON editor (or any content) rendered inside the panel body. */
4
+ children: ReactNode;
5
+ /** Optional header title. Defaults to "Advanced JSON (read-only)". */
6
+ title?: string;
7
+ /** Optional muted subtitle line below the title. */
8
+ subtitle?: string;
9
+ /**
10
+ * Called when the user clicks the "Expand all" button. The button is rendered only when
11
+ * this callback is provided. Hosts typically wire this to a `JsonEditorHandle.expandAll()` ref.
12
+ */
13
+ onExpandAll?: () => void;
14
+ /**
15
+ * Called when the user clicks the "Compress all" button. The button is rendered only when
16
+ * this callback is provided. Hosts typically wire this to a `JsonEditorHandle.collapseAll()` ref.
17
+ */
18
+ onCollapseAll?: () => void;
19
+ /** Optional override for the expand button label (default "Expand all"). */
20
+ expandAllLabel?: string;
21
+ /** Optional override for the collapse button label (default "Compress all"). */
22
+ collapseAllLabel?: string;
23
+ /** Optional extra class names applied to the outer `card` element. */
24
+ className?: string;
25
+ }
26
+ export default function AdvancedJsonPanel({ children, title, subtitle, onExpandAll, onCollapseAll, expandAllLabel, collapseAllLabel, className, }: Readonly<AdvancedJsonPanelProps>): import("react/jsx-runtime").JSX.Element;
27
+ //# sourceMappingURL=AdvancedJsonPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdvancedJsonPanel.d.ts","sourceRoot":"","sources":["../../src/panels/AdvancedJsonPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC,MAAM,WAAW,sBAAsB;IACnC,uEAAuE;IACvE,QAAQ,EAAE,SAAS,CAAC;IACpB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EACtC,QAAQ,EACR,KAAqB,EACrB,QAAQ,EACR,WAAW,EACX,aAAa,EACb,cAA6B,EAC7B,gBAAiC,EACjC,SAAS,GACZ,EAAE,QAAQ,CAAC,sBAAsB,CAAC,2CAsClC"}
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Button } from 'react-bootstrap';
4
+ import PanelHeaderCollapseCarets from '../lib/PanelHeaderCollapseCarets';
5
+ const DEFAULT_TITLE = 'Advanced JSON (read-only)';
6
+ export default function AdvancedJsonPanel({ children, title = DEFAULT_TITLE, subtitle, onExpandAll, onCollapseAll, expandAllLabel = 'Expand all', collapseAllLabel = 'Compress all', className, }) {
7
+ const [expanded, setExpanded] = useState(true);
8
+ const hasButtons = Boolean(onExpandAll) || Boolean(onCollapseAll);
9
+ const cardClassName = ['card', 'mt-3', className].filter(Boolean).join(' ');
10
+ return (_jsxs("div", { className: cardClassName, children: [_jsxs("div", { className: "card-header d-flex justify-content-between align-items-start gap-2 flex-wrap bg-body-secondary py-2 px-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "d-flex align-items-center flex-wrap gap-1", children: [_jsx("span", { className: "fw-bold", children: title }), _jsx(PanelHeaderCollapseCarets, { expanded: expanded, onToggle: () => setExpanded((v) => !v) })] }), subtitle ? _jsx("div", { className: "text-muted font-size-12", children: subtitle }) : null] }), hasButtons ? (_jsxs("div", { className: "btn-group btn-group-sm flex-shrink-0 ms-auto", children: [onExpandAll ? (_jsx(Button, { type: "button", variant: "outline-secondary", onClick: onExpandAll, children: expandAllLabel })) : null, onCollapseAll ? (_jsx(Button, { type: "button", variant: "outline-secondary", onClick: onCollapseAll, children: collapseAllLabel })) : null] })) : null] }), _jsx("div", { className: `card-body p-0${expanded ? '' : ' d-none'}`, "aria-hidden": !expanded, children: children })] }));
11
+ }
@@ -0,0 +1,20 @@
1
+ import { type AutosaveStatus } from '@signalsafe/tree-spec-editor-core';
2
+ export interface GraphEditorInfoPanelProps {
3
+ scenarioId: string;
4
+ versionId: string;
5
+ name: string;
6
+ createdAt: string | null;
7
+ updatedAt: string | null;
8
+ /** Whether this version is the published (read-only) revision. */
9
+ isPublished: boolean;
10
+ /** Autosave lifecycle state from the graph editor hook. */
11
+ autosaveStatus: AutosaveStatus;
12
+ /** Last validation time (ISO), or `null` if never validated in this session. */
13
+ lastValidatedAt: string | null;
14
+ /** Optional card header title. Defaults to "Info". */
15
+ title?: string;
16
+ /** Optional formatter for ISO timestamps. Defaults to `new Date(iso).toLocaleString()`. */
17
+ formatTimestamp?: (iso: string) => string;
18
+ }
19
+ export default function GraphEditorInfoPanel({ scenarioId, versionId, name, createdAt, updatedAt, isPublished, autosaveStatus, lastValidatedAt, title, formatTimestamp, }: Readonly<GraphEditorInfoPanelProps>): import("react/jsx-runtime").JSX.Element;
20
+ //# sourceMappingURL=GraphEditorInfoPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GraphEditorInfoPanel.d.ts","sourceRoot":"","sources":["../../src/panels/GraphEditorInfoPanel.tsx"],"names":[],"mappings":"AAKA,OAAO,EAA0B,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAEhG,MAAM,WAAW,yBAAyB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kEAAkE;IAClE,WAAW,EAAE,OAAO,CAAC;IACrB,2DAA2D;IAC3D,cAAc,EAAE,cAAc,CAAC;IAC/B,gFAAgF;IAChF,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2FAA2F;IAC3F,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CAC7C;AA0BD,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAAC,EACzC,UAAU,EACV,SAAS,EACT,IAAI,EACJ,SAAS,EACT,SAAS,EACT,WAAW,EACX,cAAc,EACd,eAAe,EACf,KAAc,EACd,eAAwC,GAC3C,EAAE,QAAQ,CAAC,yBAAyB,CAAC,2CA4DrC"}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Badge, Table } from 'react-bootstrap';
4
+ import PanelHeaderCollapseCarets from '../lib/PanelHeaderCollapseCarets';
5
+ import { getAutosaveStatusLabel } from '@signalsafe/tree-spec-editor-core';
6
+ function defaultFormatTimestamp(iso) {
7
+ return new Date(iso).toLocaleString();
8
+ }
9
+ function PropertyRow({ label, value }) {
10
+ return (_jsxs("tr", { children: [_jsx("td", { className: "align-top", children: _jsx("strong", { children: label }) }), _jsx("td", { children: value })] }));
11
+ }
12
+ const STATUS_DRAFT = 'Draft';
13
+ const STATUS_PUBLISHED = 'Published (read-only)';
14
+ function autosaveDisplay(isPublished, status) {
15
+ if (isPublished)
16
+ return '—';
17
+ const label = getAutosaveStatusLabel(status);
18
+ return label || '—';
19
+ }
20
+ export default function GraphEditorInfoPanel({ scenarioId, versionId, name, createdAt, updatedAt, isPublished, autosaveStatus, lastValidatedAt, title = 'Info', formatTimestamp = defaultFormatTimestamp, }) {
21
+ const [expanded, setExpanded] = useState(true);
22
+ return (_jsxs("div", { className: "card mb-3", children: [_jsx("div", { className: "card-header bg-body-secondary py-2 px-2", children: _jsxs("div", { className: "d-flex align-items-center flex-wrap gap-1", children: [_jsx("h5", { className: "mb-0", children: title }), _jsx(PanelHeaderCollapseCarets, { expanded: expanded, onToggle: () => setExpanded((v) => !v) })] }) }), _jsx("div", { className: `card-body p-0${expanded ? '' : ' d-none'}`, "aria-hidden": !expanded, children: _jsx(Table, { striped: true, bordered: true, hover: true, responsive: true, className: "m-0", children: _jsxs("tbody", { children: [_jsx(PropertyRow, { label: "Name", value: name }), _jsx(PropertyRow, { label: "Status", value: _jsx(Badge, { bg: isPublished ? 'success' : 'secondary', children: isPublished ? STATUS_PUBLISHED : STATUS_DRAFT }) }), _jsx(PropertyRow, { label: "Save state", value: _jsx("span", { className: "text-muted", children: autosaveDisplay(isPublished, autosaveStatus) }) }), _jsx(PropertyRow, { label: "Validated", value: _jsx("span", { className: "text-muted", children: lastValidatedAt
23
+ ? `Validated ${formatTimestamp(lastValidatedAt)}`
24
+ : 'Not validated' }) }), _jsx(PropertyRow, { label: "Scenario ID", value: _jsx("span", { className: "font-monospace small text-break", children: scenarioId }) }), _jsx(PropertyRow, { label: "Version ID", value: _jsx("span", { className: "font-monospace small text-break", children: versionId }) }), _jsx(PropertyRow, { label: "Created", value: createdAt ? formatTimestamp(createdAt) : '—' }), _jsx(PropertyRow, { label: "Updated", value: updatedAt ? formatTimestamp(updatedAt) : '—' })] }) }) })] }));
25
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Inspector panel for the TreeSpec graph editor.
3
+ *
4
+ * Renders a **Required** card (Type, Prompt, optional `renderExtraNodeFields`) and a **Choices**
5
+ * card (list-group rows with Label / Next / Outcome / delete). Hosts inject project-specific UI through optional render-prop slots:
6
+ *
7
+ * - `outcomeOptions`: override the END-transition outcome `<select>` options
8
+ * (default = the three canonical `TerminalOutcome` values from `@signalsafe/tree-spec`).
9
+ * - `hideOutcomeField`: hide the outcome `<select>` entirely when the host manages outcomes
10
+ * elsewhere.
11
+ * - `renderExtraNodeFields`: render additional UI below the prompt textarea, scoped to the
12
+ * currently-selected node.
13
+ * - `renderExtraChoiceFields`: render additional UI below each choice card.
14
+ */
15
+ import type { ReactNode } from 'react';
16
+ import { type EditorChoice, type EditorNode, type EditorTransition, type EditorTree } from '@signalsafe/tree-spec-editor-core';
17
+ /**
18
+ * Canonical outcome values from the `@signalsafe/tree-spec` wire contract.
19
+ * Hosts can override via the `outcomeOptions` prop.
20
+ */
21
+ export declare const DEFAULT_OUTCOME_OPTIONS: ReadonlyArray<{
22
+ value: string;
23
+ label: string;
24
+ }>;
25
+ export interface InspectorNodeRenderContext {
26
+ tree: EditorTree;
27
+ node: EditorNode;
28
+ isPublished: boolean;
29
+ onUpdateNode: (patch: Partial<EditorNode>) => void;
30
+ }
31
+ export interface InspectorChoiceRenderContext {
32
+ tree: EditorTree;
33
+ node: EditorNode;
34
+ choice: EditorChoice;
35
+ transition: EditorTransition | undefined;
36
+ isPublished: boolean;
37
+ onUpdateNode: (patch: Partial<EditorNode>) => void;
38
+ }
39
+ export interface InspectorPanelProps {
40
+ tree: EditorTree;
41
+ selectedNode: EditorNode | null;
42
+ focusChoiceId?: string | null;
43
+ isPublished?: boolean;
44
+ onUpdateSelectedNode: (patch: Partial<EditorNode>) => void;
45
+ onAddChoice: () => void;
46
+ onDeleteChoice: (choiceId: string) => void;
47
+ onSetChoiceTarget: (choiceId: string, targetNodeId: string) => void;
48
+ onSetChoiceOutcome: (choiceId: string, outcome: string) => void;
49
+ /**
50
+ * Override the END-transition outcome `<select>` options.
51
+ * Defaults to the canonical TreeSpec `TerminalOutcome` values.
52
+ */
53
+ outcomeOptions?: ReadonlyArray<{
54
+ value: string;
55
+ label: string;
56
+ }>;
57
+ /** Hide the outcome selector entirely. Useful when the host manages outcomes elsewhere. */
58
+ hideOutcomeField?: boolean;
59
+ /** Render extra UI below the prompt textarea, scoped to the current node. */
60
+ renderExtraNodeFields?: (ctx: InspectorNodeRenderContext) => ReactNode;
61
+ /** Render extra UI below each choice card (after Next/Outcome). */
62
+ renderExtraChoiceFields?: (ctx: InspectorChoiceRenderContext) => ReactNode;
63
+ /**
64
+ * When set, shows a delete control in the Required card header (grid-style icon button).
65
+ * Typical host: `editor.actions.deleteSelectedNode`. Disabled when `isPublished` is true.
66
+ */
67
+ onDeleteSelectedNode?: () => void;
68
+ /** Override the Required card header (default "Required"). */
69
+ title?: string;
70
+ /** Override the empty-state message (default "Select a node to edit it."). */
71
+ emptyStateText?: string;
72
+ /**
73
+ * Override the muted helper text under the Type field.
74
+ * Defaults to: "Type is stored as a string in tree_spec. You can use presets or any custom value."
75
+ */
76
+ typeHelperText?: string;
77
+ }
78
+ export default function InspectorPanel({ tree, selectedNode, focusChoiceId, isPublished, onUpdateSelectedNode, onAddChoice, onDeleteChoice, onSetChoiceTarget, onSetChoiceOutcome, outcomeOptions, hideOutcomeField, renderExtraNodeFields, renderExtraChoiceFields, title, emptyStateText, typeHelperText, onDeleteSelectedNode, }: Readonly<InspectorPanelProps>): import("react/jsx-runtime").JSX.Element;
79
+ //# sourceMappingURL=InspectorPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InspectorPanel.d.ts","sourceRoot":"","sources":["../../src/panels/InspectorPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAe,SAAS,EAAE,MAAM,OAAO,CAAC;AAKpD,OAAO,EAIH,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAClB,MAAM,mCAAmC,CAAC;AAI3C;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAInF,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACvC,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,4BAA4B;IACzC,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACzC,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,oBAAoB,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC;IAC3D,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,iBAAiB,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACpE,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhE;;;OAGG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,6EAA6E;IAC7E,qBAAqB,CAAC,EAAE,CAAC,GAAG,EAAE,0BAA0B,KAAK,SAAS,CAAC;IACvE,mEAAmE;IACnE,uBAAuB,CAAC,EAAE,CAAC,GAAG,EAAE,4BAA4B,KAAK,SAAS,CAAC;IAE3E;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAElC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAoPD,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EACnC,IAAI,EACJ,YAAY,EACZ,aAAoB,EACpB,WAAmB,EACnB,oBAAoB,EACpB,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,cAAwC,EACxC,gBAAwB,EACxB,qBAAqB,EACrB,uBAAuB,EACvB,KAAkB,EAClB,cAA4C,EAC5C,cAAyC,EACzC,oBAAoB,GACvB,EAAE,QAAQ,CAAC,mBAAmB,CAAC,2CAkF/B"}
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Form } from 'react-bootstrap';
4
+ import { TERMINAL_OUTCOME } from '@signalsafe/tree-spec';
5
+ import { END_NODE_ID, getTransition, TREE_SPEC_NODE_TYPE_PRESETS, } from '@signalsafe/tree-spec-editor-core';
6
+ import PanelHeaderCollapseCarets from '../lib/PanelHeaderCollapseCarets';
7
+ /**
8
+ * Canonical outcome values from the `@signalsafe/tree-spec` wire contract.
9
+ * Hosts can override via the `outcomeOptions` prop.
10
+ */
11
+ export const DEFAULT_OUTCOME_OPTIONS = [
12
+ { value: TERMINAL_OUTCOME.SAFE, label: TERMINAL_OUTCOME.SAFE },
13
+ { value: TERMINAL_OUTCOME.AT_RISK, label: TERMINAL_OUTCOME.AT_RISK },
14
+ { value: TERMINAL_OUTCOME.COMPROMISED, label: TERMINAL_OUTCOME.COMPROMISED },
15
+ ];
16
+ const DEFAULT_TYPE_HELPER_TEXT = 'Type is stored as a string in tree_spec. You can use presets or any custom value.';
17
+ function NodeTypeField({ selectedNode, isPublished, onUpdateSelectedNode, typeHelperText, }) {
18
+ const isPreset = TREE_SPEC_NODE_TYPE_PRESETS.includes(selectedNode.type);
19
+ return (_jsxs(Form.Group, { className: "mb-2", children: [_jsx(Form.Label, { children: "Type" }), _jsxs(Form.Select, { value: isPreset ? selectedNode.type : '__custom__', onChange: (e) => {
20
+ const nextType = e.target.value;
21
+ if (nextType === '__custom__')
22
+ return;
23
+ onUpdateSelectedNode({ type: nextType });
24
+ }, disabled: isPublished, children: [TREE_SPEC_NODE_TYPE_PRESETS.map((t) => (_jsx("option", { value: t, children: t }, t))), _jsx("option", { value: "__custom__", children: "custom\u2026" })] }), isPreset ? null : (_jsx(Form.Control, { className: "mt-2", value: selectedNode.type, onChange: (e) => onUpdateSelectedNode({ type: e.target.value }), placeholder: "Enter custom type\u2026", disabled: isPublished })), _jsx("div", { className: "text-muted mt-1 font-size-12", children: typeHelperText })] }));
25
+ }
26
+ function ChoiceEditorCard({ choice, tree, selectedNode, focusChoiceId, isPublished, onUpdateSelectedNode, onDeleteChoice, onSetChoiceTarget, onSetChoiceOutcome, outcomeOptions, hideOutcomeField, renderExtraChoiceFields, }) {
27
+ const transition = getTransition(tree, selectedNode.id, choice.id);
28
+ const targetNodeId = transition?.toNodeId ?? '';
29
+ const outcomeValue = transition?.outcome ?? outcomeOptions[0]?.value ?? '';
30
+ const isFocused = focusChoiceId === choice.id;
31
+ const showOutcome = !hideOutcomeField && targetNodeId === END_NODE_ID;
32
+ return (_jsxs("div", { id: `choice-editor-${selectedNode.id}-${choice.id}`, className: `list-group-item py-3 px-3${isFocused ? ' list-group-item-primary' : ''}`, children: [_jsxs("div", { className: "d-flex w-100 justify-content-between align-items-start gap-2 mb-2", children: [_jsx("h5", { className: `mb-0 text-uppercase fs-6 fw-semibold${isFocused ? '' : ' text-body-secondary'}`, children: choice.id }), _jsx("button", { type: "button", className: "btn p-0 border-0 bg-transparent flex-shrink-0", "aria-label": "Delete choice", title: "Delete choice", onClick: () => onDeleteChoice(choice.id), children: _jsx("i", { className: "bi bi-trash text-danger action-icon cursor-pointer", "aria-hidden": true }) })] }), _jsx(Form.Label, { className: "mt-0", children: "Label" }), _jsx(Form.Control, { value: choice.label, onChange: (event) => {
33
+ const updatedChoices = (selectedNode.choices ?? []).map((existingChoice) => existingChoice.id === choice.id
34
+ ? { ...existingChoice, label: event.target.value }
35
+ : existingChoice);
36
+ onUpdateSelectedNode({ choices: updatedChoices });
37
+ } }), _jsx(Form.Label, { className: "mt-2", children: "Next" }), _jsxs(Form.Select, { value: targetNodeId, onChange: (e) => onSetChoiceTarget(choice.id, e.target.value), children: [_jsx("option", { value: "", children: "(not connected)" }), Object.values(tree.nodes).map((n) => (_jsxs("option", { value: n.id, children: [n.id, " \u00B7 ", n.prompt?.slice(0, 40) || n.type] }, n.id))), _jsx("option", { value: END_NODE_ID, children: "END" })] }), showOutcome ? (_jsxs(_Fragment, { children: [_jsx(Form.Label, { className: "mt-2", children: "Outcome" }), _jsx(Form.Select, { value: outcomeValue, onChange: (e) => onSetChoiceOutcome(choice.id, e.target.value), children: outcomeOptions.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) })] })) : null, renderExtraChoiceFields
38
+ ? renderExtraChoiceFields({
39
+ tree,
40
+ node: selectedNode,
41
+ choice,
42
+ transition,
43
+ isPublished,
44
+ onUpdateNode: onUpdateSelectedNode,
45
+ })
46
+ : null] }));
47
+ }
48
+ function ChoiceEditorList({ tree, selectedNode, focusChoiceId, isPublished, onAddChoice, onUpdateSelectedNode, onDeleteChoice, onSetChoiceTarget, onSetChoiceOutcome, outcomeOptions, hideOutcomeField, renderExtraChoiceFields, }) {
49
+ const choices = selectedNode.choices ?? [];
50
+ const [choicesExpanded, setChoicesExpanded] = useState(true);
51
+ return (_jsxs("div", { className: "card mt-3", children: [_jsxs("div", { className: "card-header bg-body-secondary py-2 px-2 d-flex justify-content-between align-items-center gap-2", children: [_jsxs("div", { className: "d-flex align-items-center min-w-0", children: [_jsx("span", { className: "fw-semibold", children: "Choices" }), _jsx(PanelHeaderCollapseCarets, { expanded: choicesExpanded, onToggle: () => setChoicesExpanded((v) => !v) })] }), _jsx("button", { type: "button", className: "btn p-0 border-0 bg-transparent flex-shrink-0", "aria-label": "Add choice", title: "Add choice", onClick: onAddChoice, children: _jsx("i", { className: "bi bi-plus-lg text-primary action-icon cursor-pointer", "aria-hidden": true }) })] }), _jsx("div", { className: `card-body p-0${choicesExpanded ? '' : ' d-none'}`, "aria-hidden": !choicesExpanded, children: choices.length === 0 ? (_jsx("div", { className: "text-muted small py-3 px-3", children: _jsx("em", { children: "No choices" }) })) : (_jsx("div", { className: "list-group list-group-flush", children: choices.map((choice) => (_jsx(ChoiceEditorCard, { choice: choice, tree: tree, selectedNode: selectedNode, focusChoiceId: focusChoiceId, isPublished: isPublished, onUpdateSelectedNode: onUpdateSelectedNode, onDeleteChoice: onDeleteChoice, onSetChoiceTarget: onSetChoiceTarget, onSetChoiceOutcome: onSetChoiceOutcome, outcomeOptions: outcomeOptions, hideOutcomeField: hideOutcomeField, renderExtraChoiceFields: renderExtraChoiceFields }, choice.id))) })) })] }));
52
+ }
53
+ export default function InspectorPanel({ tree, selectedNode, focusChoiceId = null, isPublished = false, onUpdateSelectedNode, onAddChoice, onDeleteChoice, onSetChoiceTarget, onSetChoiceOutcome, outcomeOptions = DEFAULT_OUTCOME_OPTIONS, hideOutcomeField = false, renderExtraNodeFields, renderExtraChoiceFields, title = 'Required', emptyStateText = 'Select a node to edit it.', typeHelperText = DEFAULT_TYPE_HELPER_TEXT, onDeleteSelectedNode, }) {
54
+ const [requiredExpanded, setRequiredExpanded] = useState(true);
55
+ return (_jsx("div", { children: selectedNode ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "card", children: [_jsxs("div", { className: "card-header bg-body-secondary py-2 px-2 d-flex justify-content-between align-items-center gap-2", children: [_jsxs("div", { className: "d-flex align-items-center min-w-0", children: [_jsx("span", { className: "fw-semibold", children: title }), _jsx(PanelHeaderCollapseCarets, { expanded: requiredExpanded, onToggle: () => setRequiredExpanded((v) => !v) })] }), onDeleteSelectedNode ? (_jsx("button", { type: "button", className: "btn p-0 border-0 bg-transparent flex-shrink-0", "aria-label": "Delete node", title: "Delete node", disabled: isPublished, onClick: onDeleteSelectedNode, children: _jsx("i", { className: `bi bi-trash action-icon cursor-pointer${isPublished ? ' text-secondary opacity-50' : ' text-danger'}`, "aria-hidden": true }) })) : null] }), _jsxs("div", { className: `card-body${requiredExpanded ? '' : ' d-none'}`, "aria-hidden": !requiredExpanded, children: [_jsx(NodeTypeField, { selectedNode: selectedNode, isPublished: isPublished, onUpdateSelectedNode: onUpdateSelectedNode, typeHelperText: typeHelperText }), _jsxs(Form.Group, { className: "mb-2", children: [_jsx(Form.Label, { children: "Prompt" }), _jsx(Form.Control, { as: "textarea", rows: 4, value: selectedNode.prompt, onChange: (e) => onUpdateSelectedNode({ prompt: e.target.value }) })] }), renderExtraNodeFields
56
+ ? renderExtraNodeFields({
57
+ tree,
58
+ node: selectedNode,
59
+ isPublished,
60
+ onUpdateNode: onUpdateSelectedNode,
61
+ })
62
+ : null] })] }), _jsx(ChoiceEditorList, { tree: tree, selectedNode: selectedNode, focusChoiceId: focusChoiceId, isPublished: isPublished, onAddChoice: onAddChoice, onUpdateSelectedNode: onUpdateSelectedNode, onDeleteChoice: onDeleteChoice, onSetChoiceTarget: onSetChoiceTarget, onSetChoiceOutcome: onSetChoiceOutcome, outcomeOptions: outcomeOptions, hideOutcomeField: hideOutcomeField, renderExtraChoiceFields: renderExtraChoiceFields })] })) : (_jsx("div", { className: "text-muted", children: _jsx("em", { children: emptyStateText }) })) }));
63
+ }
@@ -0,0 +1,33 @@
1
+ import type { TreeSpecIssue } from '@signalsafe/tree-spec';
2
+ import { type EditorTree } from '@signalsafe/tree-spec-editor-core';
3
+ export interface IssuesPanelProps {
4
+ issues: TreeSpecIssue[];
5
+ lastValidatedAt: string | null;
6
+ onSelectIssue: (issue: TreeSpecIssue) => void;
7
+ /**
8
+ * Search text for filtering the list (same pattern as {@link NodesPanelProps.nodeSearch}).
9
+ */
10
+ issueSearch: string;
11
+ /** Called when the user edits the issues search field. */
12
+ onIssueSearchChange: (value: string) => void;
13
+ /**
14
+ * When set, each issue row header shows **node id**, **type** (from `tree.nodes`), and a
15
+ * severity **badge** on the right (same header rhythm as **NodesPanel**). Omit when you do
16
+ * not have a tree in scope; type falls back to **—**.
17
+ */
18
+ tree?: EditorTree;
19
+ /** Optional override for the card header title. Defaults to "Issues". */
20
+ title?: string;
21
+ /** Optional placeholder for the search field. Defaults to "Search issues…". */
22
+ searchPlaceholder?: string;
23
+ /** Optional formatter for the validated-at timestamp. Defaults to `new Date(iso).toLocaleString()`. */
24
+ formatTimestamp?: (iso: string) => string;
25
+ /**
26
+ * When `false`, the "Validated …" / "Not validated" summary is not shown in
27
+ * this card header (e.g. when the host shows it in an info panel instead).
28
+ * Default: `true`.
29
+ */
30
+ showValidationSummaryInHeader?: boolean;
31
+ }
32
+ export default function IssuesPanel({ issues, lastValidatedAt, onSelectIssue, issueSearch, onIssueSearchChange, tree, title, searchPlaceholder, formatTimestamp, showValidationSummaryInHeader, }: Readonly<IssuesPanelProps>): import("react/jsx-runtime").JSX.Element;
33
+ //# sourceMappingURL=IssuesPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IssuesPanel.d.ts","sourceRoot":"","sources":["../../src/panels/IssuesPanel.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAKxF,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,aAAa,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC9C;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,mBAAmB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C;;;;OAIG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uGAAuG;IACvG,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C;;;;OAIG;IACH,6BAA6B,CAAC,EAAE,OAAO,CAAC;CAC3C;AA4BD,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAChC,MAAM,EACN,eAAe,EACf,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,IAAI,EACJ,KAAgB,EAChB,iBAAyC,EACzC,eAAwC,EACxC,6BAAoC,GACvC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,2CAmG5B"}
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import { Form } from 'react-bootstrap';
4
+ import { buildStableEntries } from '@signalsafe/tree-spec-editor-core';
5
+ import { getIssueSeverityBadgeClass } from '../lib/panelHelpers';
6
+ import PanelHeaderCollapseCarets from '../lib/PanelHeaderCollapseCarets';
7
+ function defaultFormatTimestamp(iso) {
8
+ return new Date(iso).toLocaleString();
9
+ }
10
+ function nodeTypeLabel(tree, nodeId) {
11
+ if (!nodeId || !tree?.nodes[nodeId])
12
+ return '—';
13
+ return tree.nodes[nodeId]?.type || 'unknown';
14
+ }
15
+ function issueHeaderTitle(issue, typeLabel) {
16
+ const idPart = issue.node_id ?? '—';
17
+ return `${idPart}, ${typeLabel}`;
18
+ }
19
+ function issueMatchesQuery(issue, qLower, tree) {
20
+ if (!qLower)
21
+ return true;
22
+ const parts = [
23
+ issue.severity,
24
+ issue.message,
25
+ issue.node_id ?? '',
26
+ issue.choice_id ?? '',
27
+ nodeTypeLabel(tree, issue.node_id),
28
+ ];
29
+ return parts.some((p) => p.toLowerCase().includes(qLower));
30
+ }
31
+ export default function IssuesPanel({ issues, lastValidatedAt, onSelectIssue, issueSearch, onIssueSearchChange, tree, title = 'Issues', searchPlaceholder = 'Search issues\u2026', formatTimestamp = defaultFormatTimestamp, showValidationSummaryInHeader = true, }) {
32
+ const [expanded, setExpanded] = useState(true);
33
+ const qLower = issueSearch.trim().toLowerCase();
34
+ const filteredIssues = useMemo(() => issues.filter((issue) => issueMatchesQuery(issue, qLower, tree)), [issues, qLower, tree]);
35
+ const issueEntries = buildStableEntries(filteredIssues, (issue) => `${issue.severity}-${issue.node_id ?? ''}-${issue.message}`);
36
+ return (_jsxs("div", { className: "card", children: [_jsxs("div", { className: "card-header bg-body-secondary py-2 px-2 d-flex justify-content-between align-items-center gap-2", children: [_jsxs("div", { className: "d-flex align-items-center min-w-0", children: [_jsx("span", { className: "fw-semibold", children: title }), _jsx(PanelHeaderCollapseCarets, { expanded: expanded, onToggle: () => setExpanded((v) => !v) })] }), showValidationSummaryInHeader ? (_jsx("span", { className: "text-muted font-size-12 text-end flex-shrink-0", children: lastValidatedAt ? `Validated ${formatTimestamp(lastValidatedAt)}` : 'Not validated' })) : null] }), _jsx("div", { className: `card-body p-0${expanded ? '' : ' d-none'}`, "aria-hidden": !expanded, children: issues.length === 0 ? (_jsx("div", { className: "list-group list-group-flush", children: _jsx("div", { className: "list-group-item text-muted small py-2 px-3 border-0", children: _jsx("em", { children: "No issues" }) }) })) : (_jsxs(_Fragment, { children: [_jsx(Form.Control, { className: "rounded-0 border-0 border-bottom shadow-none", placeholder: searchPlaceholder, value: issueSearch, onChange: (e) => onIssueSearchChange(e.target.value) }), _jsx("div", { className: "overflow-auto-max-h-320", children: issueEntries.length === 0 ? (_jsx("div", { className: "list-group list-group-flush", children: _jsx("div", { className: "list-group-item text-muted small py-2 px-3 border-0", children: _jsx("em", { children: "No matching issues." }) }) })) : (_jsx("div", { className: "list-group list-group-flush", children: issueEntries.map(({ item: issue, key }) => {
37
+ const typeLabel = nodeTypeLabel(tree, issue.node_id);
38
+ const headerTitle = issueHeaderTitle(issue, typeLabel);
39
+ return (_jsxs("button", { type: "button", className: "list-group-item list-group-item-action px-3 py-2 text-start w-100", onClick: () => onSelectIssue(issue), children: [_jsxs("div", { className: "d-flex w-100 justify-content-between align-items-center gap-2 mb-1", children: [_jsxs("div", { className: "min-w-0 flex-grow-1 text-truncate text-start", title: headerTitle, children: [_jsx("span", { className: "fs-6 fw-semibold text-uppercase", children: issue.node_id ?? '—' }), _jsx("span", { className: "fs-6 text-uppercase text-body-secondary", children: `, ${typeLabel}` })] }), _jsx("span", { className: `badge flex-shrink-0 text-uppercase ${getIssueSeverityBadgeClass(issue.severity)}`, children: issue.severity })] }), _jsx("p", { className: "mb-1 small text-truncate font-monospace", style: { minWidth: 0 }, children: issue.message }), issue.choice_id ? (_jsx("small", { className: "text-body-secondary", children: issue.choice_id })) : null] }, key));
40
+ }) })) })] })) })] }));
41
+ }
@@ -0,0 +1,28 @@
1
+ import { type EditorTree, type GraphSelection } from '@signalsafe/tree-spec-editor-core';
2
+ export interface NodesPanelProps {
3
+ tree: EditorTree;
4
+ nodeSearch: string;
5
+ selection: GraphSelection;
6
+ showMiniMap: boolean;
7
+ onNodeSearchChange: (value: string) => void;
8
+ onNodeSelect: (nodeId: string) => void;
9
+ onShowMiniMapChange: (checked: boolean) => void;
10
+ /** Optional placeholder text for the search input. */
11
+ searchPlaceholder?: string;
12
+ /** Optional tip text shown below the node list. */
13
+ tipText?: string;
14
+ /**
15
+ * When set, shows an add-node control in the card header (grid-style icon button).
16
+ * Typical host: `() => { void editor.actions.addNodeOfType('prompt'); }`.
17
+ */
18
+ onAddNode?: () => void;
19
+ /**
20
+ * When set, each node row shows a delete icon; invoke to remove that node id (e.g.
21
+ * `editor.actions.deleteNodeById`). Disabled when `isPublished` is true.
22
+ */
23
+ onDeleteNode?: (nodeId: string) => void;
24
+ /** When true, node delete controls are disabled (read-only published revision). */
25
+ isPublished?: boolean;
26
+ }
27
+ export default function NodesPanel({ tree, nodeSearch, selection, showMiniMap, onNodeSearchChange, onNodeSelect, onShowMiniMapChange, searchPlaceholder, tipText, onAddNode, onDeleteNode, isPublished, }: Readonly<NodesPanelProps>): import("react/jsx-runtime").JSX.Element;
28
+ //# sourceMappingURL=NodesPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NodesPanel.d.ts","sourceRoot":"","sources":["../../src/panels/NodesPanel.tsx"],"names":[],"mappings":"AAIA,OAAO,EAIH,KAAK,UAAU,EACf,KAAK,cAAc,EACtB,MAAM,mCAAmC,CAAC;AAI3C,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,mBAAmB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAChD,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mFAAmF;IACnF,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AA0BD,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAC/B,IAAI,EACJ,UAAU,EACV,SAAS,EACT,WAAW,EACX,kBAAkB,EAClB,YAAY,EACZ,mBAAmB,EACnB,iBAAwC,EACxC,OAA0B,EAC1B,SAAS,EACT,YAAY,EACZ,WAAmB,GACtB,EAAE,QAAQ,CAAC,eAAe,CAAC,2CAqI3B"}
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Form } from 'react-bootstrap';
4
+ import { GRAPH_SELECTION_KIND, TREE_SPEC_NODE_TYPE_PRESETS, } from '@signalsafe/tree-spec-editor-core';
5
+ import PanelHeaderCollapseCarets from '../lib/PanelHeaderCollapseCarets';
6
+ const DEFAULT_TIP_TEXT = 'Tip: connect choices by dragging the blue dots. Shortcuts: \u2318S save, \u2318\u21E7V validate, \u2318D duplicate, Del delete.';
7
+ const TYPE_SORT_ORDER = new Map(TREE_SPEC_NODE_TYPE_PRESETS.map((t, i) => [t, i]));
8
+ function choiceLine(n) {
9
+ const count = n.choices?.length ?? 0;
10
+ if (count === 0)
11
+ return 'No choices';
12
+ if (count === 1)
13
+ return '1 choice';
14
+ return `${count} choices`;
15
+ }
16
+ function compareNodesByTypeThenId(a, b) {
17
+ const ta = a.type || 'unknown';
18
+ const tb = b.type || 'unknown';
19
+ const oa = TYPE_SORT_ORDER.has(ta) ? TYPE_SORT_ORDER.get(ta) : 1000;
20
+ const ob = TYPE_SORT_ORDER.has(tb) ? TYPE_SORT_ORDER.get(tb) : 1000;
21
+ if (oa !== ob)
22
+ return oa - ob;
23
+ if (ta !== tb)
24
+ return ta.localeCompare(tb);
25
+ return a.id.localeCompare(b.id);
26
+ }
27
+ export default function NodesPanel({ tree, nodeSearch, selection, showMiniMap, onNodeSearchChange, onNodeSelect, onShowMiniMapChange, searchPlaceholder = 'Search nodes\u2026', tipText = DEFAULT_TIP_TEXT, onAddNode, onDeleteNode, isPublished = false, }) {
28
+ const [expanded, setExpanded] = useState(true);
29
+ const filteredNodes = Object.values(tree.nodes)
30
+ .filter((n) => {
31
+ const q = nodeSearch.trim().toLowerCase();
32
+ if (!q)
33
+ return true;
34
+ const hitPrompt = (n.prompt || '').toLowerCase().includes(q);
35
+ const hitId = n.id.toLowerCase().includes(q);
36
+ const hitType = (n.type || '').toLowerCase().includes(q);
37
+ const hitChoices = (n.choices ?? []).some((c) => (c.label || '').toLowerCase().includes(q) || c.id.toLowerCase().includes(q));
38
+ return hitPrompt || hitId || hitType || hitChoices;
39
+ })
40
+ .sort(compareNodesByTypeThenId);
41
+ return (_jsxs("div", { className: "card mt-3", children: [_jsxs("div", { className: "card-header bg-body-secondary py-2 px-2 d-flex justify-content-between align-items-center gap-2", children: [_jsxs("div", { className: "d-flex align-items-center min-w-0", children: [_jsx("span", { className: "fw-semibold", children: "Nodes" }), _jsx(PanelHeaderCollapseCarets, { expanded: expanded, onToggle: () => setExpanded((v) => !v) })] }), onAddNode ? (_jsx("button", { type: "button", className: "btn p-0 border-0 bg-transparent flex-shrink-0", "aria-label": "Add node", title: "Add node", onClick: onAddNode, children: _jsx("i", { className: "bi bi-plus-lg text-primary action-icon cursor-pointer", "aria-hidden": true }) })) : null] }), _jsxs("div", { className: expanded ? undefined : 'd-none', "aria-hidden": !expanded, children: [_jsx(Form.Control, { className: "rounded-0 border-0 border-bottom shadow-none", placeholder: searchPlaceholder, value: nodeSearch, onChange: (e) => onNodeSearchChange(e.target.value) }), _jsx("div", { className: "overflow-auto-max-h-320", children: filteredNodes.length === 0 ? (_jsx("div", { className: "list-group list-group-flush", children: _jsx("div", { className: "list-group-item text-muted small py-2 px-3", children: "No matching nodes." }) })) : (_jsx("div", { className: "list-group list-group-flush", children: filteredNodes.map((n) => {
42
+ const isSelected = selection.kind === GRAPH_SELECTION_KIND.NODE && selection.id === n.id;
43
+ const typeLabel = n.type || 'unknown';
44
+ const mutedTop = isSelected ? '' : ' text-body-secondary';
45
+ const mutedMeta = isSelected ? 'opacity-75' : 'text-body-secondary';
46
+ return (_jsx("div", { className: `list-group-item px-3 py-2${isSelected ? ' active' : ''}`, "aria-current": isSelected ? 'true' : undefined, children: _jsxs("div", { className: "d-flex w-100 justify-content-between align-items-start gap-2", children: [_jsxs("button", { type: "button", className: "btn p-0 border-0 bg-transparent flex-grow-1 text-start", onClick: () => onNodeSelect(n.id), children: [_jsx("div", { className: "d-flex w-100 justify-content-between align-items-center gap-2 mb-1", children: _jsxs("div", { className: "min-w-0 flex-grow-1 text-truncate text-start", title: `${n.id}, ${typeLabel}`, children: [_jsx("span", { className: `fs-6 fw-semibold text-uppercase${mutedTop}`, children: n.id }), _jsx("span", { className: `fs-6 text-uppercase ${isSelected ? 'opacity-75' : 'text-body-secondary'}`, children: `, ${typeLabel}` })] }) }), _jsx("p", { className: "mb-1 small text-truncate", style: { minWidth: 0 }, children: n.prompt || '(empty)' }), _jsx("small", { className: mutedMeta, children: choiceLine(n) })] }), onDeleteNode ? (_jsx("button", { type: "button", className: "btn p-0 border-0 bg-transparent flex-shrink-0", "aria-label": `Delete node ${n.id}`, title: "Delete node", disabled: isPublished, onClick: () => onDeleteNode(n.id), children: _jsx("i", { className: `bi bi-trash action-icon cursor-pointer${isPublished ? ' text-secondary opacity-50' : ' text-danger'}`, "aria-hidden": true }) })) : null] }) }, n.id));
47
+ }) })) }), _jsx("div", { className: "card-footer p-2 border-top", children: _jsxs("div", { className: "d-flex justify-content-between align-items-center", children: [_jsx("div", { className: "text-muted font-size-12", children: tipText }), _jsx(Form.Check, { type: "switch", id: "showMiniMapToggle", label: "Mini-map", checked: showMiniMap, onChange: (e) => onShowMiniMapChange(e.target.checked), className: "font-size-12" })] }) })] })] }));
48
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Compact read-only display of the currently selected transition.
3
+ * Returns `null` when no edge is selected, so the host can render it unconditionally.
4
+ */
5
+ import type { ReactNode } from 'react';
6
+ import type { EditorTransition } from '@signalsafe/tree-spec-editor-core';
7
+ export interface SelectedEdgePanelProps {
8
+ selectedEdge: EditorTransition | null;
9
+ /** Override the card title (default "Selected Edge"). */
10
+ title?: string;
11
+ /**
12
+ * Optional custom renderer for the edge details line. Defaults to
13
+ * `from \u00b7 choice \u2192 to`.
14
+ */
15
+ renderDetails?: (edge: EditorTransition) => ReactNode;
16
+ }
17
+ export default function SelectedEdgePanel({ selectedEdge, title, renderDetails, }: Readonly<SelectedEdgePanelProps>): import("react/jsx-runtime").JSX.Element | null;
18
+ //# sourceMappingURL=SelectedEdgePanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectedEdgePanel.d.ts","sourceRoot":"","sources":["../../src/panels/SelectedEdgePanel.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAE1E,MAAM,WAAW,sBAAsB;IACnC,YAAY,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACtC,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,SAAS,CAAC;CACzD;AAUD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EACtC,YAAY,EACZ,KAAuB,EACvB,aAAoC,GACvC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,kDAWlC"}
@@ -0,0 +1,9 @@
1
+ import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ function defaultRenderDetails(edge) {
3
+ return (_jsxs(_Fragment, { children: [edge.fromNodeId, " \u00B7 ", edge.fromChoiceId, " \u2192 ", edge.toNodeId] }));
4
+ }
5
+ export default function SelectedEdgePanel({ selectedEdge, title = 'Selected Edge', renderDetails = defaultRenderDetails, }) {
6
+ if (!selectedEdge)
7
+ return null;
8
+ return (_jsx("div", { className: "card mt-3", children: _jsxs("div", { className: "card-body", children: [_jsx("div", { className: "fw-bold", children: title }), _jsx("div", { className: "text-muted font-size-12", children: renderDetails(selectedEdge) })] }) }));
9
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Data-driven toolbar for the TreeSpec graph editor (and similar editor pages).
3
+ *
4
+ * Hosts pass an array of `ToolbarItem`s describing the visual elements they want in
5
+ * the toolbar — buttons, dropdowns, badges, plain text, or arbitrary custom render
6
+ * functions. The package owns layout (wrap, gap, alignment) and Bootstrap chrome;
7
+ * the host owns every label, disabled rule, adapter-gated visibility, and callback
8
+ * (encoded as closures inside each item).
9
+ *
10
+ * The `custom` item kind exists for one-offs like router-aware links — that way
11
+ * the package stays free of `react-router` (or any other) routing concerns.
12
+ */
13
+ import { type ReactNode } from 'react';
14
+ /**
15
+ * Discriminator values used by the `kind` field on every {@link ToolbarItem}.
16
+ * Use these constants instead of bare string literals (e.g.
17
+ * `{ kind: TOOLBAR_ITEM_KIND.BUTTON, ... }`).
18
+ */
19
+ export declare const TOOLBAR_ITEM_KIND: {
20
+ readonly BUTTON: "button";
21
+ readonly DROPDOWN: "dropdown";
22
+ readonly BADGE: "badge";
23
+ readonly TEXT: "text";
24
+ readonly CUSTOM: "custom";
25
+ };
26
+ /** Allowed toolbar item kinds. Derived from {@link TOOLBAR_ITEM_KIND}. */
27
+ export type ToolbarItemKind = (typeof TOOLBAR_ITEM_KIND)[keyof typeof TOOLBAR_ITEM_KIND];
28
+ export interface ToolbarButtonItem {
29
+ kind: typeof TOOLBAR_ITEM_KIND.BUTTON;
30
+ /** Optional stable id used as the React key (otherwise list index is used). */
31
+ id?: string;
32
+ label: ReactNode;
33
+ onClick: () => void;
34
+ /** Bootstrap variant. Default: `"outline-secondary"`. */
35
+ variant?: string;
36
+ disabled?: boolean;
37
+ title?: string;
38
+ className?: string;
39
+ }
40
+ /** Single row inside a `ToolbarDropdownItem`. */
41
+ export interface ToolbarDropdownEntry {
42
+ /** Optional stable id used as the React key (otherwise list index is used). */
43
+ id?: string;
44
+ label: ReactNode;
45
+ onClick?: () => void;
46
+ disabled?: boolean;
47
+ /** When true, render a `<Dropdown.Divider />` and ignore label/onClick. */
48
+ divider?: boolean;
49
+ }
50
+ export interface ToolbarDropdownItem {
51
+ kind: typeof TOOLBAR_ITEM_KIND.DROPDOWN;
52
+ id?: string;
53
+ label: ReactNode;
54
+ entries: ToolbarDropdownEntry[];
55
+ /** Bootstrap variant for the toggle. Default: `"outline-secondary"`. */
56
+ variant?: string;
57
+ disabled?: boolean;
58
+ className?: string;
59
+ }
60
+ export interface ToolbarBadgeItem {
61
+ kind: typeof TOOLBAR_ITEM_KIND.BADGE;
62
+ id?: string;
63
+ label: ReactNode;
64
+ /** Bootstrap badge variant (e.g. `"success"`, `"secondary"`). Default: `"secondary"`. */
65
+ variant?: string;
66
+ className?: string;
67
+ }
68
+ export interface ToolbarTextItem {
69
+ kind: typeof TOOLBAR_ITEM_KIND.TEXT;
70
+ id?: string;
71
+ content: ReactNode;
72
+ /** Override the default `"ms-2 text-muted font-size-12"` styling. */
73
+ className?: string;
74
+ }
75
+ export interface ToolbarCustomItem {
76
+ kind: typeof TOOLBAR_ITEM_KIND.CUSTOM;
77
+ id?: string;
78
+ /** Render any node. Use for project-specific elements (e.g. router-aware links). */
79
+ render: () => ReactNode;
80
+ }
81
+ export type ToolbarItem = ToolbarButtonItem | ToolbarDropdownItem | ToolbarBadgeItem | ToolbarTextItem | ToolbarCustomItem;
82
+ export interface ToolbarPanelProps {
83
+ items: ToolbarItem[];
84
+ /** Override the outer wrapper className. Default: `"d-flex gap-2 align-items-center flex-wrap"`. */
85
+ className?: string;
86
+ }
87
+ export default function ToolbarPanel({ items, className, }: Readonly<ToolbarPanelProps>): import("react/jsx-runtime").JSX.Element;
88
+ //# sourceMappingURL=ToolbarPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToolbarPanel.d.ts","sourceRoot":"","sources":["../../src/panels/ToolbarPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGjD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB;;;;;;CAMpB,CAAC;AAEX,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;AAEzF,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,OAAO,iBAAiB,CAAC,MAAM,CAAC;IACtC,+EAA+E;IAC/E,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACjC,+EAA+E;IAC/E,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,OAAO,iBAAiB,CAAC,QAAQ,CAAC;IACxC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,OAAO,iBAAiB,CAAC,KAAK,CAAC;IACrC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,yFAAyF;IACzF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,OAAO,iBAAiB,CAAC,IAAI,CAAC;IACpC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,SAAS,CAAC;IACnB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,OAAO,iBAAiB,CAAC,MAAM,CAAC;IACtC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,oFAAoF;IACpF,MAAM,EAAE,MAAM,SAAS,CAAC;CAC3B;AAED,MAAM,MAAM,WAAW,GACjB,iBAAiB,GACjB,mBAAmB,GACnB,gBAAgB,GAChB,eAAe,GACf,iBAAiB,CAAC;AAExB,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,oGAAoG;IACpG,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAyED,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACjC,KAAK,EACL,SAAiC,GACpC,EAAE,QAAQ,CAAC,iBAAiB,CAAC,2CAQ7B"}
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Data-driven toolbar for the TreeSpec graph editor (and similar editor pages).
4
+ *
5
+ * Hosts pass an array of `ToolbarItem`s describing the visual elements they want in
6
+ * the toolbar — buttons, dropdowns, badges, plain text, or arbitrary custom render
7
+ * functions. The package owns layout (wrap, gap, alignment) and Bootstrap chrome;
8
+ * the host owns every label, disabled rule, adapter-gated visibility, and callback
9
+ * (encoded as closures inside each item).
10
+ *
11
+ * The `custom` item kind exists for one-offs like router-aware links — that way
12
+ * the package stays free of `react-router` (or any other) routing concerns.
13
+ */
14
+ import { Fragment } from 'react';
15
+ import { Button, Dropdown } from 'react-bootstrap';
16
+ /**
17
+ * Discriminator values used by the `kind` field on every {@link ToolbarItem}.
18
+ * Use these constants instead of bare string literals (e.g.
19
+ * `{ kind: TOOLBAR_ITEM_KIND.BUTTON, ... }`).
20
+ */
21
+ export const TOOLBAR_ITEM_KIND = {
22
+ BUTTON: 'button',
23
+ DROPDOWN: 'dropdown',
24
+ BADGE: 'badge',
25
+ TEXT: 'text',
26
+ CUSTOM: 'custom',
27
+ };
28
+ const DEFAULT_BUTTON_VARIANT = 'outline-secondary';
29
+ const DEFAULT_DROPDOWN_VARIANT = 'outline-secondary';
30
+ const DEFAULT_BADGE_VARIANT = 'secondary';
31
+ const DEFAULT_TEXT_CLASS = 'ms-2 text-muted font-size-12';
32
+ const DEFAULT_WRAPPER_CLASS = 'd-flex gap-2 align-items-center flex-wrap';
33
+ function renderItem(item) {
34
+ switch (item.kind) {
35
+ case TOOLBAR_ITEM_KIND.BUTTON:
36
+ return (_jsx(Button, { variant: item.variant ?? DEFAULT_BUTTON_VARIANT, onClick: item.onClick, disabled: item.disabled, title: item.title, className: item.className, children: item.label }));
37
+ case TOOLBAR_ITEM_KIND.DROPDOWN:
38
+ return (_jsxs(Dropdown, { className: item.className ?? 'd-inline', children: [_jsx(Dropdown.Toggle, { variant: item.variant ?? DEFAULT_DROPDOWN_VARIANT, disabled: item.disabled, children: item.label }), _jsx(Dropdown.Menu, { children: item.entries.map((entry, idx) => {
39
+ const entryKey = entry.id ?? `entry-${idx}`;
40
+ if (entry.divider) {
41
+ return _jsx(Dropdown.Divider, {}, entryKey);
42
+ }
43
+ return (_jsx(Dropdown.Item, { onClick: entry.onClick, disabled: entry.disabled, children: entry.label }, entryKey));
44
+ }) })] }));
45
+ case TOOLBAR_ITEM_KIND.BADGE:
46
+ return (_jsx("span", { className: item.className ?? `badge bg-${item.variant ?? DEFAULT_BADGE_VARIANT}`, children: item.label }));
47
+ case TOOLBAR_ITEM_KIND.TEXT:
48
+ return (_jsx("span", { className: item.className ?? DEFAULT_TEXT_CLASS, children: item.content }));
49
+ case TOOLBAR_ITEM_KIND.CUSTOM:
50
+ return item.render();
51
+ default: {
52
+ const exhaustive = item;
53
+ return exhaustive;
54
+ }
55
+ }
56
+ }
57
+ export default function ToolbarPanel({ items, className = DEFAULT_WRAPPER_CLASS, }) {
58
+ return (_jsx("div", { className: className, children: items.map((item, idx) => (_jsx(Fragment, { children: renderItem(item) }, item.id ?? `item-${idx}`))) }));
59
+ }