@principal-ai/subsystems-react 0.55.3 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreFileView.js +3 -3
- package/dist/pierre/PierreFileView.js.map +1 -1
- package/dist/pierre/PierreTrailCodeView.d.ts +1 -6
- package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreTrailCodeView.js +21 -18
- package/dist/pierre/PierreTrailCodeView.js.map +1 -1
- package/dist/pierre/fileAvailability.d.ts +4 -7
- package/dist/pierre/fileAvailability.d.ts.map +1 -1
- package/dist/pierre/fileAvailability.js +5 -10
- package/dist/pierre/fileAvailability.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +1 -1
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/DecorationPanel.d.ts +37 -0
- package/dist/subsystem/DecorationPanel.d.ts.map +1 -0
- package/dist/subsystem/DecorationPanel.js +147 -0
- package/dist/subsystem/DecorationPanel.js.map +1 -0
- package/dist/subsystem/FileDrawer.d.ts +3 -1
- package/dist/subsystem/FileDrawer.d.ts.map +1 -1
- package/dist/subsystem/FileDrawer.js +2 -2
- package/dist/subsystem/FileDrawer.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +26 -0
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +9 -5
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +37 -47
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/TrailStackBreadcrumb.d.ts +21 -0
- package/dist/subsystem/TrailStackBreadcrumb.d.ts.map +1 -0
- package/dist/subsystem/TrailStackBreadcrumb.js +77 -0
- package/dist/subsystem/TrailStackBreadcrumb.js.map +1 -0
- package/dist/subsystem/TrailStepsNav.d.ts +4 -3
- package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
- package/dist/subsystem/TrailStepsNav.js +13 -16
- package/dist/subsystem/TrailStepsNav.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts +9 -7
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +126 -76
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/decoration.d.ts +112 -0
- package/dist/subsystem/decoration.d.ts.map +1 -0
- package/dist/subsystem/decoration.js +54 -0
- package/dist/subsystem/decoration.js.map +1 -0
- package/dist/subsystem/model.d.ts +5 -18
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +3 -11
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +17 -24
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/stepEmphasis.d.ts +78 -0
- package/dist/subsystem/stepEmphasis.d.ts.map +1 -0
- package/dist/subsystem/stepEmphasis.js +89 -0
- package/dist/subsystem/stepEmphasis.js.map +1 -0
- package/dist/subsystem/trailBrief.d.ts.map +1 -1
- package/dist/subsystem/trailBrief.js +20 -0
- package/dist/subsystem/trailBrief.js.map +1 -1
- package/dist/subsystem/trailStack.d.ts +53 -0
- package/dist/subsystem/trailStack.d.ts.map +1 -0
- package/dist/subsystem/trailStack.js +154 -0
- package/dist/subsystem/trailStack.js.map +1 -0
- package/dist/subsystem/trailStepDisplay.d.ts +38 -0
- package/dist/subsystem/trailStepDisplay.d.ts.map +1 -0
- package/dist/subsystem/trailStepDisplay.js +41 -0
- package/dist/subsystem/trailStepDisplay.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +47 -1
- package/src/pierre/PierreFileView.tsx +3 -3
- package/src/pierre/PierreTrailCodeView.tsx +25 -28
- package/src/pierre/fileAvailability.test.ts +0 -6
- package/src/pierre/fileAvailability.ts +5 -13
- package/src/stories/Canvases/Subsystem/Deprecated.stories.tsx +3 -17
- package/src/stories/Canvases/Subsystem/NodeAnatomy.stories.tsx +3 -31
- package/src/stories/Canvases/Subsystem/StepEmphasis.stories.tsx +528 -0
- package/src/stories/Canvases/Subsystem/Trails.stories.tsx +0 -187
- package/src/stories/Components/DecorationPanel.stories.tsx +160 -0
- package/src/subsystem/C4Graph.tsx +0 -1
- package/src/subsystem/DecorationPanel.test.tsx +128 -0
- package/src/subsystem/DecorationPanel.tsx +334 -0
- package/src/subsystem/FileDrawer.tsx +6 -0
- package/src/subsystem/IssueList.tsx +26 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +68 -65
- package/src/subsystem/TrailStackBreadcrumb.test.tsx +109 -0
- package/src/subsystem/TrailStackBreadcrumb.tsx +172 -0
- package/src/subsystem/TrailStepsNav.test.tsx +35 -0
- package/src/subsystem/TrailStepsNav.tsx +18 -19
- package/src/subsystem/TrailsPanel.test.tsx +149 -0
- package/src/subsystem/TrailsPanel.tsx +165 -39
- package/src/subsystem/decoration.test.ts +72 -0
- package/src/subsystem/decoration.ts +151 -0
- package/src/subsystem/model.test.ts +7 -22
- package/src/subsystem/model.ts +6 -20
- package/src/subsystem/nodes.tsx +19 -28
- package/src/subsystem/stepEmphasis.test.ts +110 -0
- package/src/subsystem/stepEmphasis.ts +151 -0
- package/src/subsystem/trailBrief.test.ts +29 -0
- package/src/subsystem/trailBrief.ts +20 -0
- package/src/subsystem/trailStack.test.ts +133 -0
- package/src/subsystem/trailStack.ts +229 -0
- package/src/subsystem/trailStepDisplay.ts +68 -0
- package/src/stories/Canvases/Subsystem/Proposed.stories.tsx +0 -256
- package/src/stories/Canvases/Subsystem/ProposedTrails.stories.tsx +0 -184
|
@@ -3,6 +3,10 @@ import type { CSSProperties } from 'react';
|
|
|
3
3
|
import { ChevronRight, Route, X } from 'lucide-react';
|
|
4
4
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
5
5
|
import type { SubsystemTrail } from './model';
|
|
6
|
+
import {
|
|
7
|
+
trailStepDisplay,
|
|
8
|
+
type TrailComponentLabels,
|
|
9
|
+
} from './trailStepDisplay';
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* TrailStepsNav — the trails side panel as a two-step slide: a list of trails,
|
|
@@ -41,14 +45,12 @@ export interface TrailStepsNavProps {
|
|
|
41
45
|
onHoverStep: (trail: SubsystemTrail, stepIndex: number) => void;
|
|
42
46
|
onHoverFlow: (trail: SubsystemTrail) => void;
|
|
43
47
|
onLeaveStep: () => void;
|
|
44
|
-
/**
|
|
45
|
-
|
|
48
|
+
/** Alias → component, for turning hops into human display names. */
|
|
49
|
+
componentByAlias?: TrailComponentLabels;
|
|
46
50
|
/** Slide duration in ms. @default 340 */
|
|
47
51
|
transitionMs?: number;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
const PROPOSED_COLOR = 'darkgoldenrod';
|
|
51
|
-
|
|
52
54
|
const PANE: CSSProperties = {
|
|
53
55
|
position: 'absolute',
|
|
54
56
|
inset: 0,
|
|
@@ -58,11 +60,6 @@ const PANE: CSSProperties = {
|
|
|
58
60
|
willChange: 'transform',
|
|
59
61
|
};
|
|
60
62
|
|
|
61
|
-
function touchesProposed(trail: SubsystemTrail, proposed?: ReadonlySet<string>): boolean {
|
|
62
|
-
if (!proposed) return false;
|
|
63
|
-
return trail.steps.some((s) => proposed.has(s.from) || proposed.has(s.to));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
63
|
export function TrailStepsNav({
|
|
67
64
|
trails,
|
|
68
65
|
focusedTrailId,
|
|
@@ -74,7 +71,7 @@ export function TrailStepsNav({
|
|
|
74
71
|
onHoverStep,
|
|
75
72
|
onHoverFlow,
|
|
76
73
|
onLeaveStep,
|
|
77
|
-
|
|
74
|
+
componentByAlias,
|
|
78
75
|
transitionMs = 340,
|
|
79
76
|
}: TrailStepsNavProps) {
|
|
80
77
|
const { theme } = useTheme();
|
|
@@ -131,7 +128,6 @@ export function TrailStepsNav({
|
|
|
131
128
|
>
|
|
132
129
|
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '0 0 12px' }}>
|
|
133
130
|
{trails.map((trail) => {
|
|
134
|
-
const proposed = touchesProposed(trail, proposedAliases);
|
|
135
131
|
const focusedRow = trail.id === focusedTrailId;
|
|
136
132
|
const hovered = hoveredRow === trail.id;
|
|
137
133
|
return (
|
|
@@ -177,7 +173,7 @@ export function TrailStepsNav({
|
|
|
177
173
|
fontSize: theme.fontSizes[1],
|
|
178
174
|
fontFamily: theme.fonts.monospace,
|
|
179
175
|
fontWeight: 600,
|
|
180
|
-
color:
|
|
176
|
+
color: theme.colors.text,
|
|
181
177
|
}}
|
|
182
178
|
>
|
|
183
179
|
{trail.title}
|
|
@@ -251,7 +247,7 @@ export function TrailStepsNav({
|
|
|
251
247
|
fontSize: theme.fontSizes[1],
|
|
252
248
|
fontFamily: theme.fonts.monospace,
|
|
253
249
|
fontWeight: 600,
|
|
254
|
-
color:
|
|
250
|
+
color: theme.colors.text,
|
|
255
251
|
}}
|
|
256
252
|
>
|
|
257
253
|
{displayedTrail.title}
|
|
@@ -291,6 +287,7 @@ export function TrailStepsNav({
|
|
|
291
287
|
const hovered =
|
|
292
288
|
hoveredTrailStep?.trailId === displayedTrail.id &&
|
|
293
289
|
hoveredTrailStep.stepIndex === i;
|
|
290
|
+
const display = trailStepDisplay(step, componentByAlias);
|
|
294
291
|
return (
|
|
295
292
|
<button
|
|
296
293
|
key={`${step.from}->${step.to}-${i}`}
|
|
@@ -342,8 +339,9 @@ export function TrailStepsNav({
|
|
|
342
339
|
minWidth: 0,
|
|
343
340
|
overflowWrap: 'anywhere',
|
|
344
341
|
fontSize: theme.fontSizes[1],
|
|
345
|
-
fontFamily: theme.fonts.
|
|
342
|
+
fontFamily: theme.fonts.body,
|
|
346
343
|
color: theme.colors.text,
|
|
344
|
+
lineHeight: 1.35,
|
|
347
345
|
}}
|
|
348
346
|
>
|
|
349
347
|
{step.annotation}
|
|
@@ -351,13 +349,14 @@ export function TrailStepsNav({
|
|
|
351
349
|
)}
|
|
352
350
|
<span
|
|
353
351
|
style={{
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
352
|
+
minWidth: 0,
|
|
353
|
+
overflowWrap: 'anywhere',
|
|
354
|
+
fontSize: theme.fontSizes[1],
|
|
355
|
+
fontFamily: theme.fonts.monospace,
|
|
356
|
+
color: theme.colors.text,
|
|
358
357
|
}}
|
|
359
358
|
>
|
|
360
|
-
{
|
|
359
|
+
{display.primary}
|
|
361
360
|
</span>
|
|
362
361
|
</span>
|
|
363
362
|
</button>
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
} from './TrailsPanel';
|
|
10
10
|
import type { SubsystemTrail } from './model';
|
|
11
11
|
import { buildStepBrief } from './trailBrief';
|
|
12
|
+
import {
|
|
13
|
+
buildStepEmphasisMap,
|
|
14
|
+
stepClaimKey,
|
|
15
|
+
type StepEmphasisRecord,
|
|
16
|
+
} from './stepEmphasis';
|
|
12
17
|
|
|
13
18
|
// Bun's happy-dom test environment needs `@happy-dom/global-registrator`, which
|
|
14
19
|
// isn't installed here, so wire the DOM primitives React DOM + Testing Library
|
|
@@ -31,6 +36,12 @@ globals.cancelAnimationFrame = dom.cancelAnimationFrame.bind(dom);
|
|
|
31
36
|
globals.localStorage = dom.localStorage;
|
|
32
37
|
globals.sessionStorage = dom.sessionStorage;
|
|
33
38
|
globals.matchMedia = dom.matchMedia.bind(dom);
|
|
39
|
+
// themed-markdown measures its slide container; happy-dom has no ResizeObserver.
|
|
40
|
+
globals.ResizeObserver = class {
|
|
41
|
+
observe() {}
|
|
42
|
+
unobserve() {}
|
|
43
|
+
disconnect() {}
|
|
44
|
+
};
|
|
34
45
|
|
|
35
46
|
|
|
36
47
|
const trails: SubsystemTrail[] = [
|
|
@@ -51,6 +62,22 @@ const trails: SubsystemTrail[] = [
|
|
|
51
62
|
},
|
|
52
63
|
];
|
|
53
64
|
|
|
65
|
+
const usesTrail: SubsystemTrail = {
|
|
66
|
+
id: 'uses-flow',
|
|
67
|
+
title: 'Uses flow',
|
|
68
|
+
steps: [
|
|
69
|
+
{
|
|
70
|
+
from: 'panel',
|
|
71
|
+
to: 'merge',
|
|
72
|
+
mechanism: 'uses',
|
|
73
|
+
file: 'src/list.tsx',
|
|
74
|
+
span: { startLine: 10, endLine: 12, contentHash: 'h' },
|
|
75
|
+
purl: 'pkg:github/acme/app#src/list.tsx',
|
|
76
|
+
symbol: 'ModelList',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
54
81
|
const writeText = mock(() => Promise.resolve());
|
|
55
82
|
|
|
56
83
|
beforeEach(() => {
|
|
@@ -144,3 +171,125 @@ describe('TrailsPanel step copy', () => {
|
|
|
144
171
|
expect(copy.getAttribute('title')).toBe('Copy this step for an agent');
|
|
145
172
|
});
|
|
146
173
|
});
|
|
174
|
+
|
|
175
|
+
describe('TrailsPanel step semantics', () => {
|
|
176
|
+
test('calls leads with the caller frame', () => {
|
|
177
|
+
const { getByText, queryByText } = renderPanel();
|
|
178
|
+
expect(getByText('Login.submit')).toBeTruthy();
|
|
179
|
+
expect(queryByText(/calls →/)).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('uses leads with the logical hop and shows the via site', () => {
|
|
183
|
+
const { getByText } = renderPanel({
|
|
184
|
+
trails: [usesTrail],
|
|
185
|
+
expandedTrails: new Set(['uses-flow']),
|
|
186
|
+
componentByAlias: new Map([
|
|
187
|
+
['panel', { alias: 'panel', name: 'MaintenancePanel' }],
|
|
188
|
+
['merge', { alias: 'merge', name: 'mergeProvenance' }],
|
|
189
|
+
]),
|
|
190
|
+
});
|
|
191
|
+
expect(getByText('MaintenancePanel uses mergeProvenance')).toBeTruthy();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('falls back to aliases when no component map is supplied', () => {
|
|
195
|
+
const { getByText } = renderPanel({
|
|
196
|
+
trails: [usesTrail],
|
|
197
|
+
expandedTrails: new Set(['uses-flow']),
|
|
198
|
+
});
|
|
199
|
+
expect(getByText('panel uses merge')).toBeTruthy();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('uses the resolved target symbol as the callee', () => {
|
|
203
|
+
const withTarget: SubsystemTrail = {
|
|
204
|
+
...usesTrail,
|
|
205
|
+
steps: [
|
|
206
|
+
{
|
|
207
|
+
...usesTrail.steps[0]!,
|
|
208
|
+
target: {
|
|
209
|
+
purl: 'pkg:github/acme/app#src/store.ts',
|
|
210
|
+
file: 'src/store.ts',
|
|
211
|
+
symbol: 'Store.add',
|
|
212
|
+
span: { startLine: 3, endLine: 5, contentHash: 'h' },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
const { getByText } = renderPanel({
|
|
218
|
+
trails: [withTarget],
|
|
219
|
+
expandedTrails: new Set(['uses-flow']),
|
|
220
|
+
componentByAlias: new Map([
|
|
221
|
+
['panel', { alias: 'panel', name: 'MaintenancePanel' }],
|
|
222
|
+
['merge', { alias: 'merge', name: 'Store' }],
|
|
223
|
+
]),
|
|
224
|
+
});
|
|
225
|
+
expect(getByText('MaintenancePanel uses Store.add')).toBeTruthy();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
function emphasisRecord(
|
|
231
|
+
trailId: string,
|
|
232
|
+
stepIndex: number,
|
|
233
|
+
guidance?: string,
|
|
234
|
+
): StepEmphasisRecord {
|
|
235
|
+
return {
|
|
236
|
+
trailId,
|
|
237
|
+
stepKey: stepClaimKey(trails[0]!.steps[stepIndex]!),
|
|
238
|
+
guidance,
|
|
239
|
+
provenance: { source: 'author', at: '2026-10-19T00:00:00.000Z' },
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
describe('TrailsPanel step emphasis', () => {
|
|
244
|
+
test('renders a marked star and the trail emphasis count', () => {
|
|
245
|
+
const emphasis = buildStepEmphasisMap([
|
|
246
|
+
emphasisRecord('auth-flow', 0, 'Why this step matters.'),
|
|
247
|
+
]);
|
|
248
|
+
const { getByTitle } = renderPanel({ stepEmphasis: emphasis });
|
|
249
|
+
expect(getByTitle('1 emphasized step')).not.toBeNull();
|
|
250
|
+
expect(getByTitle('Key step')).not.toBeNull();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('shows markdown guidance when the marked step is focused', () => {
|
|
254
|
+
const emphasis = buildStepEmphasisMap([
|
|
255
|
+
emphasisRecord('auth-flow', 0, '**Start here** because this row matters.'),
|
|
256
|
+
]);
|
|
257
|
+
const { container } = renderPanel({
|
|
258
|
+
focusedTrailId: 'auth-flow',
|
|
259
|
+
focusedStepIndex: 0,
|
|
260
|
+
stepEmphasis: emphasis,
|
|
261
|
+
});
|
|
262
|
+
expect(container.textContent).toContain('Start here');
|
|
263
|
+
expect(container.textContent).toContain('because this row matters.');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('Remove guidance toggles the mark off', () => {
|
|
267
|
+
const onRemoveStepEmphasis = mock(() => {});
|
|
268
|
+
const emphasis = buildStepEmphasisMap([
|
|
269
|
+
emphasisRecord('auth-flow', 0, 'Why this step matters.'),
|
|
270
|
+
]);
|
|
271
|
+
const { getByText } = renderPanel({
|
|
272
|
+
focusedTrailId: 'auth-flow',
|
|
273
|
+
focusedStepIndex: 0,
|
|
274
|
+
stepEmphasis: emphasis,
|
|
275
|
+
onRemoveStepEmphasis,
|
|
276
|
+
});
|
|
277
|
+
fireEvent.click(getByText('Remove guidance'));
|
|
278
|
+
expect(onRemoveStepEmphasis).toHaveBeenCalledWith(trails[0], 0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('does not render guidance while the step is not focused', () => {
|
|
282
|
+
const emphasis = buildStepEmphasisMap([
|
|
283
|
+
emphasisRecord('auth-flow', 0, '**Start here** because this row matters.'),
|
|
284
|
+
]);
|
|
285
|
+
const { container } = renderPanel({ stepEmphasis: emphasis });
|
|
286
|
+
expect(container.textContent).not.toContain('because this row matters.');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('does not render a star for an unmarked step', () => {
|
|
290
|
+
const { queryByTitle } = renderPanel({
|
|
291
|
+
stepEmphasis: buildStepEmphasisMap([]),
|
|
292
|
+
});
|
|
293
|
+
expect(queryByTitle('Key step')).toBeNull();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -13,20 +13,28 @@
|
|
|
13
13
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
14
14
|
import type { DragEvent as ReactDragEvent } from 'react';
|
|
15
15
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
16
|
-
import { Check, ChevronDown, Copy, Focus, GripVertical, Minimize2, Pause, Play } from 'lucide-react';
|
|
16
|
+
import { Check, ChevronDown, Copy, Focus, GripVertical, Minimize2, Pause, Play, Star } from 'lucide-react';
|
|
17
|
+
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
17
18
|
import {
|
|
18
|
-
PROPOSED_COLOR,
|
|
19
19
|
reorderTargetIndex,
|
|
20
20
|
reorderTrails,
|
|
21
21
|
trailStepGraphEdgeId,
|
|
22
22
|
type SubsystemTrail,
|
|
23
|
-
type SubsystemTrailStep,
|
|
24
23
|
} from './model';
|
|
25
24
|
import { buildStepBrief } from './trailBrief';
|
|
25
|
+
import { emphasisKey, type StepEmphasisMap } from './stepEmphasis';
|
|
26
|
+
import {
|
|
27
|
+
trailStepDisplay,
|
|
28
|
+
type TrailComponentLabels,
|
|
29
|
+
} from './trailStepDisplay';
|
|
30
|
+
import { buildTrailStack } from './trailStack';
|
|
26
31
|
|
|
27
32
|
/** Pause (ms) between steps when a trail autoplays. */
|
|
28
33
|
export const TRAIL_PLAY_PAUSE_MS = 2500;
|
|
29
34
|
|
|
35
|
+
/** Single accent for a key step (no kind vocabulary — the mark is a star). */
|
|
36
|
+
const EMPHASIS_COLOR = '#f5a524';
|
|
37
|
+
|
|
30
38
|
/**
|
|
31
39
|
* Dwell time (ms) before a hover preview reaches the canvas.
|
|
32
40
|
*
|
|
@@ -59,6 +67,7 @@ function TrailFlow({
|
|
|
59
67
|
trail,
|
|
60
68
|
collapsed,
|
|
61
69
|
active,
|
|
70
|
+
focusedStepIndex,
|
|
62
71
|
onToggleCollapsed,
|
|
63
72
|
onFocusFlow,
|
|
64
73
|
onClearFocus,
|
|
@@ -69,14 +78,21 @@ function TrailFlow({
|
|
|
69
78
|
reorder,
|
|
70
79
|
dragActive,
|
|
71
80
|
isHoverSuppressed,
|
|
72
|
-
proposedAliases,
|
|
73
81
|
layoutFocused,
|
|
74
82
|
onToggleLayoutFocus,
|
|
83
|
+
stepEmphasis,
|
|
84
|
+
onRemoveStepEmphasis,
|
|
85
|
+
componentByAlias,
|
|
75
86
|
}: {
|
|
76
87
|
trail: SubsystemTrail;
|
|
77
88
|
collapsed: boolean;
|
|
78
89
|
/** `{ stepIndex: null }` = whole flow focused; `{ stepIndex }` = one step. */
|
|
79
90
|
active: { stepIndex: number | null } | null;
|
|
91
|
+
/**
|
|
92
|
+
* The step currently focused by the host (not merely hovered). The guidance
|
|
93
|
+
* body follows this, so sweeping the pointer does not expand every step.
|
|
94
|
+
*/
|
|
95
|
+
focusedStepIndex: number | null;
|
|
80
96
|
onToggleCollapsed: (tlId: string) => void;
|
|
81
97
|
onFocusFlow: (tl: SubsystemTrail) => void;
|
|
82
98
|
onClearFocus: () => void;
|
|
@@ -91,29 +107,33 @@ function TrailFlow({
|
|
|
91
107
|
dragActive?: boolean;
|
|
92
108
|
/** True just after a drop; ignores the browser's synthetic hover until the pointer moves. */
|
|
93
109
|
isHoverSuppressed?: () => boolean;
|
|
94
|
-
/**
|
|
95
|
-
* Aliases of components marked `proposed`. A row whose steps touch one gets
|
|
96
|
-
* its title tinted, and each such step (index and title) is tinted too
|
|
97
|
-
* (darkgoldenrod, same as the proposed node treatment).
|
|
98
|
-
*/
|
|
99
|
-
proposedAliases?: ReadonlySet<string>;
|
|
100
110
|
/** This flow's components are the only ones laid out right now. */
|
|
101
111
|
layoutFocused?: boolean;
|
|
102
112
|
/** Solo this flow's components in the layout, or restore the full model. */
|
|
103
113
|
onToggleLayoutFocus?: (tl: SubsystemTrail) => void;
|
|
114
|
+
/**
|
|
115
|
+
* Host-owned emphasis augmentations for this model. Absent = no marks. The
|
|
116
|
+
* map is keyed by `emphasisKey` (trail + claim fingerprint), so reordering
|
|
117
|
+
* steps keeps marks attached.
|
|
118
|
+
*/
|
|
119
|
+
stepEmphasis?: StepEmphasisMap;
|
|
120
|
+
/** Toggle a step's guidance off. Omit to hide the remove action. */
|
|
121
|
+
onRemoveStepEmphasis?: (tl: SubsystemTrail, stepIndex: number) => void;
|
|
122
|
+
/** Alias → component, for turning hops into human display names. */
|
|
123
|
+
componentByAlias?: TrailComponentLabels;
|
|
104
124
|
}) {
|
|
105
125
|
const { theme } = useTheme();
|
|
106
126
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
107
127
|
const hoverBg = theme.colors.background;
|
|
108
128
|
const wholeFlowActive = active !== null && active.stepIndex === null;
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
// Emphasis is keyed by trail + claim fingerprint, so the count follows the
|
|
130
|
+
// steps through a reorder.
|
|
131
|
+
const emphasisCount = stepEmphasis
|
|
132
|
+
? trail.steps.reduce(
|
|
133
|
+
(count, step) => count + (stepEmphasis.has(emphasisKey(trail.id, step)) ? 1 : 0),
|
|
134
|
+
0,
|
|
135
|
+
)
|
|
136
|
+
: 0;
|
|
117
137
|
const [headerHover, setHeaderHover] = useState(false);
|
|
118
138
|
const [collapseHover, setCollapseHover] = useState(false);
|
|
119
139
|
const [playHover, setPlayHover] = useState(false);
|
|
@@ -316,12 +336,30 @@ function TrailFlow({
|
|
|
316
336
|
fontSize: theme.fontSizes[1],
|
|
317
337
|
fontFamily: theme.fonts.monospace,
|
|
318
338
|
fontWeight: 600,
|
|
319
|
-
color:
|
|
339
|
+
color: theme.colors.text,
|
|
320
340
|
}}
|
|
321
341
|
>
|
|
322
342
|
{trail.title}
|
|
323
343
|
</span>
|
|
324
344
|
</button>
|
|
345
|
+
{emphasisCount > 0 && (
|
|
346
|
+
<span
|
|
347
|
+
title={`${emphasisCount} emphasized step${emphasisCount === 1 ? '' : 's'}`}
|
|
348
|
+
style={{
|
|
349
|
+
display: 'inline-flex',
|
|
350
|
+
alignItems: 'center',
|
|
351
|
+
gap: 2,
|
|
352
|
+
flexShrink: 0,
|
|
353
|
+
marginRight: 4,
|
|
354
|
+
fontSize: theme.fontSizes[0],
|
|
355
|
+
fontFamily: theme.fonts.monospace,
|
|
356
|
+
color: theme.colors.accent ?? theme.colors.primary ?? muted,
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
<Star size={11} strokeWidth={2} fill="currentColor" />
|
|
360
|
+
{emphasisCount}
|
|
361
|
+
</span>
|
|
362
|
+
)}
|
|
325
363
|
{!collapsed && (
|
|
326
364
|
<>
|
|
327
365
|
{onToggleLayoutFocus && trail.steps.length > 0 && (
|
|
@@ -466,11 +504,22 @@ function TrailFlow({
|
|
|
466
504
|
>
|
|
467
505
|
{trail.steps.map((step, i) => {
|
|
468
506
|
const stepActive = active !== null && active.stepIndex === i;
|
|
469
|
-
const proposed = stepProposed(step);
|
|
470
507
|
const revealed =
|
|
471
508
|
stepActive || hoveredStep === i || focusedStep === i || copiedStep === i;
|
|
472
509
|
const copied = copiedStep === i;
|
|
473
510
|
const copyHovered = copyHoverStep === i;
|
|
511
|
+
const record = stepEmphasis?.get(emphasisKey(trail.id, step));
|
|
512
|
+
const display = trailStepDisplay(step, componentByAlias);
|
|
513
|
+
// Calls nest; uses/data steps sit at their anchor frame's depth.
|
|
514
|
+
const stackDepth = buildTrailStack(
|
|
515
|
+
trail,
|
|
516
|
+
i,
|
|
517
|
+
componentByAlias,
|
|
518
|
+
).frames.length;
|
|
519
|
+
// The guidance body follows step focus: clicking a step opens it,
|
|
520
|
+
// clicking the focused step again returns to the flow. Hover is
|
|
521
|
+
// deliberately not enough — the list would flutter on sweep.
|
|
522
|
+
const guidanceVisible = focusedStepIndex === i && !!record?.guidance;
|
|
474
523
|
return (
|
|
475
524
|
<div
|
|
476
525
|
key={`${trailStepGraphEdgeId(step)}-${i}`}
|
|
@@ -482,12 +531,16 @@ function TrailFlow({
|
|
|
482
531
|
}
|
|
483
532
|
}}
|
|
484
533
|
>
|
|
534
|
+
{/* The row's absolute affordances (star, copy) are scoped to
|
|
535
|
+
this wrapper so the guidance body below cannot stretch
|
|
536
|
+
them — `top:0;bottom:0` on the outer row would grow their
|
|
537
|
+
hit area with the markdown block. */}
|
|
538
|
+
<div style={{ position: 'relative' }}>
|
|
485
539
|
<button
|
|
486
540
|
ref={(el) => {
|
|
487
541
|
stepButtonRefs.current[i] = el;
|
|
488
542
|
}}
|
|
489
543
|
type="button"
|
|
490
|
-
title={proposed ? 'Step touches a proposed component' : undefined}
|
|
491
544
|
onMouseEnter={() => {
|
|
492
545
|
setHoveredStep(i);
|
|
493
546
|
scheduleHoverPreview(() => onHoverStep(trail, i));
|
|
@@ -495,11 +548,13 @@ function TrailFlow({
|
|
|
495
548
|
onClick={() => onFocusStep(trail, i)}
|
|
496
549
|
style={{
|
|
497
550
|
display: 'flex',
|
|
498
|
-
alignItems: '
|
|
551
|
+
alignItems: 'flex-start',
|
|
499
552
|
gap: 8,
|
|
500
553
|
minWidth: 0,
|
|
501
554
|
width: '100%',
|
|
502
|
-
padding:
|
|
555
|
+
padding: `8px 70px 8px ${
|
|
556
|
+
17 + Math.max(0, stackDepth - 1) * 10
|
|
557
|
+
}px`,
|
|
503
558
|
textAlign: 'left',
|
|
504
559
|
borderRadius: 0,
|
|
505
560
|
border: 'none',
|
|
@@ -513,11 +568,12 @@ function TrailFlow({
|
|
|
513
568
|
style={{
|
|
514
569
|
flexShrink: 0,
|
|
515
570
|
width: 14,
|
|
571
|
+
paddingTop: 1,
|
|
516
572
|
fontSize: theme.fontSizes[0],
|
|
517
573
|
fontFamily: theme.fonts.monospace,
|
|
518
|
-
fontWeight:
|
|
519
|
-
color:
|
|
520
|
-
?
|
|
574
|
+
fontWeight: record ? 700 : undefined,
|
|
575
|
+
color: record
|
|
576
|
+
? EMPHASIS_COLOR
|
|
521
577
|
: stepActive
|
|
522
578
|
? theme.colors.text
|
|
523
579
|
: muted,
|
|
@@ -529,17 +585,39 @@ function TrailFlow({
|
|
|
529
585
|
style={{
|
|
530
586
|
flex: 1,
|
|
531
587
|
minWidth: 0,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
whiteSpace: 'nowrap',
|
|
588
|
+
overflowWrap: 'anywhere',
|
|
589
|
+
whiteSpace: 'normal',
|
|
535
590
|
fontSize: theme.fontSizes[1],
|
|
536
591
|
fontFamily: theme.fonts.monospace,
|
|
537
|
-
color:
|
|
592
|
+
color: record
|
|
593
|
+
? EMPHASIS_COLOR
|
|
594
|
+
: theme.colors.text,
|
|
538
595
|
}}
|
|
539
596
|
>
|
|
540
|
-
{
|
|
597
|
+
{display.primary}
|
|
541
598
|
</span>
|
|
542
599
|
</button>
|
|
600
|
+
{record && (
|
|
601
|
+
<span
|
|
602
|
+
aria-hidden
|
|
603
|
+
title="Key step"
|
|
604
|
+
style={{
|
|
605
|
+
position: 'absolute',
|
|
606
|
+
right: 34,
|
|
607
|
+
top: 0,
|
|
608
|
+
bottom: 0,
|
|
609
|
+
display: 'inline-flex',
|
|
610
|
+
alignItems: 'center',
|
|
611
|
+
justifyContent: 'center',
|
|
612
|
+
width: 'auto',
|
|
613
|
+
aspectRatio: '1 / 1',
|
|
614
|
+
pointerEvents: 'none',
|
|
615
|
+
color: EMPHASIS_COLOR,
|
|
616
|
+
}}
|
|
617
|
+
>
|
|
618
|
+
<Star size={12} strokeWidth={2} fill={EMPHASIS_COLOR} />
|
|
619
|
+
</span>
|
|
620
|
+
)}
|
|
543
621
|
{revealed && (
|
|
544
622
|
<button
|
|
545
623
|
type="button"
|
|
@@ -581,6 +659,49 @@ function TrailFlow({
|
|
|
581
659
|
)}
|
|
582
660
|
</button>
|
|
583
661
|
)}
|
|
662
|
+
</div>
|
|
663
|
+
{guidanceVisible && record?.guidance && (
|
|
664
|
+
<div
|
|
665
|
+
style={{
|
|
666
|
+
// Align with the step text: 17px row inset + 14px number
|
|
667
|
+
// column + 8px gap.
|
|
668
|
+
padding: '2px 12px 10px 39px',
|
|
669
|
+
fontSize: theme.fontSizes[0],
|
|
670
|
+
lineHeight: 1.5,
|
|
671
|
+
}}
|
|
672
|
+
>
|
|
673
|
+
<IndustryMarkdownSlide
|
|
674
|
+
content={record.guidance}
|
|
675
|
+
slideIdPrefix={`step-guidance-${trail.id}-${i}`}
|
|
676
|
+
slideIndex={0}
|
|
677
|
+
isVisible={true}
|
|
678
|
+
theme={theme}
|
|
679
|
+
disableScroll={true}
|
|
680
|
+
disableBasePadding
|
|
681
|
+
enableKeyboardScrolling={false}
|
|
682
|
+
autoFocusOnVisible={false}
|
|
683
|
+
/>
|
|
684
|
+
{onRemoveStepEmphasis && (
|
|
685
|
+
<button
|
|
686
|
+
type="button"
|
|
687
|
+
onClick={() => onRemoveStepEmphasis(trail, i)}
|
|
688
|
+
style={{
|
|
689
|
+
marginTop: 6,
|
|
690
|
+
padding: 0,
|
|
691
|
+
border: 'none',
|
|
692
|
+
background: 'transparent',
|
|
693
|
+
color: muted,
|
|
694
|
+
fontFamily: theme.fonts.monospace,
|
|
695
|
+
fontSize: theme.fontSizes[0],
|
|
696
|
+
textDecoration: 'underline',
|
|
697
|
+
cursor: 'pointer',
|
|
698
|
+
}}
|
|
699
|
+
>
|
|
700
|
+
Remove guidance
|
|
701
|
+
</button>
|
|
702
|
+
)}
|
|
703
|
+
</div>
|
|
704
|
+
)}
|
|
584
705
|
</div>
|
|
585
706
|
);
|
|
586
707
|
})}
|
|
@@ -614,12 +735,6 @@ export interface TrailsPanelProps {
|
|
|
614
735
|
* for a read-only panel.
|
|
615
736
|
*/
|
|
616
737
|
onReorder?: (next: SubsystemTrail[]) => void;
|
|
617
|
-
/**
|
|
618
|
-
* Aliases of components marked `proposed`. A row whose steps touch one gets
|
|
619
|
-
* its title tinted, and each such step (index and title) is tinted too
|
|
620
|
-
* (darkgoldenrod, same as the proposed node treatment).
|
|
621
|
-
*/
|
|
622
|
-
proposedAliases?: ReadonlySet<string>;
|
|
623
738
|
/** Trail whose components are the only ones laid out on the canvas, if any. */
|
|
624
739
|
layoutFocusedTrailId?: string | null;
|
|
625
740
|
/**
|
|
@@ -627,6 +742,12 @@ export interface TrailsPanelProps {
|
|
|
627
742
|
* flow's components and edges. Omit to hide the per-row focus control.
|
|
628
743
|
*/
|
|
629
744
|
onFocusLayout?: (trailId: string) => void;
|
|
745
|
+
/** Host-owned step emphasis augmentations, keyed by trail + claim fingerprint. */
|
|
746
|
+
stepEmphasis?: StepEmphasisMap;
|
|
747
|
+
/** Toggle a step's guidance off. Omit to hide the remove action. */
|
|
748
|
+
onRemoveStepEmphasis?: (tl: SubsystemTrail, stepIndex: number) => void;
|
|
749
|
+
/** Alias → component, for turning hops into human display names. */
|
|
750
|
+
componentByAlias?: TrailComponentLabels;
|
|
630
751
|
}
|
|
631
752
|
|
|
632
753
|
/**
|
|
@@ -648,9 +769,11 @@ export function TrailsPanel({
|
|
|
648
769
|
onHoverFlow,
|
|
649
770
|
onLeaveStep,
|
|
650
771
|
onReorder,
|
|
651
|
-
proposedAliases,
|
|
652
772
|
layoutFocusedTrailId,
|
|
653
773
|
onFocusLayout,
|
|
774
|
+
stepEmphasis,
|
|
775
|
+
onRemoveStepEmphasis,
|
|
776
|
+
componentByAlias,
|
|
654
777
|
}: TrailsPanelProps) {
|
|
655
778
|
const { theme } = useTheme();
|
|
656
779
|
// Reordering is offered only while every row is collapsed: the compact list
|
|
@@ -811,6 +934,7 @@ export function TrailsPanel({
|
|
|
811
934
|
? { stepIndex: hoveredTrailStep.stepIndex }
|
|
812
935
|
: null
|
|
813
936
|
}
|
|
937
|
+
focusedStepIndex={focusedTrailId === tl.id ? focusedStepIndex : null}
|
|
814
938
|
onToggleCollapsed={onToggleCollapsed}
|
|
815
939
|
onFocusFlow={onFocusFlow}
|
|
816
940
|
onClearFocus={onClearFocus}
|
|
@@ -830,11 +954,13 @@ export function TrailsPanel({
|
|
|
830
954
|
}
|
|
831
955
|
dragActive={dragIndex != null}
|
|
832
956
|
isHoverSuppressed={isHoverSuppressed}
|
|
833
|
-
|
|
957
|
+
componentByAlias={componentByAlias}
|
|
834
958
|
layoutFocused={layoutFocusedTrailId === tl.id}
|
|
835
959
|
onToggleLayoutFocus={
|
|
836
960
|
onFocusLayout ? (t) => onFocusLayout(t.id) : undefined
|
|
837
961
|
}
|
|
962
|
+
stepEmphasis={stepEmphasis}
|
|
963
|
+
onRemoveStepEmphasis={onRemoveStepEmphasis}
|
|
838
964
|
/>
|
|
839
965
|
</div>
|
|
840
966
|
);
|