@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.
Files changed (107) hide show
  1. package/dist/index.d.ts +12 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreFileView.js +3 -3
  6. package/dist/pierre/PierreFileView.js.map +1 -1
  7. package/dist/pierre/PierreTrailCodeView.d.ts +1 -6
  8. package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
  9. package/dist/pierre/PierreTrailCodeView.js +21 -18
  10. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  11. package/dist/pierre/fileAvailability.d.ts +4 -7
  12. package/dist/pierre/fileAvailability.d.ts.map +1 -1
  13. package/dist/pierre/fileAvailability.js +5 -10
  14. package/dist/pierre/fileAvailability.js.map +1 -1
  15. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  16. package/dist/subsystem/C4Graph.js +1 -1
  17. package/dist/subsystem/C4Graph.js.map +1 -1
  18. package/dist/subsystem/DecorationPanel.d.ts +37 -0
  19. package/dist/subsystem/DecorationPanel.d.ts.map +1 -0
  20. package/dist/subsystem/DecorationPanel.js +147 -0
  21. package/dist/subsystem/DecorationPanel.js.map +1 -0
  22. package/dist/subsystem/FileDrawer.d.ts +3 -1
  23. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  24. package/dist/subsystem/FileDrawer.js +2 -2
  25. package/dist/subsystem/FileDrawer.js.map +1 -1
  26. package/dist/subsystem/IssueList.d.ts.map +1 -1
  27. package/dist/subsystem/IssueList.js +26 -0
  28. package/dist/subsystem/IssueList.js.map +1 -1
  29. package/dist/subsystem/SubsystemComponentGraph.d.ts +9 -5
  30. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  31. package/dist/subsystem/SubsystemComponentGraph.js +37 -47
  32. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  33. package/dist/subsystem/TrailStackBreadcrumb.d.ts +21 -0
  34. package/dist/subsystem/TrailStackBreadcrumb.d.ts.map +1 -0
  35. package/dist/subsystem/TrailStackBreadcrumb.js +77 -0
  36. package/dist/subsystem/TrailStackBreadcrumb.js.map +1 -0
  37. package/dist/subsystem/TrailStepsNav.d.ts +4 -3
  38. package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
  39. package/dist/subsystem/TrailStepsNav.js +13 -16
  40. package/dist/subsystem/TrailStepsNav.js.map +1 -1
  41. package/dist/subsystem/TrailsPanel.d.ts +9 -7
  42. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  43. package/dist/subsystem/TrailsPanel.js +126 -76
  44. package/dist/subsystem/TrailsPanel.js.map +1 -1
  45. package/dist/subsystem/decoration.d.ts +112 -0
  46. package/dist/subsystem/decoration.d.ts.map +1 -0
  47. package/dist/subsystem/decoration.js +54 -0
  48. package/dist/subsystem/decoration.js.map +1 -0
  49. package/dist/subsystem/model.d.ts +5 -18
  50. package/dist/subsystem/model.d.ts.map +1 -1
  51. package/dist/subsystem/model.js +3 -11
  52. package/dist/subsystem/model.js.map +1 -1
  53. package/dist/subsystem/nodes.d.ts.map +1 -1
  54. package/dist/subsystem/nodes.js +17 -24
  55. package/dist/subsystem/nodes.js.map +1 -1
  56. package/dist/subsystem/stepEmphasis.d.ts +78 -0
  57. package/dist/subsystem/stepEmphasis.d.ts.map +1 -0
  58. package/dist/subsystem/stepEmphasis.js +89 -0
  59. package/dist/subsystem/stepEmphasis.js.map +1 -0
  60. package/dist/subsystem/trailBrief.d.ts.map +1 -1
  61. package/dist/subsystem/trailBrief.js +20 -0
  62. package/dist/subsystem/trailBrief.js.map +1 -1
  63. package/dist/subsystem/trailStack.d.ts +53 -0
  64. package/dist/subsystem/trailStack.d.ts.map +1 -0
  65. package/dist/subsystem/trailStack.js +154 -0
  66. package/dist/subsystem/trailStack.js.map +1 -0
  67. package/dist/subsystem/trailStepDisplay.d.ts +38 -0
  68. package/dist/subsystem/trailStepDisplay.d.ts.map +1 -0
  69. package/dist/subsystem/trailStepDisplay.js +41 -0
  70. package/dist/subsystem/trailStepDisplay.js.map +1 -0
  71. package/package.json +3 -3
  72. package/src/index.ts +47 -1
  73. package/src/pierre/PierreFileView.tsx +3 -3
  74. package/src/pierre/PierreTrailCodeView.tsx +25 -28
  75. package/src/pierre/fileAvailability.test.ts +0 -6
  76. package/src/pierre/fileAvailability.ts +5 -13
  77. package/src/stories/Canvases/Subsystem/Deprecated.stories.tsx +3 -17
  78. package/src/stories/Canvases/Subsystem/NodeAnatomy.stories.tsx +3 -31
  79. package/src/stories/Canvases/Subsystem/StepEmphasis.stories.tsx +528 -0
  80. package/src/stories/Canvases/Subsystem/Trails.stories.tsx +0 -187
  81. package/src/stories/Components/DecorationPanel.stories.tsx +160 -0
  82. package/src/subsystem/C4Graph.tsx +0 -1
  83. package/src/subsystem/DecorationPanel.test.tsx +128 -0
  84. package/src/subsystem/DecorationPanel.tsx +334 -0
  85. package/src/subsystem/FileDrawer.tsx +6 -0
  86. package/src/subsystem/IssueList.tsx +26 -0
  87. package/src/subsystem/SubsystemComponentGraph.tsx +68 -65
  88. package/src/subsystem/TrailStackBreadcrumb.test.tsx +109 -0
  89. package/src/subsystem/TrailStackBreadcrumb.tsx +172 -0
  90. package/src/subsystem/TrailStepsNav.test.tsx +35 -0
  91. package/src/subsystem/TrailStepsNav.tsx +18 -19
  92. package/src/subsystem/TrailsPanel.test.tsx +149 -0
  93. package/src/subsystem/TrailsPanel.tsx +165 -39
  94. package/src/subsystem/decoration.test.ts +72 -0
  95. package/src/subsystem/decoration.ts +151 -0
  96. package/src/subsystem/model.test.ts +7 -22
  97. package/src/subsystem/model.ts +6 -20
  98. package/src/subsystem/nodes.tsx +19 -28
  99. package/src/subsystem/stepEmphasis.test.ts +110 -0
  100. package/src/subsystem/stepEmphasis.ts +151 -0
  101. package/src/subsystem/trailBrief.test.ts +29 -0
  102. package/src/subsystem/trailBrief.ts +20 -0
  103. package/src/subsystem/trailStack.test.ts +133 -0
  104. package/src/subsystem/trailStack.ts +229 -0
  105. package/src/subsystem/trailStepDisplay.ts +68 -0
  106. package/src/stories/Canvases/Subsystem/Proposed.stories.tsx +0 -256
  107. 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
- /** Aliases of components marked `proposed` — a row whose steps touch one tints. */
45
- proposedAliases?: ReadonlySet<string>;
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
- proposedAliases,
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: proposed ? PROPOSED_COLOR : theme.colors.text,
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: touchesProposed(displayedTrail, proposedAliases) ? PROPOSED_COLOR : theme.colors.text,
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.monospace,
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
- fontSize: theme.fontSizes[0],
355
- fontFamily: theme.fonts.body,
356
- color: muted,
357
- lineHeight: 1.35,
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
- {step.symbol ?? step.from}
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
- // A step is "proposed" when it says so itself OR either endpoint component
110
- // is proposed — the step flag covers planned seams whose endpoints already
111
- // exist as real components.
112
- const stepProposed = (step: SubsystemTrailStep) =>
113
- step.proposed === true ||
114
- (proposedAliases != null &&
115
- (proposedAliases.has(step.from) || proposedAliases.has(step.to)));
116
- const touchesProposed = trail.steps.some((s) => stepProposed(s));
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: touchesProposed ? PROPOSED_COLOR : theme.colors.text,
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: 'center',
551
+ alignItems: 'flex-start',
499
552
  gap: 8,
500
553
  minWidth: 0,
501
554
  width: '100%',
502
- padding: '8px 36px 8px 17px',
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: proposed ? 700 : undefined,
519
- color: proposed
520
- ? PROPOSED_COLOR
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
- overflow: 'hidden',
533
- textOverflow: 'ellipsis',
534
- whiteSpace: 'nowrap',
588
+ overflowWrap: 'anywhere',
589
+ whiteSpace: 'normal',
535
590
  fontSize: theme.fontSizes[1],
536
591
  fontFamily: theme.fonts.monospace,
537
- color: proposed ? PROPOSED_COLOR : theme.colors.text,
592
+ color: record
593
+ ? EMPHASIS_COLOR
594
+ : theme.colors.text,
538
595
  }}
539
596
  >
540
- {step.symbol}
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
- proposedAliases={proposedAliases}
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
  );