@principal-ai/subsystems-react 0.20.6 → 0.21.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.
- package/README.md +1 -1
- package/dist/graphify/anchor.d.ts +17 -0
- package/dist/graphify/anchor.d.ts.map +1 -1
- package/dist/graphify/anchor.js +41 -0
- package/dist/graphify/anchor.js.map +1 -1
- package/dist/graphify/consolidated.d.ts +40 -1
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/pierre/{PierreThroughlineCodeView.d.ts → PierreWalkthroughCodeView.d.ts} +7 -7
- package/dist/pierre/{PierreThroughlineCodeView.d.ts.map → PierreWalkthroughCodeView.d.ts.map} +1 -1
- package/dist/pierre/{PierreThroughlineCodeView.js → PierreWalkthroughCodeView.js} +28 -36
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -0
- package/dist/pierre/index.d.ts +2 -2
- package/dist/pierre/index.js +1 -1
- package/dist/pierre/pierreFileLang.d.ts +1 -1
- package/dist/pierre/pierreFileLang.js +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +14 -2
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +86 -20
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/FileDrawer.d.ts +2 -2
- package/dist/subsystem/FileDrawer.js +2 -2
- package/dist/subsystem/SubsystemComponentGraph.d.ts +25 -24
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +204 -199
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts +2 -1
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.js +57 -33
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +85 -35
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +85 -6
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +30 -13
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.js +2 -2
- package/dist/subsystem/tokenizeComponent.js.map +1 -1
- package/package.json +3 -3
- package/src/graphify/anchor.test.ts +31 -2
- package/src/graphify/anchor.ts +46 -0
- package/src/graphify/consolidated.ts +43 -1
- package/src/index.ts +13 -5
- package/src/pierre/{PierreThroughlineCodeView.tsx → PierreWalkthroughCodeView.tsx} +39 -47
- package/src/pierre/index.ts +2 -2
- package/src/pierre/pierreFileLang.ts +1 -1
- package/src/stories/Pierre/CodeView.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +14 -14
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +13 -15
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +8 -8
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +14 -14
- package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +11 -10
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +13 -14
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +31 -66
- package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +2 -2
- package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +1 -1
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +518 -33
- package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +13 -13
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +105 -21
- package/src/subsystem/FileDrawer.tsx +2 -2
- package/src/subsystem/SubsystemComponentGraph.tsx +268 -259
- package/src/subsystem/formatDeclaration.test.ts +76 -1
- package/src/subsystem/formatDeclaration.ts +57 -33
- package/src/subsystem/model.test.ts +27 -15
- package/src/subsystem/model.ts +175 -45
- package/src/subsystem/nodes.tsx +47 -13
- package/src/subsystem/tokenizeComponent.ts +2 -2
- package/dist/components/ConfigurationSelector.d.ts +0 -35
- package/dist/components/ConfigurationSelector.d.ts.map +0 -1
- package/dist/components/ConfigurationSelector.js +0 -63
- package/dist/components/ConfigurationSelector.js.map +0 -1
- package/dist/components/GraphRenderer.d.ts +0 -295
- package/dist/components/GraphRenderer.d.ts.map +0 -1
- package/dist/components/GraphRenderer.js +0 -2008
- package/dist/components/GraphRenderer.js.map +0 -1
- package/dist/components/MultiCanvasRenderer.d.ts +0 -75
- package/dist/components/MultiCanvasRenderer.d.ts.map +0 -1
- package/dist/components/MultiCanvasRenderer.js +0 -203
- package/dist/components/MultiCanvasRenderer.js.map +0 -1
- package/dist/components/NodeTooltip.d.ts +0 -24
- package/dist/components/NodeTooltip.d.ts.map +0 -1
- package/dist/components/NodeTooltip.js +0 -160
- package/dist/components/NodeTooltip.js.map +0 -1
- package/dist/components/SelectionSidebar.d.ts +0 -18
- package/dist/components/SelectionSidebar.d.ts.map +0 -1
- package/dist/components/SelectionSidebar.js +0 -190
- package/dist/components/SelectionSidebar.js.map +0 -1
- package/dist/components/SequenceDiagramRenderer.d.ts +0 -77
- package/dist/components/SequenceDiagramRenderer.d.ts.map +0 -1
- package/dist/components/SequenceDiagramRenderer.js +0 -604
- package/dist/components/SequenceDiagramRenderer.js.map +0 -1
- package/dist/components/WorkflowSequenceDiagram.d.ts +0 -63
- package/dist/components/WorkflowSequenceDiagram.d.ts.map +0 -1
- package/dist/components/WorkflowSequenceDiagram.js +0 -147
- package/dist/components/WorkflowSequenceDiagram.js.map +0 -1
- package/dist/components/dashboard/DashboardRenderer.d.ts +0 -10
- package/dist/components/dashboard/DashboardRenderer.d.ts.map +0 -1
- package/dist/components/dashboard/DashboardRenderer.js +0 -179
- package/dist/components/dashboard/DashboardRenderer.js.map +0 -1
- package/dist/components/dashboard/MetricPanel.d.ts +0 -10
- package/dist/components/dashboard/MetricPanel.d.ts.map +0 -1
- package/dist/components/dashboard/MetricPanel.js +0 -103
- package/dist/components/dashboard/MetricPanel.js.map +0 -1
- package/dist/components/dashboard/MockDataProvider.d.ts +0 -30
- package/dist/components/dashboard/MockDataProvider.d.ts.map +0 -1
- package/dist/components/dashboard/MockDataProvider.js +0 -270
- package/dist/components/dashboard/MockDataProvider.js.map +0 -1
- package/dist/components/dashboard/components/BarChart.d.ts +0 -10
- package/dist/components/dashboard/components/BarChart.d.ts.map +0 -1
- package/dist/components/dashboard/components/BarChart.js +0 -167
- package/dist/components/dashboard/components/BarChart.js.map +0 -1
- package/dist/components/dashboard/components/LineChart.d.ts +0 -10
- package/dist/components/dashboard/components/LineChart.d.ts.map +0 -1
- package/dist/components/dashboard/components/LineChart.js +0 -141
- package/dist/components/dashboard/components/LineChart.js.map +0 -1
- package/dist/components/dashboard/components/MetricCard.d.ts +0 -9
- package/dist/components/dashboard/components/MetricCard.d.ts.map +0 -1
- package/dist/components/dashboard/components/MetricCard.js +0 -163
- package/dist/components/dashboard/components/MetricCard.js.map +0 -1
- package/dist/components/dashboard/components/SourceLink.d.ts +0 -9
- package/dist/components/dashboard/components/SourceLink.d.ts.map +0 -1
- package/dist/components/dashboard/components/SourceLink.js +0 -39
- package/dist/components/dashboard/components/SourceLink.js.map +0 -1
- package/dist/components/dashboard/components/TimeRangeSelector.d.ts +0 -9
- package/dist/components/dashboard/components/TimeRangeSelector.d.ts.map +0 -1
- package/dist/components/dashboard/components/TimeRangeSelector.js +0 -167
- package/dist/components/dashboard/components/TimeRangeSelector.js.map +0 -1
- package/dist/components/dashboard/components/index.d.ts +0 -6
- package/dist/components/dashboard/components/index.d.ts.map +0 -1
- package/dist/components/dashboard/components/index.js +0 -6
- package/dist/components/dashboard/components/index.js.map +0 -1
- package/dist/components/dashboard/index.d.ts +0 -6
- package/dist/components/dashboard/index.d.ts.map +0 -1
- package/dist/components/dashboard/index.js +0 -8
- package/dist/components/dashboard/index.js.map +0 -1
- package/dist/components/dashboard/types.d.ts +0 -74
- package/dist/components/dashboard/types.d.ts.map +0 -1
- package/dist/components/dashboard/types.js +0 -8
- package/dist/components/dashboard/types.js.map +0 -1
- package/dist/components/state-view/PipelineView.d.ts +0 -14
- package/dist/components/state-view/PipelineView.d.ts.map +0 -1
- package/dist/components/state-view/PipelineView.js +0 -195
- package/dist/components/state-view/PipelineView.js.map +0 -1
- package/dist/components/state-view/index.d.ts +0 -14
- package/dist/components/state-view/index.d.ts.map +0 -1
- package/dist/components/state-view/index.js +0 -12
- package/dist/components/state-view/index.js.map +0 -1
- package/dist/components/state-view/types.d.ts +0 -188
- package/dist/components/state-view/types.d.ts.map +0 -1
- package/dist/components/state-view/types.js +0 -10
- package/dist/components/state-view/types.js.map +0 -1
- package/dist/components/state-view/useStateView.d.ts +0 -32
- package/dist/components/state-view/useStateView.d.ts.map +0 -1
- package/dist/components/state-view/useStateView.js +0 -129
- package/dist/components/state-view/useStateView.js.map +0 -1
- package/dist/contexts/GraphEditContext.d.ts +0 -20
- package/dist/contexts/GraphEditContext.d.ts.map +0 -1
- package/dist/contexts/GraphEditContext.js +0 -8
- package/dist/contexts/GraphEditContext.js.map +0 -1
- package/dist/contexts/TooltipPortalContext.d.ts +0 -8
- package/dist/contexts/TooltipPortalContext.d.ts.map +0 -1
- package/dist/contexts/TooltipPortalContext.js +0 -8
- package/dist/contexts/TooltipPortalContext.js.map +0 -1
- package/dist/edges/CustomEdge.d.ts +0 -24
- package/dist/edges/CustomEdge.d.ts.map +0 -1
- package/dist/edges/CustomEdge.js +0 -222
- package/dist/edges/CustomEdge.js.map +0 -1
- package/dist/edges/GenericEdge.d.ts +0 -18
- package/dist/edges/GenericEdge.d.ts.map +0 -1
- package/dist/edges/GenericEdge.js +0 -10
- package/dist/edges/GenericEdge.js.map +0 -1
- package/dist/graphify/kind.d.ts +0 -23
- package/dist/graphify/kind.d.ts.map +0 -1
- package/dist/graphify/kind.js +0 -77
- package/dist/graphify/kind.js.map +0 -1
- package/dist/hooks/usePathBasedEvents.d.ts +0 -42
- package/dist/hooks/usePathBasedEvents.d.ts.map +0 -1
- package/dist/hooks/usePathBasedEvents.js +0 -119
- package/dist/hooks/usePathBasedEvents.js.map +0 -1
- package/dist/hooks/useSequenceLayout.d.ts +0 -186
- package/dist/hooks/useSequenceLayout.d.ts.map +0 -1
- package/dist/hooks/useSequenceLayout.js +0 -327
- package/dist/hooks/useSequenceLayout.js.map +0 -1
- package/dist/hooks/useUndoRedo.d.ts +0 -80
- package/dist/hooks/useUndoRedo.d.ts.map +0 -1
- package/dist/hooks/useUndoRedo.js +0 -70
- package/dist/hooks/useUndoRedo.js.map +0 -1
- package/dist/nodes/CustomNode.d.ts +0 -28
- package/dist/nodes/CustomNode.d.ts.map +0 -1
- package/dist/nodes/CustomNode.js +0 -820
- package/dist/nodes/CustomNode.js.map +0 -1
- package/dist/nodes/GenericNode.d.ts +0 -20
- package/dist/nodes/GenericNode.d.ts.map +0 -1
- package/dist/nodes/GenericNode.js +0 -20
- package/dist/nodes/GenericNode.js.map +0 -1
- package/dist/nodes/otel/OtelBoundaryNode.d.ts +0 -55
- package/dist/nodes/otel/OtelBoundaryNode.d.ts.map +0 -1
- package/dist/nodes/otel/OtelBoundaryNode.js +0 -81
- package/dist/nodes/otel/OtelBoundaryNode.js.map +0 -1
- package/dist/nodes/otel/OtelEventNode.d.ts +0 -59
- package/dist/nodes/otel/OtelEventNode.d.ts.map +0 -1
- package/dist/nodes/otel/OtelEventNode.js +0 -82
- package/dist/nodes/otel/OtelEventNode.js.map +0 -1
- package/dist/nodes/otel/OtelResourceNode.d.ts +0 -53
- package/dist/nodes/otel/OtelResourceNode.d.ts.map +0 -1
- package/dist/nodes/otel/OtelResourceNode.js +0 -105
- package/dist/nodes/otel/OtelResourceNode.js.map +0 -1
- package/dist/nodes/otel/OtelScopeNode.d.ts +0 -53
- package/dist/nodes/otel/OtelScopeNode.d.ts.map +0 -1
- package/dist/nodes/otel/OtelScopeNode.js +0 -81
- package/dist/nodes/otel/OtelScopeNode.js.map +0 -1
- package/dist/nodes/otel/OtelSpanConventionNode.d.ts +0 -61
- package/dist/nodes/otel/OtelSpanConventionNode.d.ts.map +0 -1
- package/dist/nodes/otel/OtelSpanConventionNode.js +0 -131
- package/dist/nodes/otel/OtelSpanConventionNode.js.map +0 -1
- package/dist/nodes/otel/index.d.ts +0 -17
- package/dist/nodes/otel/index.d.ts.map +0 -1
- package/dist/nodes/otel/index.js +0 -13
- package/dist/nodes/otel/index.js.map +0 -1
- package/dist/nodes/otel/shared/NodeBadges.d.ts +0 -10
- package/dist/nodes/otel/shared/NodeBadges.d.ts.map +0 -1
- package/dist/nodes/otel/shared/NodeBadges.js +0 -178
- package/dist/nodes/otel/shared/NodeBadges.js.map +0 -1
- package/dist/nodes/otel/shared/NodeContent.d.ts +0 -10
- package/dist/nodes/otel/shared/NodeContent.d.ts.map +0 -1
- package/dist/nodes/otel/shared/NodeContent.js +0 -96
- package/dist/nodes/otel/shared/NodeContent.js.map +0 -1
- package/dist/nodes/otel/shared/index.d.ts +0 -8
- package/dist/nodes/otel/shared/index.d.ts.map +0 -1
- package/dist/nodes/otel/shared/index.js +0 -8
- package/dist/nodes/otel/shared/index.js.map +0 -1
- package/dist/nodes/otel/shared/types.d.ts +0 -129
- package/dist/nodes/otel/shared/types.d.ts.map +0 -1
- package/dist/nodes/otel/shared/types.js +0 -5
- package/dist/nodes/otel/shared/types.js.map +0 -1
- package/dist/nodes/otel/shared/useNodeBehavior.d.ts +0 -42
- package/dist/nodes/otel/shared/useNodeBehavior.d.ts.map +0 -1
- package/dist/nodes/otel/shared/useNodeBehavior.js +0 -61
- package/dist/nodes/otel/shared/useNodeBehavior.js.map +0 -1
- package/dist/pierre/PierreThroughlineCodeView.js.map +0 -1
- package/dist/subsystem/SubsystemCarousel.d.ts +0 -36
- package/dist/subsystem/SubsystemCarousel.d.ts.map +0 -1
- package/dist/subsystem/SubsystemCarousel.js +0 -163
- package/dist/subsystem/SubsystemCarousel.js.map +0 -1
- package/dist/utils/animationMapping.d.ts +0 -53
- package/dist/utils/animationMapping.d.ts.map +0 -1
- package/dist/utils/animationMapping.js +0 -126
- package/dist/utils/animationMapping.js.map +0 -1
- package/dist/utils/canvasBounds.d.ts +0 -50
- package/dist/utils/canvasBounds.d.ts.map +0 -1
- package/dist/utils/canvasBounds.js +0 -88
- package/dist/utils/canvasBounds.js.map +0 -1
- package/dist/utils/graphConverter.d.ts +0 -18
- package/dist/utils/graphConverter.d.ts.map +0 -1
- package/dist/utils/graphConverter.js +0 -114
- package/dist/utils/graphConverter.js.map +0 -1
- package/dist/utils/iconResolver.d.ts +0 -29
- package/dist/utils/iconResolver.d.ts.map +0 -1
- package/dist/utils/iconResolver.js +0 -30
- package/dist/utils/iconResolver.js.map +0 -1
- package/dist/utils/orientationUtils.d.ts +0 -19
- package/dist/utils/orientationUtils.d.ts.map +0 -1
- package/dist/utils/orientationUtils.js +0 -62
- package/dist/utils/orientationUtils.js.map +0 -1
|
@@ -36,13 +36,16 @@ import { ChevronDown, ChevronUp, Pause, Play, X } from 'lucide-react';
|
|
|
36
36
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
37
37
|
import {
|
|
38
38
|
buildSubsystemGraph,
|
|
39
|
+
deriveGraphEdges,
|
|
39
40
|
MECHANISM_COLOR,
|
|
40
41
|
MECHANISM_DESCRIPTIONS,
|
|
41
42
|
subsystemGraphLayoutKey,
|
|
43
|
+
walkthroughStepGraphEdgeId,
|
|
42
44
|
type SubsystemComponentEdge,
|
|
43
45
|
type SubsystemComponent,
|
|
44
46
|
type SubsystemEdgeMechanism,
|
|
45
|
-
type
|
|
47
|
+
type SubsystemRelation,
|
|
48
|
+
type SubsystemWalkthrough,
|
|
46
49
|
} from './model';
|
|
47
50
|
import type { SubsystemOpenFileOptions } from './declarationRef';
|
|
48
51
|
import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility } from './nodes';
|
|
@@ -58,33 +61,34 @@ const EDGE_LABEL_MAX_EDGE_FRACTION = 0.55;
|
|
|
58
61
|
/** Rough monospace width at fontSize 10 + horizontal padding/border. */
|
|
59
62
|
const EDGE_LABEL_CHAR_PX = 6.2;
|
|
60
63
|
const EDGE_LABEL_PAD_PX = 18;
|
|
61
|
-
/** Pause (ms) between steps when a
|
|
62
|
-
const
|
|
64
|
+
/** Pause (ms) between steps when a walkthrough autoplays. */
|
|
65
|
+
const WALKTHROUGH_PLAY_PAUSE_MS = 2500;
|
|
63
66
|
|
|
64
|
-
/** Context passed to `
|
|
65
|
-
export interface
|
|
66
|
-
|
|
67
|
+
/** Context passed to `renderWalkthroughViewer` when a flow/step is focused. */
|
|
68
|
+
export interface WalkthroughViewerContext {
|
|
69
|
+
walkthrough: SubsystemWalkthrough;
|
|
67
70
|
/** Focused step index; `null` means the whole flow (no specific step). */
|
|
68
71
|
stepIndex: number | null;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
type DrawerTarget =
|
|
72
75
|
| { kind: 'file'; file: string; startLine?: number }
|
|
73
|
-
| { kind: '
|
|
76
|
+
| { kind: 'walkthrough'; walkthroughId: string; stepIndex: number | null };
|
|
74
77
|
|
|
75
78
|
export interface SubsystemComponentGraphProps {
|
|
76
79
|
components: SubsystemComponent[];
|
|
77
|
-
edges
|
|
80
|
+
/** Topology relations (structural / module / type). Display edges are derived with walkthrough hops. */
|
|
81
|
+
relations: SubsystemRelation[];
|
|
78
82
|
/**
|
|
79
|
-
* Ordered
|
|
80
|
-
*
|
|
81
|
-
* the flows panel lists each
|
|
83
|
+
* Ordered runtime walkthroughs — one walkthrough per flow. When present the
|
|
84
|
+
* sidebar's bottom half offers a Files/Walkthroughs toggle:
|
|
85
|
+
* the flows panel lists each walkthrough's steps (`symbol` or `file:line`);
|
|
82
86
|
* clicking a flow row toggles its steps; clicking a step focuses that
|
|
83
|
-
* step's edge and (when `
|
|
87
|
+
* step's edge and (when `renderWalkthroughViewer` is set) opens the bottom
|
|
84
88
|
* drawer on that flow's snippets. Opened flows stay on the canvas
|
|
85
89
|
* (unselected ones dimmed); everything else is hidden.
|
|
86
90
|
*/
|
|
87
|
-
|
|
91
|
+
walkthroughs?: SubsystemWalkthrough[];
|
|
88
92
|
onSelect?: (componentId: string) => void;
|
|
89
93
|
/** Called when an edge is clicked (relationship / mechanism + refs seam). */
|
|
90
94
|
onEdgeSelect?: (edge: SubsystemComponentEdge) => void;
|
|
@@ -97,29 +101,29 @@ export interface SubsystemComponentGraphProps {
|
|
|
97
101
|
title?: string;
|
|
98
102
|
/**
|
|
99
103
|
* Suppresses the sidebar entirely (title, description, file tree,
|
|
100
|
-
*
|
|
104
|
+
* walkthroughs) for graph-only embeds. Pair with `graphTitle` to keep the
|
|
101
105
|
* subsystem name visible as an overlay on the canvas.
|
|
102
106
|
*/
|
|
103
107
|
hideSidebar?: boolean;
|
|
104
108
|
/**
|
|
105
|
-
* How
|
|
109
|
+
* How walkthrough step highlighting behaves on the canvas.
|
|
106
110
|
* - `focus` (default): zoom to the step, hide non-participants, open drawer
|
|
107
111
|
* - `dim`: keep the full graph, dim non-participants (same as hovering a step)
|
|
108
112
|
*/
|
|
109
|
-
|
|
113
|
+
walkthroughStepMode?: 'focus' | 'dim';
|
|
110
114
|
/**
|
|
111
|
-
* When true, cycles
|
|
112
|
-
* (no zoom, no drawer). Loops across all
|
|
115
|
+
* When true, cycles walkthrough steps automatically using `dim` highlighting
|
|
116
|
+
* (no zoom, no drawer). Loops across all walkthroughs that have steps.
|
|
113
117
|
* Useful for graph-only embeds (`hideSidebar`).
|
|
114
118
|
*/
|
|
115
|
-
|
|
119
|
+
autoPlayWalkthroughs?: boolean;
|
|
116
120
|
/** Pause between autoplay steps in ms. @default 2500 */
|
|
117
|
-
|
|
121
|
+
walkthroughAutoPlayIntervalMs?: number;
|
|
118
122
|
/**
|
|
119
|
-
* When false, focusing a
|
|
123
|
+
* When false, focusing a walkthrough/step does not call `fitView`.
|
|
120
124
|
* @default true
|
|
121
125
|
*/
|
|
122
|
-
|
|
126
|
+
zoomOnWalkthroughFocus?: boolean;
|
|
123
127
|
/**
|
|
124
128
|
* Subsystem title rendered as a non-interactive overlay chip on the graph
|
|
125
129
|
* canvas (top-center). Does not trigger the sidebar — for graph-only
|
|
@@ -127,11 +131,11 @@ export interface SubsystemComponentGraphProps {
|
|
|
127
131
|
*/
|
|
128
132
|
graphTitle?: string;
|
|
129
133
|
/**
|
|
130
|
-
* When true, shows the active
|
|
134
|
+
* When true, shows the active walkthrough's title as a non-interactive
|
|
131
135
|
* overlay chip on the canvas (under `graphTitle` when both are set).
|
|
132
|
-
* Uses the focused or hover-highlighted
|
|
136
|
+
* Uses the focused or hover-highlighted walkthrough.
|
|
133
137
|
*/
|
|
134
|
-
|
|
138
|
+
showWalkthroughTitle?: boolean;
|
|
135
139
|
/** Markdown description rendered in the sidebar. */
|
|
136
140
|
description?: string;
|
|
137
141
|
/** Rendered over the graph canvas only (not the title/legend sidebar). */
|
|
@@ -147,11 +151,11 @@ export interface SubsystemComponentGraphProps {
|
|
|
147
151
|
*/
|
|
148
152
|
renderFileViewer?: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
|
|
149
153
|
/**
|
|
150
|
-
* Host-injected multi-snippet viewer for a focused
|
|
154
|
+
* Host-injected multi-snippet viewer for a focused walkthrough. When set,
|
|
151
155
|
* clicking a flow step opens the bottom drawer with this content and
|
|
152
156
|
* updates it as the focused step changes.
|
|
153
157
|
*/
|
|
154
|
-
|
|
158
|
+
renderWalkthroughViewer?: (ctx: WalkthroughViewerContext) => ReactNode;
|
|
155
159
|
/**
|
|
156
160
|
* Legacy component-keyed variant, kept for backward compatibility. When
|
|
157
161
|
* `renderFileViewer` is absent, drawer content resolves via the first
|
|
@@ -194,33 +198,37 @@ const FileDrawerContent = memo(function FileDrawerContent({
|
|
|
194
198
|
return <>{render(file, startLine != null ? { startLine } : undefined)}</>;
|
|
195
199
|
});
|
|
196
200
|
|
|
197
|
-
const
|
|
201
|
+
const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({
|
|
198
202
|
render,
|
|
199
|
-
|
|
203
|
+
walkthrough,
|
|
200
204
|
stepIndex,
|
|
201
205
|
}: {
|
|
202
|
-
render: (ctx:
|
|
203
|
-
|
|
206
|
+
render: (ctx: WalkthroughViewerContext) => ReactNode;
|
|
207
|
+
walkthrough: SubsystemWalkthrough;
|
|
204
208
|
stepIndex: number | null;
|
|
205
209
|
}) {
|
|
206
|
-
return <>{render({
|
|
210
|
+
return <>{render({ walkthrough, stepIndex })}</>;
|
|
207
211
|
});
|
|
208
212
|
|
|
209
213
|
interface InnerProps extends SubsystemComponentGraphProps {
|
|
210
214
|
measured: { w: number; h: number } | null;
|
|
211
215
|
}
|
|
212
216
|
|
|
213
|
-
function Inner({ components,
|
|
217
|
+
function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
214
218
|
const { theme } = useTheme();
|
|
215
219
|
const { fitView } = useReactFlow();
|
|
216
220
|
const viewport = useViewport();
|
|
221
|
+
const graphEdges = useMemo(
|
|
222
|
+
() => deriveGraphEdges({ relations, walkthroughs }),
|
|
223
|
+
[relations, walkthroughs],
|
|
224
|
+
);
|
|
217
225
|
const [built, setBuilt] = useState<{ nodes: Node[]; edges: Edge[] }>({
|
|
218
226
|
nodes: [],
|
|
219
227
|
edges: [],
|
|
220
228
|
});
|
|
221
229
|
const [layoutReady, setLayoutReady] = useState(false);
|
|
222
230
|
const [selected, setSelected] = useState<SubsystemComponent | null>(null);
|
|
223
|
-
/** Bottom drawer: single file or
|
|
231
|
+
/** Bottom drawer: single file or walkthrough multi-snippet mode. */
|
|
224
232
|
const [drawerTarget, setDrawerTarget] = useState<DrawerTarget | null>(null);
|
|
225
233
|
// Mirror so step-focus can decide whether to wait for the drawer open
|
|
226
234
|
// transition before fitView (avoids framing against full-height canvas).
|
|
@@ -230,27 +238,27 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
230
238
|
const pendingFocusFitRef = useRef<number | null>(null);
|
|
231
239
|
// Component the pointer is over (null on leave) → transient tree highlight.
|
|
232
240
|
const [hoveredComponentId, setHoveredComponentId] = useState<string | null>(null);
|
|
233
|
-
//
|
|
241
|
+
// Walkthrough focus — selected flow (or step) is full strength; other
|
|
234
242
|
// opened-flow members stay visible but dimmed; everything else is hidden.
|
|
235
|
-
const [
|
|
243
|
+
const [focusedWalkthroughId, setFocusedWalkthroughId] = useState<string | null>(null);
|
|
236
244
|
// `null` = whole flow focused; a number = that single step's edge focused.
|
|
237
245
|
const [focusedStepIndex, setFocusedStepIndex] = useState<number | null>(null);
|
|
238
|
-
// Hovered
|
|
246
|
+
// Hovered walkthrough in the flows panel: dims every canvas node/edge not
|
|
239
247
|
// involved in the hover preview (or selected ∪ hovered when a step is
|
|
240
248
|
// focused). `stepIndex: null` = whole flow (collapsed title hover);
|
|
241
249
|
// a number = that step. Transient — no drawer. Camera only reframes when
|
|
242
250
|
// a step is already selected.
|
|
243
|
-
const [
|
|
244
|
-
|
|
251
|
+
const [hoveredWalkthroughStep, setHoveredWalkthroughStep] = useState<{
|
|
252
|
+
walkthroughId: string;
|
|
245
253
|
stepIndex: number | null;
|
|
246
254
|
} | null>(null);
|
|
247
|
-
// Sidebar bottom half: which panel is shown when
|
|
248
|
-
const [sidebarView, setSidebarView] = useState<'files' | '
|
|
249
|
-
|
|
255
|
+
// Sidebar bottom half: which panel is shown when walkthroughs exist.
|
|
256
|
+
const [sidebarView, setSidebarView] = useState<'files' | 'walkthroughs'>(() =>
|
|
257
|
+
walkthroughs?.length ? 'walkthroughs' : 'files',
|
|
250
258
|
);
|
|
251
|
-
//
|
|
259
|
+
// Walkthrough flows the user has expanded (via the title row). Closed by
|
|
252
260
|
// default so a graph with several flows doesn't dump every step list at once.
|
|
253
|
-
const [
|
|
261
|
+
const [expandedWalkthroughs, setExpandedWalkthroughs] = useState<Set<string>>(new Set());
|
|
254
262
|
// Sidebar description visibility. Hidden by default so the files/flows
|
|
255
263
|
// panel gets the vertical room; the title-row toggle reveals it.
|
|
256
264
|
const [descriptionVisible, setDescriptionVisible] = useState(false);
|
|
@@ -314,36 +322,36 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
314
322
|
? { file: drawerTarget.file, startLine: drawerTarget.startLine }
|
|
315
323
|
: null;
|
|
316
324
|
|
|
317
|
-
const
|
|
318
|
-
if (drawerTarget?.kind !== '
|
|
319
|
-
return
|
|
320
|
-
}, [drawerTarget,
|
|
325
|
+
const focusedWalkthrough = useMemo(() => {
|
|
326
|
+
if (drawerTarget?.kind !== 'walkthrough' || !walkthroughs) return null;
|
|
327
|
+
return walkthroughs.find((t) => t.id === drawerTarget.walkthroughId) ?? null;
|
|
328
|
+
}, [drawerTarget, walkthroughs]);
|
|
321
329
|
|
|
322
|
-
//
|
|
323
|
-
const
|
|
324
|
-
if (!
|
|
325
|
-
const id =
|
|
330
|
+
// Walkthrough shown on the canvas title chip (focus or hover/autoplay highlight).
|
|
331
|
+
const overlayWalkthroughTitle = useMemo(() => {
|
|
332
|
+
if (!showWalkthroughTitle || !walkthroughs?.length) return null;
|
|
333
|
+
const id = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId;
|
|
326
334
|
if (!id) return null;
|
|
327
|
-
return
|
|
335
|
+
return walkthroughs.find((t) => t.id === id)?.title ?? null;
|
|
328
336
|
}, [
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
337
|
+
showWalkthroughTitle,
|
|
338
|
+
walkthroughs,
|
|
339
|
+
focusedWalkthroughId,
|
|
340
|
+
hoveredWalkthroughStep,
|
|
333
341
|
]);
|
|
334
342
|
|
|
335
343
|
// Active step for the bottom-of-title progress + annotation chip.
|
|
336
|
-
const
|
|
337
|
-
if (!
|
|
338
|
-
const tlId =
|
|
344
|
+
const overlayWalkthroughStep = useMemo(() => {
|
|
345
|
+
if (!showWalkthroughTitle || !walkthroughs?.length) return null;
|
|
346
|
+
const tlId = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId ?? null;
|
|
339
347
|
let stepIndex: number | null = null;
|
|
340
|
-
if (
|
|
348
|
+
if (focusedWalkthroughId != null) {
|
|
341
349
|
stepIndex = focusedStepIndex;
|
|
342
|
-
} else if (
|
|
343
|
-
stepIndex =
|
|
350
|
+
} else if (hoveredWalkthroughStep != null) {
|
|
351
|
+
stepIndex = hoveredWalkthroughStep.stepIndex;
|
|
344
352
|
}
|
|
345
353
|
if (tlId == null || stepIndex == null) return null;
|
|
346
|
-
const tl =
|
|
354
|
+
const tl = walkthroughs.find((t) => t.id === tlId);
|
|
347
355
|
const step = tl?.steps[stepIndex];
|
|
348
356
|
if (!step || !tl) return null;
|
|
349
357
|
return {
|
|
@@ -352,24 +360,24 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
352
360
|
annotation: step.annotation,
|
|
353
361
|
};
|
|
354
362
|
}, [
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
363
|
+
showWalkthroughTitle,
|
|
364
|
+
walkthroughs,
|
|
365
|
+
focusedWalkthroughId,
|
|
358
366
|
focusedStepIndex,
|
|
359
|
-
|
|
367
|
+
hoveredWalkthroughStep,
|
|
360
368
|
]);
|
|
361
369
|
|
|
362
370
|
const drawerTitle = useMemo(() => {
|
|
363
371
|
if (!drawerTarget) return null;
|
|
364
372
|
if (drawerTarget.kind === 'file') return drawerTarget.file;
|
|
365
|
-
const tl =
|
|
373
|
+
const tl = focusedWalkthrough;
|
|
366
374
|
if (!tl) return null;
|
|
367
375
|
if (drawerTarget.stepIndex == null) return tl.title;
|
|
368
376
|
const step = tl.steps[drawerTarget.stepIndex];
|
|
369
377
|
if (!step) return tl.title;
|
|
370
378
|
const site = `${step.file.split('/').pop() ?? step.file}:${step.line}`;
|
|
371
379
|
return `${tl.title} · ${site}`;
|
|
372
|
-
}, [drawerTarget,
|
|
380
|
+
}, [drawerTarget, focusedWalkthrough]);
|
|
373
381
|
|
|
374
382
|
// Refresh selected component when the components list updates (e.g. verify
|
|
375
383
|
// writes back declarationRef).
|
|
@@ -383,13 +391,15 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
383
391
|
// The pane stays hidden until Pass 2 completes. Key off layout-affecting
|
|
384
392
|
// fields only — declarationRef updates after verify must not re-run ELK.
|
|
385
393
|
const layoutKey = useMemo(
|
|
386
|
-
() => subsystemGraphLayoutKey({ components,
|
|
387
|
-
[components,
|
|
394
|
+
() => subsystemGraphLayoutKey({ components, relations, walkthroughs }),
|
|
395
|
+
[components, relations, walkthroughs],
|
|
388
396
|
);
|
|
389
397
|
const componentsRef = useRef(components);
|
|
390
|
-
const
|
|
398
|
+
const relationsRef = useRef(relations);
|
|
399
|
+
const walkthroughsRef = useRef(walkthroughs);
|
|
391
400
|
componentsRef.current = components;
|
|
392
|
-
|
|
401
|
+
relationsRef.current = relations;
|
|
402
|
+
walkthroughsRef.current = walkthroughs;
|
|
393
403
|
|
|
394
404
|
// Track measured dimensions from React Flow's dimension changes.
|
|
395
405
|
// These arrive as { type: 'dimensions', id, dimensions } in onNodesChange.
|
|
@@ -406,7 +416,11 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
406
416
|
prevMeasuredSigRef.current = '';
|
|
407
417
|
// Bump generation so any in-flight Pass 2 from the prior layoutKey is ignored.
|
|
408
418
|
pass2GenRef.current += 1;
|
|
409
|
-
const doc = {
|
|
419
|
+
const doc = {
|
|
420
|
+
components: componentsRef.current,
|
|
421
|
+
relations: relationsRef.current,
|
|
422
|
+
walkthroughs: walkthroughsRef.current,
|
|
423
|
+
};
|
|
410
424
|
void buildSubsystemGraph(doc, { maxNodeWidth, showEdgeLabels })
|
|
411
425
|
.then(({ nodes, edges: e }) => {
|
|
412
426
|
if (!alive) return;
|
|
@@ -455,7 +469,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
455
469
|
const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id)!.height]));
|
|
456
470
|
const gen = ++pass2GenRef.current;
|
|
457
471
|
void buildSubsystemGraph(
|
|
458
|
-
{ components,
|
|
472
|
+
{ components, relations, walkthroughs },
|
|
459
473
|
{ maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights },
|
|
460
474
|
)
|
|
461
475
|
.then(({ nodes, edges: e }) => {
|
|
@@ -469,7 +483,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
469
483
|
// Reveal Pass 1 layout rather than leaving the cover up forever.
|
|
470
484
|
setLayoutReady(true);
|
|
471
485
|
});
|
|
472
|
-
}, [built.nodes, components,
|
|
486
|
+
}, [built.nodes, components, relations, walkthroughs, maxNodeWidth, showEdgeLabels]);
|
|
473
487
|
|
|
474
488
|
// After Pass 1 commits, try Pass 2 immediately with retained measurements.
|
|
475
489
|
// Same-id live updates often get no new React Flow `dimensions` events, so
|
|
@@ -498,7 +512,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
498
512
|
// Node components call SUBSYSTEM_CALLBACKS.onSelect on click (their inner
|
|
499
513
|
// onClick stops React Flow propagation), so wire selection + width through it.
|
|
500
514
|
// Lookup from edge id → the authored SubsystemComponentEdge (for onEdgeSelect).
|
|
501
|
-
const edgeById = useMemo(() => new Map(
|
|
515
|
+
const edgeById = useMemo(() => new Map(graphEdges.map((e) => [e.id, e])), [graphEdges]);
|
|
502
516
|
|
|
503
517
|
// Shared edge-selection logic used by both React Flow's onEdgeClick and the
|
|
504
518
|
// clickable edge label (SUBSYSTEM_CALLBACKS.onEdgeSelect).
|
|
@@ -510,8 +524,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
510
524
|
}
|
|
511
525
|
const src = edgeById.get(edgeId);
|
|
512
526
|
setSelectedEdgeId(edgeId);
|
|
513
|
-
// A direct edge selection on the canvas supersedes any
|
|
514
|
-
|
|
527
|
+
// A direct edge selection on the canvas supersedes any walkthrough focus.
|
|
528
|
+
setFocusedWalkthroughId(null);
|
|
515
529
|
setFocusedStepIndex(null);
|
|
516
530
|
if (src) onEdgeSelect?.(src);
|
|
517
531
|
},
|
|
@@ -531,7 +545,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
531
545
|
}
|
|
532
546
|
setSelected(comp);
|
|
533
547
|
setSelectedEdgeId(null);
|
|
534
|
-
|
|
548
|
+
setFocusedWalkthroughId(null);
|
|
535
549
|
setFocusedStepIndex(null);
|
|
536
550
|
onSelect?.(id);
|
|
537
551
|
}
|
|
@@ -557,45 +571,45 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
557
571
|
// without a stale closure value.
|
|
558
572
|
const selectedEdgeIdRef = useRef<string | null>(null);
|
|
559
573
|
selectedEdgeIdRef.current = selectedEdgeId;
|
|
560
|
-
// Edge ids in
|
|
561
|
-
// step's edge). Used to frame the camera. `null` = no
|
|
574
|
+
// Edge ids in walkthrough focus (an active flow's edge set, or a single
|
|
575
|
+
// step's edge). Used to frame the camera. `null` = no walkthrough focus.
|
|
562
576
|
const focusEdgeIds = useMemo(() => {
|
|
563
|
-
if (
|
|
564
|
-
const tl =
|
|
577
|
+
if (focusedWalkthroughId == null || !walkthroughs) return null;
|
|
578
|
+
const tl = walkthroughs.find((t) => t.id === focusedWalkthroughId);
|
|
565
579
|
if (!tl) return null;
|
|
566
580
|
if (focusedStepIndex != null) {
|
|
567
581
|
const step = tl.steps[focusedStepIndex];
|
|
568
|
-
return step ? new Set([step
|
|
582
|
+
return step ? new Set([walkthroughStepGraphEdgeId(step)]) : null;
|
|
569
583
|
}
|
|
570
|
-
return new Set(tl.steps.map((s) => s
|
|
571
|
-
}, [
|
|
584
|
+
return new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s)));
|
|
585
|
+
}, [walkthroughs, focusedWalkthroughId, focusedStepIndex]);
|
|
572
586
|
|
|
573
587
|
// 1-based step numbers per edge of the active flow (focused or
|
|
574
588
|
// hover/autoplay-highlighted). An edge can appear in more than one step.
|
|
575
589
|
const selectedFlowStepNos = useMemo(() => {
|
|
576
|
-
const activeId =
|
|
577
|
-
if (activeId == null || !
|
|
578
|
-
const tl =
|
|
590
|
+
const activeId = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId;
|
|
591
|
+
if (activeId == null || !walkthroughs) return null;
|
|
592
|
+
const tl = walkthroughs.find((t) => t.id === activeId);
|
|
579
593
|
if (!tl) return null;
|
|
580
594
|
const map = new Map<string, number[]>();
|
|
581
595
|
tl.steps.forEach((s, i) => {
|
|
582
|
-
const list = map.get(s
|
|
596
|
+
const list = map.get(walkthroughStepGraphEdgeId(s)) ?? [];
|
|
583
597
|
list.push(i + 1);
|
|
584
|
-
map.set(s
|
|
598
|
+
map.set(walkthroughStepGraphEdgeId(s), list);
|
|
585
599
|
});
|
|
586
600
|
return map;
|
|
587
|
-
}, [
|
|
601
|
+
}, [walkthroughs, focusedWalkthroughId, hoveredWalkthroughStep]);
|
|
588
602
|
|
|
589
|
-
// Union of every expanded (opened)
|
|
603
|
+
// Union of every expanded (opened) walkthrough's edges — the visible set.
|
|
590
604
|
const openedEdgeIds = useMemo(() => {
|
|
591
|
-
if (!
|
|
605
|
+
if (!walkthroughs || expandedWalkthroughs.size === 0) return null;
|
|
592
606
|
const ids = new Set<string>();
|
|
593
|
-
for (const tl of
|
|
594
|
-
if (!
|
|
595
|
-
for (const s of tl.steps) ids.add(s
|
|
607
|
+
for (const tl of walkthroughs) {
|
|
608
|
+
if (!expandedWalkthroughs.has(tl.id)) continue;
|
|
609
|
+
for (const s of tl.steps) ids.add(walkthroughStepGraphEdgeId(s));
|
|
596
610
|
}
|
|
597
611
|
return ids.size > 0 ? ids : null;
|
|
598
|
-
}, [
|
|
612
|
+
}, [walkthroughs, expandedWalkthroughs]);
|
|
599
613
|
|
|
600
614
|
const endpointsOf = (edgeIds: ReadonlySet<string> | null): Set<string> | null => {
|
|
601
615
|
if (!edgeIds) return null;
|
|
@@ -609,15 +623,15 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
609
623
|
};
|
|
610
624
|
// Edge/nodes involved in the hovered step or whole flow.
|
|
611
625
|
const hoverEdgeIds = useMemo(() => {
|
|
612
|
-
if (!
|
|
613
|
-
const tl =
|
|
626
|
+
if (!hoveredWalkthroughStep || !walkthroughs) return null;
|
|
627
|
+
const tl = walkthroughs.find((t) => t.id === hoveredWalkthroughStep.walkthroughId);
|
|
614
628
|
if (!tl) return null;
|
|
615
|
-
if (
|
|
616
|
-
return new Set(tl.steps.map((s) => s
|
|
629
|
+
if (hoveredWalkthroughStep.stepIndex == null) {
|
|
630
|
+
return new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s)));
|
|
617
631
|
}
|
|
618
|
-
const step = tl.steps[
|
|
619
|
-
return step ? new Set([step
|
|
620
|
-
}, [
|
|
632
|
+
const step = tl.steps[hoveredWalkthroughStep.stepIndex];
|
|
633
|
+
return step ? new Set([walkthroughStepGraphEdgeId(step)]) : null;
|
|
634
|
+
}, [walkthroughs, hoveredWalkthroughStep]);
|
|
621
635
|
// While hovering with a *step* already selected, brighten the union of
|
|
622
636
|
// selected + hovered participants. Whole-flow focus (or no focus) keeps
|
|
623
637
|
// the old hover-replace preview so a step hover still dims the rest.
|
|
@@ -667,8 +681,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
667
681
|
// React Flow's own selection state from updating.
|
|
668
682
|
const dispNodes = useMemo(() => {
|
|
669
683
|
// While a step/flow is hovered, selected + hovered participants stay
|
|
670
|
-
// bright; everything else among the currently-visible (opened-
|
|
671
|
-
// nodes is dimmed. Nodes outside the opened-
|
|
684
|
+
// bright; everything else among the currently-visible (opened-walkthrough)
|
|
685
|
+
// nodes is dimmed. Nodes outside the opened-walkthrough set stay hidden.
|
|
672
686
|
return xyflowNodesBase.map((n) => {
|
|
673
687
|
// Boundary frames follow their members: hidden when no member is
|
|
674
688
|
// visible, dimmed when members are dimmed. Never selectable.
|
|
@@ -800,7 +814,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
800
814
|
(_e, node: Node) => {
|
|
801
815
|
const comp = (node.data as { component?: SubsystemComponent } | undefined)?.component;
|
|
802
816
|
setSelectedEdgeId(null);
|
|
803
|
-
|
|
817
|
+
setFocusedWalkthroughId(null);
|
|
804
818
|
setFocusedStepIndex(null);
|
|
805
819
|
if (node.type === 'subsystem-component' && comp) {
|
|
806
820
|
// Clicking the already-selected node unselects it (toggle off).
|
|
@@ -827,14 +841,14 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
827
841
|
const onPaneClick = useCallback(() => {
|
|
828
842
|
setSelected(null);
|
|
829
843
|
setSelectedEdgeId(null);
|
|
830
|
-
|
|
844
|
+
setFocusedWalkthroughId(null);
|
|
831
845
|
setFocusedStepIndex(null);
|
|
832
846
|
}, []);
|
|
833
847
|
|
|
834
848
|
// Sidebar file trees — one per repo on multi-repo graphs, each under its
|
|
835
849
|
// own owner-avatar header. Clicking a header collapses that repo's tree.
|
|
836
850
|
const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
|
|
837
|
-
const
|
|
851
|
+
const hasWalkthroughs = useMemo(() => (walkthroughs?.length ?? 0) > 0, [walkthroughs]);
|
|
838
852
|
const [collapsedRepos, setCollapsedRepos] = useState<Set<string>>(new Set());
|
|
839
853
|
const toggleRepoCollapsed = useCallback((key: string) => {
|
|
840
854
|
setCollapsedRepos((prev) => {
|
|
@@ -876,13 +890,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
876
890
|
[onFileSelect],
|
|
877
891
|
);
|
|
878
892
|
|
|
879
|
-
// Camera helper shared by the
|
|
893
|
+
// Camera helper shared by the walkthrough interactions: frames the focused
|
|
880
894
|
// edges' endpoint nodes via `fitView({ nodes })`, which uses the store's
|
|
881
895
|
// live positions + measured dims — so the frame always includes BOTH
|
|
882
896
|
// components the edge attaches to (and therefore the edge line between them).
|
|
883
897
|
const fitFocusBounds = useCallback(
|
|
884
898
|
(ids: ReadonlySet<string>) => {
|
|
885
|
-
if (!
|
|
899
|
+
if (!zoomOnWalkthroughFocus) return;
|
|
886
900
|
const nodeIds = new Set<string>();
|
|
887
901
|
for (const e of baseEdges) {
|
|
888
902
|
if (!ids.has(e.id)) continue;
|
|
@@ -896,13 +910,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
896
910
|
duration: 300,
|
|
897
911
|
});
|
|
898
912
|
},
|
|
899
|
-
[baseEdges, fitView,
|
|
913
|
+
[baseEdges, fitView, zoomOnWalkthroughFocus],
|
|
900
914
|
);
|
|
901
915
|
|
|
902
|
-
// Zoom back out to the full diagram after the last expanded
|
|
916
|
+
// Zoom back out to the full diagram after the last expanded walkthrough
|
|
903
917
|
// closes (visibility restores non-flow nodes that were hidden).
|
|
904
918
|
const fitOverview = useCallback(() => {
|
|
905
|
-
if (!
|
|
919
|
+
if (!zoomOnWalkthroughFocus) return;
|
|
906
920
|
fitView({
|
|
907
921
|
padding: 0.1,
|
|
908
922
|
includeHiddenNodes: false,
|
|
@@ -910,83 +924,83 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
910
924
|
maxZoom: 2,
|
|
911
925
|
duration: 300,
|
|
912
926
|
});
|
|
913
|
-
}, [fitView,
|
|
927
|
+
}, [fitView, zoomOnWalkthroughFocus]);
|
|
914
928
|
|
|
915
929
|
// Focus an entire flow: hide everything but the flow's nodes and edges, and
|
|
916
930
|
// frame the flow on the canvas. Selection state is cleared — the graph now
|
|
917
931
|
// reads as the narrative. No drawer: the code view only opens on a step
|
|
918
|
-
// click. A stale
|
|
932
|
+
// click. A stale walkthrough drawer (from a previously focused flow's step)
|
|
919
933
|
// closes; an explicitly opened file drawer stays.
|
|
920
934
|
// In `dim` mode: clear step highlight and leave the full graph visible
|
|
921
935
|
// (no zoom / hide) — matching "step away from a hovered step".
|
|
922
|
-
const
|
|
923
|
-
(tl:
|
|
936
|
+
const focusWalkthroughEdges = useCallback(
|
|
937
|
+
(tl: SubsystemWalkthrough) => {
|
|
924
938
|
setSelected(null);
|
|
925
939
|
setSelectedEdgeId(null);
|
|
926
|
-
|
|
927
|
-
if (
|
|
940
|
+
setHoveredWalkthroughStep(null);
|
|
941
|
+
if (walkthroughStepMode === 'dim') {
|
|
928
942
|
setFocusedStepIndex(null);
|
|
929
|
-
|
|
930
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
943
|
+
setFocusedWalkthroughId(null);
|
|
944
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
931
945
|
return;
|
|
932
946
|
}
|
|
933
947
|
setFocusedStepIndex(null);
|
|
934
|
-
|
|
935
|
-
fitFocusBounds(new Set(tl.steps.map((s) => s
|
|
936
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
948
|
+
setFocusedWalkthroughId(tl.id);
|
|
949
|
+
fitFocusBounds(new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s))));
|
|
950
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
937
951
|
},
|
|
938
|
-
[fitFocusBounds,
|
|
952
|
+
[fitFocusBounds, walkthroughStepMode],
|
|
939
953
|
);
|
|
940
954
|
|
|
941
|
-
const
|
|
942
|
-
|
|
955
|
+
const clearWalkthroughFocus = useCallback(() => {
|
|
956
|
+
setFocusedWalkthroughId(null);
|
|
943
957
|
setFocusedStepIndex(null);
|
|
944
|
-
|
|
945
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
958
|
+
setHoveredWalkthroughStep(null);
|
|
959
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
946
960
|
}, []);
|
|
947
961
|
|
|
948
|
-
// Focus a single step's edge on the canvas and open/scroll the
|
|
962
|
+
// Focus a single step's edge on the canvas and open/scroll the walkthrough
|
|
949
963
|
// drawer to that step's snippet. Clicking the already-focused step clears
|
|
950
964
|
// step focus and returns to whole-flow (high-level) framing.
|
|
951
965
|
// In `dim` mode: only dim non-participants (same as hovering a step) —
|
|
952
966
|
// no camera move, no drawer, no hide.
|
|
953
|
-
const
|
|
954
|
-
(tl:
|
|
967
|
+
const focusWalkthroughStep = useCallback(
|
|
968
|
+
(tl: SubsystemWalkthrough, stepIndex: number) => {
|
|
955
969
|
const step = tl.steps[stepIndex];
|
|
956
970
|
if (!step) return;
|
|
957
971
|
setSelected(null);
|
|
958
972
|
setSelectedEdgeId(null);
|
|
959
|
-
if (
|
|
973
|
+
if (walkthroughStepMode === 'dim') {
|
|
960
974
|
setFocusedStepIndex(null);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
975
|
+
setFocusedWalkthroughId(null);
|
|
976
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex });
|
|
977
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
964
978
|
return;
|
|
965
979
|
}
|
|
966
980
|
// Toggle: clicking the selected step unselects it (back to whole flow).
|
|
967
|
-
if (
|
|
981
|
+
if (focusedWalkthroughId === tl.id && focusedStepIndex === stepIndex) {
|
|
968
982
|
setFocusedStepIndex(null);
|
|
969
983
|
// Pointer is still over the row — keep hover preview so dimming doesn't
|
|
970
984
|
// flash off until mouseleave.
|
|
971
|
-
|
|
972
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
985
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex });
|
|
986
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
973
987
|
if (pendingFocusFitRef.current != null) {
|
|
974
988
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
975
989
|
pendingFocusFitRef.current = null;
|
|
976
990
|
}
|
|
977
|
-
fitFocusBounds(new Set(tl.steps.map((s) => s
|
|
991
|
+
fitFocusBounds(new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s))));
|
|
978
992
|
return;
|
|
979
993
|
}
|
|
980
994
|
setFocusedStepIndex(stepIndex);
|
|
981
|
-
|
|
982
|
-
|
|
995
|
+
setFocusedWalkthroughId(tl.id);
|
|
996
|
+
setHoveredWalkthroughStep(null);
|
|
983
997
|
// Open the drawer before fitting. If it was closed, wait for its height
|
|
984
998
|
// transition so fitView uses the reduced canvas — not full height.
|
|
985
999
|
const drawerWasOpen = drawerOpenRef.current;
|
|
986
|
-
if (
|
|
1000
|
+
if (renderWalkthroughViewer) {
|
|
987
1001
|
setDrawerTarget({
|
|
988
|
-
kind: '
|
|
989
|
-
|
|
1002
|
+
kind: 'walkthrough',
|
|
1003
|
+
walkthroughId: tl.id,
|
|
990
1004
|
stepIndex,
|
|
991
1005
|
});
|
|
992
1006
|
} else {
|
|
@@ -996,7 +1010,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
996
1010
|
startLine: step.line,
|
|
997
1011
|
});
|
|
998
1012
|
}
|
|
999
|
-
const edgeIds = new Set([step
|
|
1013
|
+
const edgeIds = new Set([walkthroughStepGraphEdgeId(step)]);
|
|
1000
1014
|
if (pendingFocusFitRef.current != null) {
|
|
1001
1015
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
1002
1016
|
pendingFocusFitRef.current = null;
|
|
@@ -1013,9 +1027,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1013
1027
|
[
|
|
1014
1028
|
fitFocusBounds,
|
|
1015
1029
|
focusedStepIndex,
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1030
|
+
focusedWalkthroughId,
|
|
1031
|
+
renderWalkthroughViewer,
|
|
1032
|
+
walkthroughStepMode,
|
|
1019
1033
|
],
|
|
1020
1034
|
);
|
|
1021
1035
|
|
|
@@ -1037,9 +1051,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1037
1051
|
const prev = prevPreviewEdgeIdsRef.current;
|
|
1038
1052
|
prevPreviewEdgeIdsRef.current = previewEdgeIds;
|
|
1039
1053
|
|
|
1040
|
-
if (!
|
|
1054
|
+
if (!zoomOnWalkthroughFocus || walkthroughStepMode === 'dim') return;
|
|
1041
1055
|
// Camera follows hover only when a specific step is already focused.
|
|
1042
|
-
if (
|
|
1056
|
+
if (focusedWalkthroughId == null || focusedStepIndex == null) return;
|
|
1043
1057
|
|
|
1044
1058
|
if (previewEdgeIds) {
|
|
1045
1059
|
fitFocusBounds(previewEdgeIds);
|
|
@@ -1051,33 +1065,33 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1051
1065
|
}, [
|
|
1052
1066
|
previewEdgeIds,
|
|
1053
1067
|
focusEdgeIds,
|
|
1054
|
-
|
|
1068
|
+
focusedWalkthroughId,
|
|
1055
1069
|
focusedStepIndex,
|
|
1056
1070
|
fitFocusBounds,
|
|
1057
|
-
|
|
1058
|
-
|
|
1071
|
+
zoomOnWalkthroughFocus,
|
|
1072
|
+
walkthroughStepMode,
|
|
1059
1073
|
]);
|
|
1060
1074
|
|
|
1061
|
-
// Graph-only embeds: cycle
|
|
1075
|
+
// Graph-only embeds: cycle walkthrough steps with hover-style dimming.
|
|
1062
1076
|
useEffect(() => {
|
|
1063
|
-
if (!
|
|
1064
|
-
const playable =
|
|
1077
|
+
if (!autoPlayWalkthroughs || !walkthroughs?.length || !layoutReady) return;
|
|
1078
|
+
const playable = walkthroughs.filter((tl) => tl.steps.length > 0);
|
|
1065
1079
|
if (playable.length === 0) return;
|
|
1066
1080
|
|
|
1067
1081
|
let cancelled = false;
|
|
1068
1082
|
let tlIdx = 0;
|
|
1069
1083
|
let stepIdx = 0;
|
|
1070
1084
|
let timer: number | null = null;
|
|
1071
|
-
const interval = Math.max(400,
|
|
1085
|
+
const interval = Math.max(400, walkthroughAutoPlayIntervalMs);
|
|
1072
1086
|
|
|
1073
1087
|
const tick = () => {
|
|
1074
1088
|
if (cancelled) return;
|
|
1075
1089
|
const tl = playable[tlIdx]!;
|
|
1076
1090
|
setSelected(null);
|
|
1077
1091
|
setSelectedEdgeId(null);
|
|
1078
|
-
|
|
1092
|
+
setFocusedWalkthroughId(null);
|
|
1079
1093
|
setFocusedStepIndex(null);
|
|
1080
|
-
|
|
1094
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: stepIdx });
|
|
1081
1095
|
stepIdx += 1;
|
|
1082
1096
|
if (stepIdx >= tl.steps.length) {
|
|
1083
1097
|
stepIdx = 0;
|
|
@@ -1090,17 +1104,17 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1090
1104
|
return () => {
|
|
1091
1105
|
cancelled = true;
|
|
1092
1106
|
if (timer != null) window.clearTimeout(timer);
|
|
1093
|
-
|
|
1107
|
+
setHoveredWalkthroughStep(null);
|
|
1094
1108
|
};
|
|
1095
|
-
}, [
|
|
1109
|
+
}, [autoPlayWalkthroughs, walkthroughs, walkthroughAutoPlayIntervalMs, layoutReady]);
|
|
1096
1110
|
|
|
1097
|
-
// Arrow keys step through the focused
|
|
1111
|
+
// Arrow keys step through the focused walkthrough once a step is active
|
|
1098
1112
|
// (sidebar click or drawer open). Ignores typing targets and chords.
|
|
1099
1113
|
useEffect(() => {
|
|
1100
|
-
if (
|
|
1114
|
+
if (focusedWalkthroughId == null || focusedStepIndex == null || !walkthroughs) {
|
|
1101
1115
|
return;
|
|
1102
1116
|
}
|
|
1103
|
-
const tl =
|
|
1117
|
+
const tl = walkthroughs.find((t) => t.id === focusedWalkthroughId);
|
|
1104
1118
|
if (!tl || tl.steps.length === 0) return;
|
|
1105
1119
|
|
|
1106
1120
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
@@ -1126,31 +1140,31 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1126
1140
|
}
|
|
1127
1141
|
if (next === focusedStepIndex) return;
|
|
1128
1142
|
e.preventDefault();
|
|
1129
|
-
|
|
1143
|
+
focusWalkthroughStep(tl, next);
|
|
1130
1144
|
};
|
|
1131
1145
|
|
|
1132
1146
|
window.addEventListener('keydown', onKeyDown);
|
|
1133
1147
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
1134
1148
|
}, [
|
|
1135
|
-
|
|
1149
|
+
focusedWalkthroughId,
|
|
1136
1150
|
focusedStepIndex,
|
|
1137
|
-
|
|
1138
|
-
|
|
1151
|
+
walkthroughs,
|
|
1152
|
+
focusWalkthroughStep,
|
|
1139
1153
|
]);
|
|
1140
1154
|
|
|
1141
|
-
const
|
|
1155
|
+
const toggleWalkthroughCollapsed = useCallback(
|
|
1142
1156
|
(tlId: string) => {
|
|
1143
1157
|
const collapsingLast =
|
|
1144
|
-
|
|
1158
|
+
expandedWalkthroughs.has(tlId) && expandedWalkthroughs.size === 1;
|
|
1145
1159
|
|
|
1146
|
-
|
|
1160
|
+
setExpandedWalkthroughs((prev) => {
|
|
1147
1161
|
const next = new Set(prev);
|
|
1148
1162
|
if (next.has(tlId)) next.delete(tlId);
|
|
1149
1163
|
else next.add(tlId);
|
|
1150
1164
|
return next;
|
|
1151
1165
|
});
|
|
1152
1166
|
|
|
1153
|
-
if (!collapsingLast || !
|
|
1167
|
+
if (!collapsingLast || !zoomOnWalkthroughFocus) return;
|
|
1154
1168
|
|
|
1155
1169
|
// Cancel any pending focus fit so it doesn't fight the overview zoom.
|
|
1156
1170
|
if (pendingFocusFitRef.current != null) {
|
|
@@ -1170,7 +1184,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1170
1184
|
drawerWasOpen ? FILE_DRAWER_HEIGHT_MS + 20 : 20,
|
|
1171
1185
|
);
|
|
1172
1186
|
},
|
|
1173
|
-
[
|
|
1187
|
+
[expandedWalkthroughs, fitOverview, zoomOnWalkthroughFocus],
|
|
1174
1188
|
);
|
|
1175
1189
|
|
|
1176
1190
|
// Filename-badge clicks on nodes open the drawer through the same path as
|
|
@@ -1200,7 +1214,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1200
1214
|
if (!comp || comp.id === selectedRef.current?.id) return;
|
|
1201
1215
|
setSelected(comp);
|
|
1202
1216
|
setSelectedEdgeId(null);
|
|
1203
|
-
|
|
1217
|
+
setFocusedWalkthroughId(null);
|
|
1204
1218
|
setFocusedStepIndex(null);
|
|
1205
1219
|
onSelect?.(comp.id);
|
|
1206
1220
|
},
|
|
@@ -1266,11 +1280,11 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1266
1280
|
[],
|
|
1267
1281
|
);
|
|
1268
1282
|
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
const
|
|
1272
|
-
(ctx:
|
|
1273
|
-
|
|
1283
|
+
const walkthroughViewerRef = useRef(renderWalkthroughViewer);
|
|
1284
|
+
walkthroughViewerRef.current = renderWalkthroughViewer;
|
|
1285
|
+
const renderWalkthroughDrawerContent = useCallback(
|
|
1286
|
+
(ctx: WalkthroughViewerContext) =>
|
|
1287
|
+
walkthroughViewerRef.current?.(ctx) ?? null,
|
|
1274
1288
|
[],
|
|
1275
1289
|
);
|
|
1276
1290
|
|
|
@@ -1279,7 +1293,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1279
1293
|
{/* Sidebar: scrollable title/description on top, files or flows pinned below.
|
|
1280
1294
|
The description hides by default so files/flows get the room; the
|
|
1281
1295
|
title-row toggle reveals it, and the lower panel yields back to 50%. */}
|
|
1282
|
-
{!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 ||
|
|
1296
|
+
{!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1283
1297
|
<div
|
|
1284
1298
|
style={{
|
|
1285
1299
|
width: sidebarWidth,
|
|
@@ -1368,7 +1382,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1368
1382
|
)}
|
|
1369
1383
|
{sidebarAfterDescription}
|
|
1370
1384
|
</div>
|
|
1371
|
-
{(treeFilePaths.length > 0 ||
|
|
1385
|
+
{(treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1372
1386
|
<div
|
|
1373
1387
|
style={{
|
|
1374
1388
|
...(showDesc ? { height: '50%' as const } : { flex: 1, minHeight: 0 }),
|
|
@@ -1380,7 +1394,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1380
1394
|
overflow: 'hidden',
|
|
1381
1395
|
}}
|
|
1382
1396
|
>
|
|
1383
|
-
{
|
|
1397
|
+
{hasWalkthroughs && (
|
|
1384
1398
|
<div
|
|
1385
1399
|
role="tablist"
|
|
1386
1400
|
aria-label="Sidebar view"
|
|
@@ -1392,7 +1406,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1392
1406
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
1393
1407
|
}}
|
|
1394
1408
|
>
|
|
1395
|
-
{(['
|
|
1409
|
+
{(['walkthroughs', 'files'] as const).map((view) => (
|
|
1396
1410
|
<button
|
|
1397
1411
|
key={view}
|
|
1398
1412
|
type="button"
|
|
@@ -1416,12 +1430,12 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1416
1430
|
cursor: 'pointer',
|
|
1417
1431
|
}}
|
|
1418
1432
|
>
|
|
1419
|
-
{view}
|
|
1433
|
+
{view === 'walkthroughs' ? 'Walkthroughs' : 'Files'}
|
|
1420
1434
|
</button>
|
|
1421
1435
|
))}
|
|
1422
1436
|
</div>
|
|
1423
1437
|
)}
|
|
1424
|
-
{sidebarView === '
|
|
1438
|
+
{sidebarView === 'walkthroughs' && walkthroughs && walkthroughs.length > 0 ? (
|
|
1425
1439
|
<div
|
|
1426
1440
|
style={{
|
|
1427
1441
|
flex: 1,
|
|
@@ -1430,30 +1444,29 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1430
1444
|
padding: '0 0 12px',
|
|
1431
1445
|
}}
|
|
1432
1446
|
>
|
|
1433
|
-
{
|
|
1434
|
-
<
|
|
1447
|
+
{walkthroughs.map((tl) => (
|
|
1448
|
+
<WalkthroughFlow
|
|
1435
1449
|
key={tl.id}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
collapsed={!expandedThroughlines.has(tl.id)}
|
|
1450
|
+
walkthrough={tl}
|
|
1451
|
+
collapsed={!expandedWalkthroughs.has(tl.id)}
|
|
1439
1452
|
active={
|
|
1440
|
-
|
|
1453
|
+
focusedWalkthroughId === tl.id
|
|
1441
1454
|
? { stepIndex: focusedStepIndex }
|
|
1442
|
-
:
|
|
1443
|
-
? { stepIndex:
|
|
1455
|
+
: hoveredWalkthroughStep?.walkthroughId === tl.id
|
|
1456
|
+
? { stepIndex: hoveredWalkthroughStep.stepIndex }
|
|
1444
1457
|
: null
|
|
1445
1458
|
}
|
|
1446
|
-
onToggleCollapsed={
|
|
1447
|
-
onFocusFlow={
|
|
1448
|
-
onClearFocus={
|
|
1449
|
-
onFocusStep={
|
|
1459
|
+
onToggleCollapsed={toggleWalkthroughCollapsed}
|
|
1460
|
+
onFocusFlow={focusWalkthroughEdges}
|
|
1461
|
+
onClearFocus={clearWalkthroughFocus}
|
|
1462
|
+
onFocusStep={focusWalkthroughStep}
|
|
1450
1463
|
onHoverStep={(tl, i) =>
|
|
1451
|
-
|
|
1464
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: i })
|
|
1452
1465
|
}
|
|
1453
1466
|
onHoverFlow={(tl) =>
|
|
1454
|
-
|
|
1467
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: null })
|
|
1455
1468
|
}
|
|
1456
|
-
onLeaveStep={() =>
|
|
1469
|
+
onLeaveStep={() => setHoveredWalkthroughStep(null)}
|
|
1457
1470
|
/>
|
|
1458
1471
|
))}
|
|
1459
1472
|
</div>
|
|
@@ -1500,7 +1513,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1500
1513
|
)}
|
|
1501
1514
|
|
|
1502
1515
|
{/* Drag handle between sidebar and canvas — resize the left panel. */}
|
|
1503
|
-
{!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 ||
|
|
1516
|
+
{!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (
|
|
1504
1517
|
<div
|
|
1505
1518
|
onMouseDown={onSidebarResizeStart}
|
|
1506
1519
|
aria-label="Resize sidebar"
|
|
@@ -1625,9 +1638,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1625
1638
|
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
|
|
1626
1639
|
<Controls showZoom showFitView showInteractive />
|
|
1627
1640
|
</ReactFlow>
|
|
1628
|
-
{/* Graph /
|
|
1641
|
+
{/* Graph / walkthrough / step titles — non-interactive chips at the top
|
|
1629
1642
|
of the canvas. Graph-only embeds use these without opening the sidebar. */}
|
|
1630
|
-
{(graphTitle ||
|
|
1643
|
+
{(graphTitle || overlayWalkthroughTitle || overlayWalkthroughStep) && (
|
|
1631
1644
|
<div
|
|
1632
1645
|
style={{
|
|
1633
1646
|
position: 'absolute',
|
|
@@ -1664,11 +1677,11 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1664
1677
|
{graphTitle}
|
|
1665
1678
|
</div>
|
|
1666
1679
|
)}
|
|
1667
|
-
{
|
|
1680
|
+
{overlayWalkthroughTitle && (
|
|
1668
1681
|
<div
|
|
1669
1682
|
style={{
|
|
1670
1683
|
maxWidth: '100%',
|
|
1671
|
-
minWidth:
|
|
1684
|
+
minWidth: overlayWalkthroughStep ? 160 : undefined,
|
|
1672
1685
|
display: 'flex',
|
|
1673
1686
|
flexDirection: 'column',
|
|
1674
1687
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
@@ -1679,9 +1692,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1679
1692
|
opacity: 0.95,
|
|
1680
1693
|
}}
|
|
1681
1694
|
aria-label={
|
|
1682
|
-
|
|
1683
|
-
? `${
|
|
1684
|
-
:
|
|
1695
|
+
overlayWalkthroughStep
|
|
1696
|
+
? `${overlayWalkthroughTitle}, step ${overlayWalkthroughStep.index} of ${overlayWalkthroughStep.total}`
|
|
1697
|
+
: overlayWalkthroughTitle
|
|
1685
1698
|
}
|
|
1686
1699
|
>
|
|
1687
1700
|
<div
|
|
@@ -1697,9 +1710,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1697
1710
|
textAlign: 'center',
|
|
1698
1711
|
}}
|
|
1699
1712
|
>
|
|
1700
|
-
{
|
|
1713
|
+
{overlayWalkthroughTitle}
|
|
1701
1714
|
</div>
|
|
1702
|
-
{
|
|
1715
|
+
{overlayWalkthroughStep && overlayWalkthroughStep.total > 0 && (
|
|
1703
1716
|
<div
|
|
1704
1717
|
style={{
|
|
1705
1718
|
display: 'flex',
|
|
@@ -1708,10 +1721,10 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1708
1721
|
}}
|
|
1709
1722
|
aria-hidden="true"
|
|
1710
1723
|
>
|
|
1711
|
-
{Array.from({ length:
|
|
1724
|
+
{Array.from({ length: overlayWalkthroughStep.total }, (_, i) => {
|
|
1712
1725
|
const n = i + 1;
|
|
1713
|
-
const active = n ===
|
|
1714
|
-
const done = n <
|
|
1726
|
+
const active = n === overlayWalkthroughStep.index;
|
|
1727
|
+
const done = n < overlayWalkthroughStep.index;
|
|
1715
1728
|
return (
|
|
1716
1729
|
<span
|
|
1717
1730
|
key={n}
|
|
@@ -1734,7 +1747,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1734
1747
|
)}
|
|
1735
1748
|
</div>
|
|
1736
1749
|
)}
|
|
1737
|
-
{
|
|
1750
|
+
{overlayWalkthroughStep?.annotation && (
|
|
1738
1751
|
<div
|
|
1739
1752
|
style={{
|
|
1740
1753
|
maxWidth: '100%',
|
|
@@ -1750,7 +1763,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1750
1763
|
lineHeight: 1.35,
|
|
1751
1764
|
}}
|
|
1752
1765
|
>
|
|
1753
|
-
{
|
|
1766
|
+
{overlayWalkthroughStep.annotation}
|
|
1754
1767
|
</div>
|
|
1755
1768
|
)}
|
|
1756
1769
|
</div>
|
|
@@ -1784,12 +1797,12 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1784
1797
|
)}
|
|
1785
1798
|
</div>
|
|
1786
1799
|
<FileDrawer title={drawerTitle} onClose={() => setDrawerTarget(null)}>
|
|
1787
|
-
{drawerTarget?.kind === '
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
<
|
|
1791
|
-
render={
|
|
1792
|
-
|
|
1800
|
+
{drawerTarget?.kind === 'walkthrough' &&
|
|
1801
|
+
focusedWalkthrough &&
|
|
1802
|
+
renderWalkthroughViewer ? (
|
|
1803
|
+
<WalkthroughDrawerContent
|
|
1804
|
+
render={renderWalkthroughDrawerContent}
|
|
1805
|
+
walkthrough={focusedWalkthrough}
|
|
1793
1806
|
stepIndex={drawerTarget.stepIndex}
|
|
1794
1807
|
/>
|
|
1795
1808
|
) : drawerTarget?.kind === 'file' ? (
|
|
@@ -1887,13 +1900,12 @@ function RepoGroupHeader({
|
|
|
1887
1900
|
);
|
|
1888
1901
|
}
|
|
1889
1902
|
|
|
1890
|
-
/** One collapsible
|
|
1903
|
+
/** One collapsible walkthrough in the sidebar's flows panel. Clicking the
|
|
1891
1904
|
* title: closed → open + select; open and unselected → select; open and
|
|
1892
1905
|
* selected → close + clear focus. The right-aligned close button collapses
|
|
1893
1906
|
* without selecting. A step row focuses that step's edge. */
|
|
1894
|
-
function
|
|
1895
|
-
|
|
1896
|
-
edges,
|
|
1907
|
+
function WalkthroughFlow({
|
|
1908
|
+
walkthrough,
|
|
1897
1909
|
collapsed,
|
|
1898
1910
|
active,
|
|
1899
1911
|
onToggleCollapsed,
|
|
@@ -1904,24 +1916,22 @@ function ThroughlineFlow({
|
|
|
1904
1916
|
onHoverFlow,
|
|
1905
1917
|
onLeaveStep,
|
|
1906
1918
|
}: {
|
|
1907
|
-
|
|
1908
|
-
edges: SubsystemComponentEdge[];
|
|
1919
|
+
walkthrough: SubsystemWalkthrough;
|
|
1909
1920
|
collapsed: boolean;
|
|
1910
1921
|
/** `{ stepIndex: null }` = whole flow focused; `{ stepIndex }` = one step. */
|
|
1911
1922
|
active: { stepIndex: number | null } | null;
|
|
1912
1923
|
onToggleCollapsed: (tlId: string) => void;
|
|
1913
|
-
onFocusFlow: (tl:
|
|
1924
|
+
onFocusFlow: (tl: SubsystemWalkthrough) => void;
|
|
1914
1925
|
onClearFocus: () => void;
|
|
1915
|
-
onFocusStep: (tl:
|
|
1916
|
-
onHoverStep: (tl:
|
|
1926
|
+
onFocusStep: (tl: SubsystemWalkthrough, stepIndex: number) => void;
|
|
1927
|
+
onHoverStep: (tl: SubsystemWalkthrough, stepIndex: number) => void;
|
|
1917
1928
|
/** Preview the whole flow on the canvas (used while the row is collapsed). */
|
|
1918
|
-
onHoverFlow: (tl:
|
|
1929
|
+
onHoverFlow: (tl: SubsystemWalkthrough) => void;
|
|
1919
1930
|
onLeaveStep: () => void;
|
|
1920
1931
|
}) {
|
|
1921
1932
|
const { theme } = useTheme();
|
|
1922
1933
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
1923
1934
|
const hoverBg = theme.colors.background;
|
|
1924
|
-
const edgeById = useMemo(() => new Map(edges.map((e) => [e.id, e])), [edges]);
|
|
1925
1935
|
const wholeFlowActive = active !== null && active.stepIndex === null;
|
|
1926
1936
|
const [headerHover, setHeaderHover] = useState(false);
|
|
1927
1937
|
const [closeHover, setCloseHover] = useState(false);
|
|
@@ -1945,9 +1955,9 @@ function ThroughlineFlow({
|
|
|
1945
1955
|
useEffect(() => () => stopPlaying(), []);
|
|
1946
1956
|
|
|
1947
1957
|
const startPlaying = useCallback(() => {
|
|
1948
|
-
if (collapsed) onToggleCollapsed(
|
|
1949
|
-
if (active === null || active.stepIndex !== null) onFocusFlow(
|
|
1950
|
-
const stepCount =
|
|
1958
|
+
if (collapsed) onToggleCollapsed(walkthrough.id);
|
|
1959
|
+
if (active === null || active.stepIndex !== null) onFocusFlow(walkthrough);
|
|
1960
|
+
const stepCount = walkthrough.steps.length;
|
|
1951
1961
|
if (stepCount === 0) return;
|
|
1952
1962
|
setPlaying(true);
|
|
1953
1963
|
let i = 0;
|
|
@@ -1957,12 +1967,12 @@ function ThroughlineFlow({
|
|
|
1957
1967
|
setPlaying(false);
|
|
1958
1968
|
return;
|
|
1959
1969
|
}
|
|
1960
|
-
onFocusStep(
|
|
1970
|
+
onFocusStep(walkthrough, i);
|
|
1961
1971
|
i += 1;
|
|
1962
|
-
playTimerRef.current = window.setTimeout(tick,
|
|
1972
|
+
playTimerRef.current = window.setTimeout(tick, WALKTHROUGH_PLAY_PAUSE_MS);
|
|
1963
1973
|
};
|
|
1964
1974
|
tick();
|
|
1965
|
-
}, [collapsed, active, onToggleCollapsed, onFocusFlow,
|
|
1975
|
+
}, [collapsed, active, onToggleCollapsed, onFocusFlow, walkthrough, onFocusStep]);
|
|
1966
1976
|
|
|
1967
1977
|
const togglePlay = useCallback(() => {
|
|
1968
1978
|
if (playing) {
|
|
@@ -1985,7 +1995,7 @@ function ThroughlineFlow({
|
|
|
1985
1995
|
<div
|
|
1986
1996
|
onMouseEnter={() => {
|
|
1987
1997
|
setHeaderHover(true);
|
|
1988
|
-
if (collapsed) onHoverFlow(
|
|
1998
|
+
if (collapsed) onHoverFlow(walkthrough);
|
|
1989
1999
|
}}
|
|
1990
2000
|
onMouseLeave={() => {
|
|
1991
2001
|
setHeaderHover(false);
|
|
@@ -2004,12 +2014,12 @@ function ThroughlineFlow({
|
|
|
2004
2014
|
type="button"
|
|
2005
2015
|
onClick={() => {
|
|
2006
2016
|
if (collapsed) {
|
|
2007
|
-
onToggleCollapsed(
|
|
2008
|
-
onFocusFlow(
|
|
2017
|
+
onToggleCollapsed(walkthrough.id);
|
|
2018
|
+
onFocusFlow(walkthrough);
|
|
2009
2019
|
} else if (active === null) {
|
|
2010
|
-
onFocusFlow(
|
|
2020
|
+
onFocusFlow(walkthrough);
|
|
2011
2021
|
} else {
|
|
2012
|
-
onToggleCollapsed(
|
|
2022
|
+
onToggleCollapsed(walkthrough.id);
|
|
2013
2023
|
onClearFocus();
|
|
2014
2024
|
}
|
|
2015
2025
|
}}
|
|
@@ -2036,14 +2046,14 @@ function ThroughlineFlow({
|
|
|
2036
2046
|
color: theme.colors.text,
|
|
2037
2047
|
}}
|
|
2038
2048
|
>
|
|
2039
|
-
{
|
|
2049
|
+
{walkthrough.title}
|
|
2040
2050
|
</span>
|
|
2041
2051
|
</button>
|
|
2042
2052
|
{!collapsed && (
|
|
2043
2053
|
<>
|
|
2044
2054
|
<button
|
|
2045
2055
|
type="button"
|
|
2046
|
-
aria-label={playing ? `Pause ${
|
|
2056
|
+
aria-label={playing ? `Pause ${walkthrough.title} autoplay` : `Play ${walkthrough.title}`}
|
|
2047
2057
|
title={playing ? 'Pause' : 'Play through steps'}
|
|
2048
2058
|
onMouseEnter={() => setPlayHover(true)}
|
|
2049
2059
|
onMouseLeave={() => setPlayHover(false)}
|
|
@@ -2071,12 +2081,12 @@ function ThroughlineFlow({
|
|
|
2071
2081
|
</button>
|
|
2072
2082
|
<button
|
|
2073
2083
|
type="button"
|
|
2074
|
-
aria-label={`Close ${
|
|
2084
|
+
aria-label={`Close ${walkthrough.title}`}
|
|
2075
2085
|
onMouseEnter={() => setCloseHover(true)}
|
|
2076
2086
|
onMouseLeave={() => setCloseHover(false)}
|
|
2077
2087
|
onClick={(e) => {
|
|
2078
2088
|
e.stopPropagation();
|
|
2079
|
-
onToggleCollapsed(
|
|
2089
|
+
onToggleCollapsed(walkthrough.id);
|
|
2080
2090
|
if (active !== null) onClearFocus();
|
|
2081
2091
|
}}
|
|
2082
2092
|
style={{
|
|
@@ -2108,23 +2118,22 @@ function ThroughlineFlow({
|
|
|
2108
2118
|
onLeaveStep();
|
|
2109
2119
|
}}
|
|
2110
2120
|
>
|
|
2111
|
-
{
|
|
2112
|
-
const
|
|
2113
|
-
const mech = edge?.mechanism;
|
|
2121
|
+
{walkthrough.steps.map((step, i) => {
|
|
2122
|
+
const mech = step.mechanism;
|
|
2114
2123
|
const color = mech ? MECHANISM_COLOR[mech] : muted;
|
|
2115
2124
|
const stepActive = active !== null && active.stepIndex === i;
|
|
2116
2125
|
return (
|
|
2117
2126
|
<button
|
|
2118
|
-
key={`${step
|
|
2127
|
+
key={`${walkthroughStepGraphEdgeId(step)}-${i}`}
|
|
2119
2128
|
ref={(el) => {
|
|
2120
2129
|
stepButtonRefs.current[i] = el;
|
|
2121
2130
|
}}
|
|
2122
2131
|
type="button"
|
|
2123
2132
|
onMouseEnter={() => {
|
|
2124
2133
|
setHoveredStep(i);
|
|
2125
|
-
onHoverStep(
|
|
2134
|
+
onHoverStep(walkthrough, i);
|
|
2126
2135
|
}}
|
|
2127
|
-
onClick={() => onFocusStep(
|
|
2136
|
+
onClick={() => onFocusStep(walkthrough, i)}
|
|
2128
2137
|
style={{
|
|
2129
2138
|
display: 'flex',
|
|
2130
2139
|
alignItems: 'center',
|
|
@@ -2180,7 +2189,7 @@ function ThroughlineFlow({
|
|
|
2180
2189
|
color,
|
|
2181
2190
|
}}
|
|
2182
2191
|
>
|
|
2183
|
-
{mech ?? step.
|
|
2192
|
+
{mech ?? step.mechanism}
|
|
2184
2193
|
</span>
|
|
2185
2194
|
<span
|
|
2186
2195
|
style={{
|