@signalsafe/tree-spec-editor 0.2.1 → 0.2.2

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -59
  3. package/dist/index.d.ts +10 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +5 -1
  6. package/dist/lib/PanelHeaderCollapseCarets.d.ts +1 -1
  7. package/dist/lib/PanelHeaderCollapseCarets.d.ts.map +1 -1
  8. package/dist/lib/buildDefaultToolbarSpec.d.ts +3 -79
  9. package/dist/lib/buildDefaultToolbarSpec.d.ts.map +1 -1
  10. package/dist/lib/buildDefaultToolbarSpec.js +9 -155
  11. package/dist/lib/panelNodeLink.d.ts +13 -0
  12. package/dist/lib/panelNodeLink.d.ts.map +1 -0
  13. package/dist/lib/panelNodeLink.js +12 -0
  14. package/dist/lib/selectionStyles.d.ts +9 -0
  15. package/dist/lib/selectionStyles.d.ts.map +1 -0
  16. package/dist/lib/selectionStyles.js +8 -0
  17. package/dist/lib/toolbar/defaultToolbarTypes.d.ts +89 -0
  18. package/dist/lib/toolbar/defaultToolbarTypes.d.ts.map +1 -0
  19. package/dist/lib/toolbar/defaultToolbarTypes.js +1 -0
  20. package/dist/lib/toolbar/toolbarSectionBuilders.d.ts +10 -0
  21. package/dist/lib/toolbar/toolbarSectionBuilders.d.ts.map +1 -0
  22. package/dist/lib/toolbar/toolbarSectionBuilders.js +153 -0
  23. package/dist/modals/AuditLogModal.d.ts +1 -1
  24. package/dist/modals/AuditLogModal.d.ts.map +1 -1
  25. package/dist/modals/AuditLogModal.js +1 -1
  26. package/dist/modals/DraftHistoryModal.d.ts +1 -1
  27. package/dist/modals/DraftHistoryModal.d.ts.map +1 -1
  28. package/dist/modals/DraftHistoryModal.js +1 -1
  29. package/dist/modals/PublishReviewModal.d.ts +1 -1
  30. package/dist/modals/PublishReviewModal.d.ts.map +1 -1
  31. package/dist/panels/AdvancedJsonPanel.d.ts +1 -1
  32. package/dist/panels/AdvancedJsonPanel.d.ts.map +1 -1
  33. package/dist/panels/AppearancePanel.d.ts +26 -0
  34. package/dist/panels/AppearancePanel.d.ts.map +1 -0
  35. package/dist/panels/AppearancePanel.js +76 -0
  36. package/dist/panels/ChoiceEdgeAppearanceFields.d.ts +10 -0
  37. package/dist/panels/ChoiceEdgeAppearanceFields.d.ts.map +1 -0
  38. package/dist/panels/ChoiceEdgeAppearanceFields.js +13 -0
  39. package/dist/panels/ChoiceTemplatesPanel.d.ts +27 -0
  40. package/dist/panels/ChoiceTemplatesPanel.d.ts.map +1 -0
  41. package/dist/panels/ChoiceTemplatesPanel.js +9 -0
  42. package/dist/panels/GraphEditorInfoPanel.d.ts +5 -2
  43. package/dist/panels/GraphEditorInfoPanel.d.ts.map +1 -1
  44. package/dist/panels/GraphEditorInfoPanel.js +6 -6
  45. package/dist/panels/InspectorPanel.d.ts +3 -78
  46. package/dist/panels/InspectorPanel.d.ts.map +1 -1
  47. package/dist/panels/InspectorPanel.js +20 -60
  48. package/dist/panels/IssuesPanel.d.ts +6 -2
  49. package/dist/panels/IssuesPanel.d.ts.map +1 -1
  50. package/dist/panels/IssuesPanel.js +15 -3
  51. package/dist/panels/NodesPanel.d.ts +3 -1
  52. package/dist/panels/NodesPanel.d.ts.map +1 -1
  53. package/dist/panels/NodesPanel.js +12 -6
  54. package/dist/panels/SelectedEdgePanel.d.ts +10 -3
  55. package/dist/panels/SelectedEdgePanel.d.ts.map +1 -1
  56. package/dist/panels/SelectedEdgePanel.js +5 -6
  57. package/dist/panels/ToolbarPanel.d.ts +1 -1
  58. package/dist/panels/ToolbarPanel.d.ts.map +1 -1
  59. package/dist/panels/ToolbarPanel.js +2 -4
  60. package/dist/panels/inspector/ChoiceEditorCard.d.ts +28 -0
  61. package/dist/panels/inspector/ChoiceEditorCard.d.ts.map +1 -0
  62. package/dist/panels/inspector/ChoiceEditorCard.js +36 -0
  63. package/dist/panels/inspector/ChoiceEditorList.d.ts +25 -0
  64. package/dist/panels/inspector/ChoiceEditorList.d.ts.map +1 -0
  65. package/dist/panels/inspector/ChoiceEditorList.js +10 -0
  66. package/dist/panels/inspector/NodeTypeField.d.ts +8 -0
  67. package/dist/panels/inspector/NodeTypeField.d.ts.map +1 -0
  68. package/dist/panels/inspector/NodeTypeField.js +12 -0
  69. package/dist/panels/inspector/RequiredNodeCard.d.ts +14 -0
  70. package/dist/panels/inspector/RequiredNodeCard.d.ts.map +1 -0
  71. package/dist/panels/inspector/RequiredNodeCard.js +16 -0
  72. package/dist/panels/inspector/choiceActionIconProps.d.ts +12 -0
  73. package/dist/panels/inspector/choiceActionIconProps.d.ts.map +1 -0
  74. package/dist/panels/inspector/choiceActionIconProps.js +26 -0
  75. package/dist/panels/inspector/types.d.ts +77 -0
  76. package/dist/panels/inspector/types.d.ts.map +1 -0
  77. package/dist/panels/inspector/types.js +11 -0
  78. package/package.json +83 -65
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { getTransition, } from '@signalsafe/tree-spec-editor-core';
4
+ import PanelHeaderCollapseCarets from '../lib/PanelHeaderCollapseCarets';
5
+ import { PanelNodeLink } from '../lib/panelNodeLink';
6
+ import ChoiceEdgeAppearanceFields from './ChoiceEdgeAppearanceFields';
7
+ function resolveActiveChoiceId(choices, selectedEdge, focusChoiceId) {
8
+ if (selectedEdge)
9
+ return selectedEdge.fromChoiceId;
10
+ const ids = (choices ?? []).map((choice) => choice.id);
11
+ if (focusChoiceId && ids.includes(focusChoiceId))
12
+ return focusChoiceId;
13
+ return null;
14
+ }
15
+ const EDGE_APPEARANCE_HELPER = 'Canvas only (not shown in preview or training)';
16
+ function EdgeContextBreadcrumb({ fromNodeId, choiceId, toNodeId, onNodeSelect, }) {
17
+ return (_jsxs("div", { className: "text-muted font-size-12 mb-3", children: [_jsx(PanelNodeLink, { nodeId: fromNodeId, onNodeSelect: onNodeSelect }), ' · ', choiceId, ' → ', toNodeId ? _jsx(PanelNodeLink, { nodeId: toNodeId, onNodeSelect: onNodeSelect }) : '(not connected)'] }));
18
+ }
19
+ function EdgeAppearanceSection({ choice, isPublished, onPatch, }) {
20
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-muted small mb-2", children: EDGE_APPEARANCE_HELPER }), _jsx("div", { className: "small fw-semibold mb-2", children: "Edge appearance" }), _jsx(ChoiceEdgeAppearanceFields, { variant: "embedded", choice: choice, isPublished: isPublished, onPatch: onPatch })] }));
21
+ }
22
+ function appearancePanelBodyClassName(hasSelection, expanded) {
23
+ if (hasSelection && !expanded) {
24
+ return 'card-body d-none';
25
+ }
26
+ return 'card-body';
27
+ }
28
+ function deriveAppearancePanelViewState({ tree, selectedNode, selectedEdge, focusChoiceId, onUpdateChoiceEdgeHints, }) {
29
+ const edgeFocused = Boolean(selectedEdge);
30
+ const contextNode = selectedNode ??
31
+ (selectedEdge ? tree.nodes[selectedEdge.fromNodeId] ?? null : null);
32
+ const choices = contextNode?.choices ?? [];
33
+ const activeChoiceId = resolveActiveChoiceId(choices, selectedEdge, focusChoiceId);
34
+ const activeChoice = activeChoiceId
35
+ ? choices.find((choice) => choice.id === activeChoiceId) ?? null
36
+ : null;
37
+ const choiceFocused = Boolean(focusChoiceId &&
38
+ !edgeFocused &&
39
+ selectedNode &&
40
+ choices.some((choice) => choice.id === focusChoiceId));
41
+ const edgeAppearanceMode = edgeFocused || choiceFocused;
42
+ const showEdgeAppearance = Boolean(onUpdateChoiceEdgeHints && contextNode && activeChoice && edgeAppearanceMode);
43
+ const showNodeAppearance = Boolean(contextNode && !edgeAppearanceMode);
44
+ const hasSelection = Boolean(selectedNode || selectedEdge);
45
+ const edgeFromNodeId = selectedEdge?.fromNodeId ?? (choiceFocused ? selectedNode?.id ?? null : null);
46
+ const edgeToNodeId = selectedEdge?.toNodeId ??
47
+ (edgeFromNodeId && activeChoiceId
48
+ ? getTransition(tree, edgeFromNodeId, activeChoiceId)?.toNodeId ?? null
49
+ : null);
50
+ return {
51
+ contextNode,
52
+ activeChoiceId,
53
+ activeChoice,
54
+ showEdgeAppearance,
55
+ showNodeAppearance,
56
+ hasSelection,
57
+ edgeFromNodeId,
58
+ edgeToNodeId,
59
+ };
60
+ }
61
+ export default function AppearancePanel({ tree, selectedNode, selectedEdge = null, focusChoiceId = null, isPublished = false, onUpdateSelectedNode, renderAppearanceFields, onUpdateChoiceEdgeHints, onNodeSelect, title = 'Appearance', emptyStateText = 'Select a node or edge to edit appearance.', helperText = 'Canvas node only (not shown in preview or training)', }) {
62
+ const [expanded, setExpanded] = useState(true);
63
+ const { contextNode, activeChoiceId, activeChoice, showEdgeAppearance, showNodeAppearance, hasSelection, edgeFromNodeId, edgeToNodeId, } = deriveAppearancePanelViewState({
64
+ tree,
65
+ selectedNode,
66
+ selectedEdge,
67
+ focusChoiceId,
68
+ onUpdateChoiceEdgeHints,
69
+ });
70
+ return (_jsxs("div", { className: "card mt-3", children: [_jsxs("div", { className: "card-header bg-body-secondary py-2 px-2 d-flex align-items-center min-w-0", children: [_jsx("span", { className: "fw-semibold", children: title }), hasSelection ? (_jsx(PanelHeaderCollapseCarets, { expanded: expanded, onToggle: () => setExpanded((v) => !v) })) : null] }), _jsx("div", { className: appearancePanelBodyClassName(hasSelection, expanded), "aria-hidden": Boolean(hasSelection && !expanded), children: hasSelection && contextNode ? (_jsxs(_Fragment, { children: [showNodeAppearance ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-muted small mb-2", children: helperText }), renderAppearanceFields({
71
+ tree,
72
+ node: contextNode,
73
+ isPublished,
74
+ onUpdateNode: onUpdateSelectedNode,
75
+ })] })) : null, showEdgeAppearance && edgeFromNodeId && activeChoiceId && activeChoice ? (_jsxs(_Fragment, { children: [_jsx(EdgeContextBreadcrumb, { fromNodeId: edgeFromNodeId, choiceId: activeChoiceId, toNodeId: edgeToNodeId, onNodeSelect: onNodeSelect }), _jsx(EdgeAppearanceSection, { choice: activeChoice, isPublished: isPublished, onPatch: (patch) => onUpdateChoiceEdgeHints(contextNode.id, activeChoice.id, patch) })] })) : null] })) : (_jsx("div", { className: "text-muted", children: _jsx("em", { children: emptyStateText }) })) })] }));
76
+ }
@@ -0,0 +1,10 @@
1
+ import { type ChoiceEdgeHints, type EditorChoice } from '@signalsafe/tree-spec-editor-core';
2
+ export interface ChoiceEdgeAppearanceFieldsProps {
3
+ choice: EditorChoice;
4
+ isPublished?: boolean;
5
+ onPatch: (patch: Partial<ChoiceEdgeHints>) => void;
6
+ /** Standalone adds a section border/title for embedding under choice cards. */
7
+ variant?: 'standalone' | 'embedded';
8
+ }
9
+ export default function ChoiceEdgeAppearanceFields({ choice, isPublished, onPatch, variant, }: Readonly<ChoiceEdgeAppearanceFieldsProps>): import("react").JSX.Element;
10
+ //# sourceMappingURL=ChoiceEdgeAppearanceFields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChoiceEdgeAppearanceFields.d.ts","sourceRoot":"","sources":["../../src/panels/ChoiceEdgeAppearanceFields.tsx"],"names":[],"mappings":"AAGA,OAAO,EAIH,KAAK,eAAe,EACpB,KAAK,YAAY,EACpB,MAAM,mCAAmC,CAAC;AAE3C,MAAM,WAAW,+BAA+B;IAC5C,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC;IACnD,+EAA+E;IAC/E,OAAO,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;CACvC;AAED,MAAM,CAAC,OAAO,UAAU,0BAA0B,CAAC,EAC/C,MAAM,EACN,WAAmB,EACnB,OAAO,EACP,OAAsB,GACzB,EAAE,QAAQ,CAAC,+BAA+B,CAAC,+BAqE3C"}
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Form } from 'react-bootstrap';
3
+ import { EDITOR_EDGE_TYPE_OPTIONS, getChoiceEdgeHints, resolveEdgeStrokeColorForDisplay, } from '@signalsafe/tree-spec-editor-core';
4
+ export default function ChoiceEdgeAppearanceFields({ choice, isPublished = false, onPatch, variant = 'standalone', }) {
5
+ const hints = getChoiceEdgeHints(choice);
6
+ const fields = (_jsxs(_Fragment, { children: [_jsx(Form.Check, { type: "switch", id: `choice-edge-show-label-${choice.id}`, className: "mb-2", label: "Show label on canvas", checked: hints.showLabel !== false, disabled: isPublished, onChange: (event) => onPatch({ showLabel: event.target.checked }) }), _jsxs(Form.Group, { className: "mb-2", children: [_jsx(Form.Label, { className: "small mb-1", children: "Stroke color" }), _jsxs("div", { className: "d-flex gap-2 align-items-center", children: [_jsx(Form.Control, { type: "color", className: "graph-editor-appearance-color-input", value: resolveEdgeStrokeColorForDisplay(choice), disabled: isPublished, onChange: (event) => onPatch({ strokeColor: event.target.value }) }), _jsx("button", { type: "button", className: "btn btn-sm btn-outline-secondary", disabled: isPublished || hints.strokeColor === undefined, onClick: () => onPatch({ strokeColor: undefined }), children: "Reset" })] })] }), _jsxs(Form.Group, { className: "mb-0", children: [_jsx(Form.Label, { className: "small mb-1", children: "Edge type" }), _jsx(Form.Select, { value: hints.edgeType ?? 'default', disabled: isPublished, onChange: (event) => onPatch({
7
+ edgeType: event.target.value,
8
+ }), children: EDITOR_EDGE_TYPE_OPTIONS.map((option) => (_jsx("option", { value: option.value ?? 'default', children: option.label }, option.value ?? 'default'))) })] })] }));
9
+ if (variant === 'embedded') {
10
+ return fields;
11
+ }
12
+ return (_jsxs("div", { className: "mt-3 pt-2 border-top", children: [_jsx("div", { className: "small fw-semibold mb-2", children: "Edge appearance (canvas only)" }), fields] }));
13
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Preset choice bundles for quick insertion into a node.
3
+ * Exported for custom host layouts; not yet wired in the reference graph editor page.
4
+ * Pair with `appendChoiceTemplate` from `@signalsafe/tree-spec-editor-core`.
5
+ */
6
+ /** Host-provided preset choice bundle (optional stable ids per choice). */
7
+ export type ChoiceTemplateItem = {
8
+ id: string;
9
+ label: string;
10
+ choices: ReadonlyArray<{
11
+ label: string;
12
+ id?: string;
13
+ }>;
14
+ };
15
+ /** Host-provided single choice preset (stable id + default label). */
16
+ export type ChoicePresetItem = {
17
+ id: string;
18
+ label: string;
19
+ };
20
+ export interface ChoiceTemplatesPanelProps {
21
+ templates: ReadonlyArray<ChoiceTemplateItem>;
22
+ selectedNodeId: string | null;
23
+ isPublished?: boolean;
24
+ onApplyTemplate: (template: ChoiceTemplateItem) => void;
25
+ }
26
+ export default function ChoiceTemplatesPanel({ templates, selectedNodeId, isPublished, onApplyTemplate, }: Readonly<ChoiceTemplatesPanelProps>): import("react").JSX.Element;
27
+ //# sourceMappingURL=ChoiceTemplatesPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChoiceTemplatesPanel.d.ts","sourceRoot":"","sources":["../../src/panels/ChoiceTemplatesPanel.tsx"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,2EAA2E;AAC3E,MAAM,MAAM,kBAAkB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1D,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,gBAAgB,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACtC,SAAS,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;IAC7C,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,eAAe,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC3D;AAED,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAAC,EACzC,SAAS,EACT,cAAc,EACd,WAAmB,EACnB,eAAe,GAClB,EAAE,QAAQ,CAAC,yBAAyB,CAAC,+BAiCrC"}
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Button, Card } from 'react-bootstrap';
4
+ import PanelHeaderCollapseCarets from '../lib/PanelHeaderCollapseCarets';
5
+ export default function ChoiceTemplatesPanel({ templates, selectedNodeId, isPublished = false, onApplyTemplate, }) {
6
+ const [expanded, setExpanded] = useState(true);
7
+ const disabled = isPublished || !selectedNodeId;
8
+ return (_jsxs(Card, { className: "mb-3", children: [_jsxs(Card.Header, { className: "d-flex align-items-center justify-content-between py-2", children: [_jsx("span", { className: "fw-semibold", children: "Choice templates" }), _jsx(PanelHeaderCollapseCarets, { expanded: expanded, onToggle: () => setExpanded((value) => !value) })] }), _jsx("div", { className: `card-body py-2${expanded ? '' : ' d-none'}`, "aria-hidden": !expanded, children: selectedNodeId ? (_jsx("div", { className: "d-flex flex-wrap gap-2", children: templates.map((template) => (_jsx(Button, { type: "button", size: "sm", variant: "outline-secondary", disabled: disabled, title: template.choices.map((choice) => choice.label).join(' · '), onClick: () => onApplyTemplate(template), children: template.label }, template.id))) })) : (_jsx("p", { className: "text-muted small mb-0", children: "Select a node to add preset choices." })) })] }));
9
+ }
@@ -1,4 +1,4 @@
1
- import { type AutosaveStatus } from '@signalsafe/tree-spec-editor-core';
1
+ import { type AutosaveStatus, type GraphEditorEdgeType } from '@signalsafe/tree-spec-editor-core';
2
2
  export interface GraphEditorInfoPanelProps {
3
3
  scenarioId: string;
4
4
  versionId: string;
@@ -15,6 +15,9 @@ export interface GraphEditorInfoPanelProps {
15
15
  title?: string;
16
16
  /** Optional formatter for ISO timestamps. Defaults to `new Date(iso).toLocaleString()`. */
17
17
  formatTimestamp?: (iso: string) => string;
18
+ /** Scenario-level default edge routing when a choice uses Default. */
19
+ defaultEdgeType?: GraphEditorEdgeType;
20
+ onUpdateDefaultEdgeType?: (edgeType: GraphEditorEdgeType) => void;
18
21
  }
19
- export default function GraphEditorInfoPanel({ scenarioId, versionId, name, createdAt, updatedAt, isPublished, autosaveStatus, lastValidatedAt, title, formatTimestamp, }: Readonly<GraphEditorInfoPanelProps>): import("react/jsx-runtime").JSX.Element;
22
+ export default function GraphEditorInfoPanel({ scenarioId, versionId, name, createdAt, updatedAt, isPublished, autosaveStatus, lastValidatedAt, title, formatTimestamp, defaultEdgeType, onUpdateDefaultEdgeType, }: Readonly<GraphEditorInfoPanelProps>): import("react").JSX.Element;
20
23
  //# sourceMappingURL=GraphEditorInfoPanel.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"GraphEditorInfoPanel.d.ts","sourceRoot":"","sources":["../../src/panels/GraphEditorInfoPanel.tsx"],"names":[],"mappings":"AAKA,OAAO,EAGH,KAAK,cAAc,EACnB,KAAK,mBAAmB,EAC3B,MAAM,mCAAmC,CAAC;AAE3C,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;IAC1C,sEAAsE;IACtE,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,uBAAuB,CAAC,EAAE,CAAC,QAAQ,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACrE;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,EACxC,eAA8B,EAC9B,uBAAuB,GAC1B,EAAE,QAAQ,CAAC,yBAAyB,CAAC,+BAsFrC"}
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
- import { Badge, Table } from 'react-bootstrap';
3
+ import { Badge, Form, Table } from 'react-bootstrap';
4
4
  import PanelHeaderCollapseCarets from '../lib/PanelHeaderCollapseCarets';
5
- import { getAutosaveStatusLabel } from '@signalsafe/tree-spec-editor-core';
5
+ import { EDITOR_EDGE_TYPE_OPTIONS, getAutosaveStatusLabel, } from '@signalsafe/tree-spec-editor-core';
6
6
  function defaultFormatTimestamp(iso) {
7
7
  return new Date(iso).toLocaleString();
8
8
  }
@@ -17,9 +17,9 @@ function autosaveDisplay(isPublished, status) {
17
17
  const label = getAutosaveStatusLabel(status);
18
18
  return label || '—';
19
19
  }
20
- export default function GraphEditorInfoPanel({ scenarioId, versionId, name, createdAt, updatedAt, isPublished, autosaveStatus, lastValidatedAt, title = 'Info', formatTimestamp = defaultFormatTimestamp, }) {
20
+ export default function GraphEditorInfoPanel({ scenarioId, versionId, name, createdAt, updatedAt, isPublished, autosaveStatus, lastValidatedAt, title = 'Info', formatTimestamp = defaultFormatTimestamp, defaultEdgeType = 'smoothstep', onUpdateDefaultEdgeType, }) {
21
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) : '—' })] }) }) })] }));
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) })] }) }), _jsxs("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) : '—' })] }) }), onUpdateDefaultEdgeType ? (_jsxs("div", { className: "p-3 border-top", children: [_jsx("div", { className: "small fw-semibold mb-2", children: "Global" }), _jsxs(Form.Group, { className: "mb-0", children: [_jsx(Form.Label, { className: "small mb-1", children: "Scenario default edge type" }), _jsx(Form.Select, { value: defaultEdgeType, disabled: isPublished, onChange: (event) => onUpdateDefaultEdgeType(event.target.value), children: EDITOR_EDGE_TYPE_OPTIONS.filter((option) => option.value !== 'default').map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) }), _jsx("div", { className: "text-muted small mt-1", children: "Used when a choice edge type is set to Default." })] })] })) : null] })] }));
25
25
  }
@@ -1,79 +1,4 @@
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;
1
+ import { type InspectorPanelProps } from './inspector/types';
2
+ export { DEFAULT_OUTCOME_OPTIONS, type ChoiceTypeOption, type InspectorChoiceRenderContext, type InspectorNodeRenderContext, type InspectorPanelProps, } from './inspector/types';
3
+ export default function InspectorPanel({ tree, selectedNode, focusChoiceId, isPublished, onUpdateSelectedNode, onAddChoice, onDeleteChoice, onMoveChoice, onSetChoiceTarget, onSetChoiceOutcome, outcomeOptions, hideOutcomeField, renderExtraNodeFields, renderExtraChoiceFields, choiceTypes, onSetChoiceType, title, emptyStateText, typeHelperText, onDeleteSelectedNode, onFocusChoice, }: Readonly<InspectorPanelProps>): import("react").JSX.Element;
79
4
  //# sourceMappingURL=InspectorPanel.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"InspectorPanel.d.ts","sourceRoot":"","sources":["../../src/panels/InspectorPanel.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAGH,KAAK,mBAAmB,EAC3B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACH,uBAAuB,EACvB,KAAK,gBAAgB,EACrB,KAAK,4BAA4B,EACjC,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,GAC3B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EACnC,IAAI,EACJ,YAAY,EACZ,aAAoB,EACpB,WAAmB,EACnB,oBAAoB,EACpB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,cAAwC,EACxC,gBAAwB,EACxB,qBAAqB,EACrB,uBAAuB,EACvB,WAAW,EACX,eAAe,EACf,KAAkB,EAClB,cAA4C,EAC5C,cAAyC,EACzC,oBAAoB,EACpB,aAAa,GAChB,EAAE,QAAQ,CAAC,mBAAmB,CAAC,+BAuC/B"}
@@ -1,63 +1,23 @@
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';
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
7
2
  /**
8
- * Canonical outcome values from the `@signalsafe/tree-spec` wire contract.
9
- * Hosts can override via the `outcomeOptions` prop.
3
+ * Inspector panel for the TreeSpec graph editor.
4
+ *
5
+ * Renders a **Required** card (Type, Prompt, optional `renderExtraNodeFields`) and a **Choices**
6
+ * card (list-group rows with Label / Next / Outcome / delete). Hosts inject project-specific UI through optional render-prop slots:
7
+ *
8
+ * - `outcomeOptions`: override the END-transition outcome `<select>` options
9
+ * (default = the three canonical `TerminalOutcome` values from `@signalsafe/tree-spec`).
10
+ * - `hideOutcomeField`: hide the outcome `<select>` entirely when the host manages outcomes
11
+ * elsewhere.
12
+ * - `renderExtraNodeFields`: render additional UI below the prompt textarea, scoped to the
13
+ * currently-selected node.
14
+ * - `renderExtraChoiceFields`: render additional UI below each choice card.
15
+ * - `choiceTypes` / `onSetChoiceType`: host-defined choice type catalog (stable ids).
10
16
  */
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 }) })) }));
17
+ import ChoiceEditorList from './inspector/ChoiceEditorList';
18
+ import RequiredNodeCard from './inspector/RequiredNodeCard';
19
+ import { DEFAULT_OUTCOME_OPTIONS, DEFAULT_TYPE_HELPER_TEXT, } from './inspector/types';
20
+ export { DEFAULT_OUTCOME_OPTIONS, } from './inspector/types';
21
+ export default function InspectorPanel({ tree, selectedNode, focusChoiceId = null, isPublished = false, onUpdateSelectedNode, onAddChoice, onDeleteChoice, onMoveChoice, onSetChoiceTarget, onSetChoiceOutcome, outcomeOptions = DEFAULT_OUTCOME_OPTIONS, hideOutcomeField = false, renderExtraNodeFields, renderExtraChoiceFields, choiceTypes, onSetChoiceType, title = 'Required', emptyStateText = 'Select a node to edit it.', typeHelperText = DEFAULT_TYPE_HELPER_TEXT, onDeleteSelectedNode, onFocusChoice, }) {
22
+ return (_jsx("div", { children: selectedNode ? (_jsxs(_Fragment, { children: [_jsx(RequiredNodeCard, { tree: tree, selectedNode: selectedNode, isPublished: isPublished, title: title, typeHelperText: typeHelperText, onUpdateSelectedNode: onUpdateSelectedNode, onDeleteSelectedNode: onDeleteSelectedNode, renderExtraNodeFields: renderExtraNodeFields }), _jsx(ChoiceEditorList, { tree: tree, selectedNode: selectedNode, focusChoiceId: focusChoiceId, isPublished: isPublished, onAddChoice: onAddChoice, onUpdateSelectedNode: onUpdateSelectedNode, onDeleteChoice: onDeleteChoice, onMoveChoice: onMoveChoice, onSetChoiceTarget: onSetChoiceTarget, onSetChoiceOutcome: onSetChoiceOutcome, outcomeOptions: outcomeOptions, hideOutcomeField: hideOutcomeField, renderExtraChoiceFields: renderExtraChoiceFields, choiceTypes: choiceTypes, onSetChoiceType: onSetChoiceType, onFocusChoice: onFocusChoice })] })) : (_jsx("div", { className: "text-muted", children: _jsx("em", { children: emptyStateText }) })) }));
63
23
  }
@@ -1,5 +1,5 @@
1
1
  import type { TreeSpecIssue } from '@signalsafe/tree-spec';
2
- import { type EditorTree } from '@signalsafe/tree-spec-editor-core';
2
+ import { type EditorTree, type GraphSelection } from '@signalsafe/tree-spec-editor-core';
3
3
  export interface IssuesPanelProps {
4
4
  issues: TreeSpecIssue[];
5
5
  lastValidatedAt: string | null;
@@ -16,6 +16,10 @@ export interface IssuesPanelProps {
16
16
  * not have a tree in scope; type falls back to **—**.
17
17
  */
18
18
  tree?: EditorTree;
19
+ /** Current graph selection — used to highlight the active issue row. */
20
+ selection?: GraphSelection;
21
+ /** Focused choice id from the editor — pairs with `selection` for issue row highlight. */
22
+ focusChoiceId?: string | null;
19
23
  /** Optional override for the card header title. Defaults to "Issues". */
20
24
  title?: string;
21
25
  /** Optional placeholder for the search field. Defaults to "Search issues…". */
@@ -29,5 +33,5 @@ export interface IssuesPanelProps {
29
33
  */
30
34
  showValidationSummaryInHeader?: boolean;
31
35
  }
32
- export default function IssuesPanel({ issues, lastValidatedAt, onSelectIssue, issueSearch, onIssueSearchChange, tree, title, searchPlaceholder, formatTimestamp, showValidationSummaryInHeader, }: Readonly<IssuesPanelProps>): import("react/jsx-runtime").JSX.Element;
36
+ export default function IssuesPanel({ issues, lastValidatedAt, onSelectIssue, issueSearch, onIssueSearchChange, tree, selection, focusChoiceId, title, searchPlaceholder, formatTimestamp, showValidationSummaryInHeader, }: Readonly<IssuesPanelProps>): import("react").JSX.Element;
33
37
  //# sourceMappingURL=IssuesPanel.d.ts.map
@@ -1 +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"}
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,EAGH,KAAK,UAAU,EACf,KAAK,cAAc,EACtB,MAAM,mCAAmC,CAAC;AAM3C,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,wEAAwE;IACxE,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,0FAA0F;IAC1F,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,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;AAwCD,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAChC,MAAM,EACN,eAAe,EACf,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,IAAI,EACJ,SAAS,EACT,aAAoB,EACpB,KAAgB,EAChB,iBAAyC,EACzC,eAAwC,EACxC,6BAAoC,GACvC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,+BAoH5B"}
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from 'react';
3
3
  import { Form } from 'react-bootstrap';
4
- import { buildStableEntries } from '@signalsafe/tree-spec-editor-core';
4
+ import { buildStableEntries, GRAPH_SELECTION_KIND, } from '@signalsafe/tree-spec-editor-core';
5
5
  import { getIssueSeverityBadgeClass } from '../lib/panelHelpers';
6
6
  import PanelHeaderCollapseCarets from '../lib/PanelHeaderCollapseCarets';
7
+ import { LIST_SELECTION_CLASS, LIST_SELECTION_TEXT_CLASS } from '../lib/selectionStyles';
7
8
  function defaultFormatTimestamp(iso) {
8
9
  return new Date(iso).toLocaleString();
9
10
  }
@@ -28,7 +29,15 @@ function issueMatchesQuery(issue, qLower, tree) {
28
29
  ];
29
30
  return parts.some((p) => p.toLowerCase().includes(qLower));
30
31
  }
31
- export default function IssuesPanel({ issues, lastValidatedAt, onSelectIssue, issueSearch, onIssueSearchChange, tree, title = 'Issues', searchPlaceholder = 'Search issues\u2026', formatTimestamp = defaultFormatTimestamp, showValidationSummaryInHeader = true, }) {
32
+ function isIssueRowSelected(issue, selection, focusChoiceId) {
33
+ if (selection?.kind !== GRAPH_SELECTION_KIND.NODE || !issue.node_id) {
34
+ return false;
35
+ }
36
+ if (issue.node_id !== selection.id)
37
+ return false;
38
+ return (issue.choice_id ?? null) === (focusChoiceId ?? null);
39
+ }
40
+ export default function IssuesPanel({ issues, lastValidatedAt, onSelectIssue, issueSearch, onIssueSearchChange, tree, selection, focusChoiceId = null, title = 'Issues', searchPlaceholder = 'Search issues\u2026', formatTimestamp = defaultFormatTimestamp, showValidationSummaryInHeader = true, }) {
32
41
  const [expanded, setExpanded] = useState(true);
33
42
  const qLower = issueSearch.trim().toLowerCase();
34
43
  const filteredIssues = useMemo(() => issues.filter((issue) => issueMatchesQuery(issue, qLower, tree)), [issues, qLower, tree]);
@@ -36,6 +45,9 @@ export default function IssuesPanel({ issues, lastValidatedAt, onSelectIssue, is
36
45
  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
46
  const typeLabel = nodeTypeLabel(tree, issue.node_id);
38
47
  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));
48
+ const isSelected = isIssueRowSelected(issue, selection, focusChoiceId);
49
+ return (_jsxs("button", { type: "button", className: `list-group-item px-3 py-2 text-start w-100 min-w-0${isSelected
50
+ ? ` ${LIST_SELECTION_CLASS} ${LIST_SELECTION_TEXT_CLASS}`
51
+ : ''}`, "aria-current": isSelected ? 'true' : undefined, onClick: () => onSelectIssue(issue), children: [_jsxs("div", { className: "d-flex w-100 justify-content-between align-items-start gap-2 mb-1 min-w-0", children: [_jsxs("div", { className: "min-w-0 flex-grow-1 text-break 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${isSelected ? '' : ' 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-break font-monospace${isSelected ? '' : ' text-body-secondary'}`, children: issue.message }), issue.choice_id ? (_jsx("small", { className: `text-break${isSelected ? '' : ' text-body-secondary'}`, children: issue.choice_id })) : null] }, key));
40
52
  }) })) })] })) })] }));
41
53
  }
@@ -3,6 +3,8 @@ export interface NodesPanelProps {
3
3
  tree: EditorTree;
4
4
  nodeSearch: string;
5
5
  selection: GraphSelection;
6
+ /** Node to highlight when a choice or edge is focused (may differ from `selection`). */
7
+ focusNodeId?: string | null;
6
8
  showMiniMap: boolean;
7
9
  onNodeSearchChange: (value: string) => void;
8
10
  onNodeSelect: (nodeId: string) => void;
@@ -24,5 +26,5 @@ export interface NodesPanelProps {
24
26
  /** When true, node delete controls are disabled (read-only published revision). */
25
27
  isPublished?: boolean;
26
28
  }
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;
29
+ export default function NodesPanel({ tree, nodeSearch, selection, focusNodeId, showMiniMap, onNodeSearchChange, onNodeSelect, onShowMiniMapChange, searchPlaceholder, tipText, onAddNode, onDeleteNode, isPublished, }: Readonly<NodesPanelProps>): import("react").JSX.Element;
28
30
  //# sourceMappingURL=NodesPanel.d.ts.map
@@ -1 +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"}
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;AAK3C,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,wFAAwF;IACxF,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,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;AAmCD,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAC/B,IAAI,EACJ,UAAU,EACV,SAAS,EACT,WAAkB,EAClB,WAAW,EACX,kBAAkB,EAClB,YAAY,EACZ,mBAAmB,EACnB,iBAAwC,EACxC,OAA0B,EAC1B,SAAS,EACT,YAAY,EACZ,WAAmB,GACtB,EAAE,QAAQ,CAAC,eAAe,CAAC,+BA8I3B"}
@@ -3,6 +3,7 @@ import { useState } from 'react';
3
3
  import { Form } from 'react-bootstrap';
4
4
  import { GRAPH_SELECTION_KIND, TREE_SPEC_NODE_TYPE_PRESETS, } from '@signalsafe/tree-spec-editor-core';
5
5
  import PanelHeaderCollapseCarets from '../lib/PanelHeaderCollapseCarets';
6
+ import { LIST_SELECTION_CLASS, LIST_SELECTION_TEXT_CLASS } from '../lib/selectionStyles';
6
7
  const DEFAULT_TIP_TEXT = 'Tip: connect choices by dragging the blue dots. Shortcuts: \u2318S save, \u2318\u21E7V validate, \u2318D duplicate, Del delete.';
7
8
  const TYPE_SORT_ORDER = new Map(TREE_SPEC_NODE_TYPE_PRESETS.map((t, i) => [t, i]));
8
9
  function choiceLine(n) {
@@ -24,7 +25,12 @@ function compareNodesByTypeThenId(a, b) {
24
25
  return ta.localeCompare(tb);
25
26
  return a.id.localeCompare(b.id);
26
27
  }
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
+ function isNodeRowSelected(nodeId, selection, focusNodeId) {
29
+ if (focusNodeId === nodeId)
30
+ return true;
31
+ return selection.kind === GRAPH_SELECTION_KIND.NODE && selection.id === nodeId;
32
+ }
33
+ export default function NodesPanel({ tree, nodeSearch, selection, focusNodeId = null, showMiniMap, onNodeSearchChange, onNodeSelect, onShowMiniMapChange, searchPlaceholder = 'Search nodes\u2026', tipText = DEFAULT_TIP_TEXT, onAddNode, onDeleteNode, isPublished = false, }) {
28
34
  const [expanded, setExpanded] = useState(true);
29
35
  const filteredNodes = Object.values(tree.nodes)
30
36
  .filter((n) => {
@@ -39,10 +45,10 @@ export default function NodesPanel({ tree, nodeSearch, selection, showMiniMap, o
39
45
  })
40
46
  .sort(compareNodesByTypeThenId);
41
47
  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;
48
+ const isSelected = isNodeRowSelected(n.id, selection, focusNodeId);
43
49
  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" })] }) })] })] }));
50
+ return (_jsx("div", { className: `list-group-item px-3 py-2${isSelected
51
+ ? ` ${LIST_SELECTION_CLASS} ${LIST_SELECTION_TEXT_CLASS}`
52
+ : ''}`, "aria-current": isSelected ? 'true' : undefined, children: _jsxs("div", { className: "d-flex w-100 justify-content-between align-items-start gap-2 min-w-0", children: [_jsxs("button", { type: "button", className: "btn p-0 border-0 bg-transparent flex-grow-1 text-start min-w-0", onClick: () => onNodeSelect(n.id), children: [_jsx("div", { className: "d-flex w-100 justify-content-between align-items-center gap-2 mb-1 min-w-0", children: _jsxs("div", { className: "min-w-0 flex-grow-1 text-break text-start", title: `${n.id}, ${typeLabel}`, children: [_jsx("span", { className: `fs-6 fw-semibold text-uppercase${isSelected ? '' : ' text-body-secondary'}`, children: n.id }), _jsx("span", { className: `fs-6 text-uppercase${isSelected ? '' : ' text-body-secondary'}`, children: `, ${typeLabel}` })] }) }), _jsx("p", { className: `mb-1 small text-break${isSelected ? '' : ' text-body-secondary'}`, children: n.prompt || '(empty)' }), _jsx("small", { className: isSelected ? '' : ' text-body-secondary', 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));
53
+ }) })) }), _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 text-break min-w-0 flex-grow-1 me-2", 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
54
  }
@@ -1,18 +1,25 @@
1
1
  /**
2
2
  * Compact read-only display of the currently selected transition.
3
3
  * Returns `null` when no edge is selected, so the host can render it unconditionally.
4
+ *
5
+ * For editing edge/choice appearance, use {@link AppearancePanel} instead.
4
6
  */
5
7
  import type { ReactNode } from 'react';
6
8
  import type { EditorTransition } from '@signalsafe/tree-spec-editor-core';
9
+ import { type PanelNodeLinkContext } from '../lib/panelNodeLink';
10
+ export type SelectedEdgeDetailsContext = PanelNodeLinkContext;
11
+ export { PanelNodeLink } from '../lib/panelNodeLink';
7
12
  export interface SelectedEdgePanelProps {
8
13
  selectedEdge: EditorTransition | null;
14
+ /** Select a node when its id is clicked in the default details line. */
15
+ onNodeSelect?: (nodeId: string) => void;
9
16
  /** Override the card title (default "Selected Edge"). */
10
17
  title?: string;
11
18
  /**
12
19
  * Optional custom renderer for the edge details line. Defaults to
13
- * `from \u00b7 choice \u2192 to`.
20
+ * `from · choice → to` with clickable node ids when `onNodeSelect` is set.
14
21
  */
15
- renderDetails?: (edge: EditorTransition) => ReactNode;
22
+ renderDetails?: (edge: EditorTransition, ctx: SelectedEdgeDetailsContext) => ReactNode;
16
23
  }
17
- export default function SelectedEdgePanel({ selectedEdge, title, renderDetails, }: Readonly<SelectedEdgePanelProps>): import("react/jsx-runtime").JSX.Element | null;
24
+ export default function SelectedEdgePanel({ selectedEdge, onNodeSelect, title, renderDetails, }: Readonly<SelectedEdgePanelProps>): import("react").JSX.Element | null;
18
25
  //# sourceMappingURL=SelectedEdgePanel.d.ts.map