@principal-ai/subsystems-react 0.53.6 → 0.54.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 +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreTrailCodeView.js +7 -5
- package/dist/pierre/PierreTrailCodeView.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +1 -2
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +144 -9
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +924 -148
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +22 -1
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
- package/dist/subsystem/C4NodeCard.js +10 -2
- package/dist/subsystem/C4NodeCard.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +5 -4
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +12 -4
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +10 -7
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/GraphSidebar.d.ts +71 -0
- package/dist/subsystem/GraphSidebar.d.ts.map +1 -0
- package/dist/subsystem/GraphSidebar.js +145 -0
- package/dist/subsystem/GraphSidebar.js.map +1 -0
- package/dist/subsystem/GraphWithSidebar.d.ts +24 -0
- package/dist/subsystem/GraphWithSidebar.d.ts.map +1 -0
- package/dist/subsystem/GraphWithSidebar.js +25 -0
- package/dist/subsystem/GraphWithSidebar.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +33 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +364 -411
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/TrailStepsNav.d.ts +48 -0
- package/dist/subsystem/TrailStepsNav.d.ts.map +1 -0
- package/dist/subsystem/TrailStepsNav.js +189 -0
- package/dist/subsystem/TrailStepsNav.js.map +1 -0
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +3 -4
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/__fixtures__/c4Drilldown.d.ts +151 -0
- package/dist/subsystem/__fixtures__/c4Drilldown.d.ts.map +1 -0
- package/dist/subsystem/__fixtures__/c4Drilldown.js +783 -0
- package/dist/subsystem/__fixtures__/c4Drilldown.js.map +1 -0
- package/dist/subsystem/c4.d.ts +39 -8
- package/dist/subsystem/c4.d.ts.map +1 -1
- package/dist/subsystem/c4.js.map +1 -1
- package/dist/subsystem/declarationRef.d.ts +26 -18
- package/dist/subsystem/declarationRef.d.ts.map +1 -1
- package/dist/subsystem/declarationRef.js +9 -4
- package/dist/subsystem/declarationRef.js.map +1 -1
- package/dist/subsystem/model.d.ts +19 -1
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +12 -2
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +24 -4
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/trailBrief.d.ts.map +1 -1
- package/dist/subsystem/trailBrief.js +6 -1
- package/dist/subsystem/trailBrief.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +9 -2
- package/src/pierre/PierreTrailCodeView.tsx +9 -13
- package/src/stories/Subsystem/C4Graph/C4ToComponentDrill.stories.tsx +496 -0
- package/src/stories/Subsystem/ComponentGraph/AnimatedProgression.stories.tsx +7 -7
- package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +19 -25
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +8 -10
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +19 -19
- package/src/stories/Subsystem/ComponentGraph/ProposedTrails.stories.tsx +6 -6
- package/src/stories/Subsystem/ComponentGraph/TrailAutoplay.stories.tsx +7 -7
- package/src/stories/Subsystem/ComponentGraph/Trails.stories.tsx +27 -30
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +1 -2
- package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +10 -13
- package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +3 -4
- package/src/stories/Subsystem/Sidebar/GraphSidebar.stories.tsx +206 -0
- package/src/subsystem/C4Graph.groups.test.ts +96 -2
- package/src/subsystem/C4Graph.tsx +1377 -165
- package/src/subsystem/C4NodeCard.test.ts +1 -2
- package/src/subsystem/C4NodeCard.tsx +24 -2
- package/src/subsystem/ComponentDeclaration.tsx +18 -8
- package/src/subsystem/ConstructsCatalog.tsx +23 -8
- package/src/subsystem/GraphSidebar.test.tsx +152 -0
- package/src/subsystem/GraphSidebar.tsx +300 -0
- package/src/subsystem/GraphWithSidebar.test.tsx +78 -0
- package/src/subsystem/GraphWithSidebar.tsx +60 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +284 -370
- package/src/subsystem/TrailStepsNav.test.tsx +179 -0
- package/src/subsystem/TrailStepsNav.tsx +372 -0
- package/src/subsystem/TrailsPanel.tsx +3 -4
- package/src/subsystem/__fixtures__/c4Drilldown.test.ts +338 -0
- package/src/subsystem/__fixtures__/c4Drilldown.ts +847 -0
- package/src/subsystem/c4.ts +41 -8
- package/src/subsystem/declarationRef.ts +28 -19
- package/src/subsystem/model.test.ts +32 -12
- package/src/subsystem/model.ts +30 -1
- package/src/subsystem/nodes.group.test.tsx +82 -0
- package/src/subsystem/nodes.tsx +30 -2
- package/src/subsystem/trailBrief.test.ts +2 -2
- package/src/subsystem/trailBrief.ts +6 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { Window } from 'happy-dom';
|
|
4
|
+
import { cleanup, fireEvent, render } from '@testing-library/react/pure';
|
|
5
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
6
|
+
import { TrailStepsNav } from './TrailStepsNav';
|
|
7
|
+
import type { SubsystemTrail } from './model';
|
|
8
|
+
|
|
9
|
+
// Bun's happy-dom test environment isn't registered (no @happy-dom/global-
|
|
10
|
+
// registrator), so wire the DOM primitives React DOM needs before rendering.
|
|
11
|
+
const dom = new Window();
|
|
12
|
+
const globals = globalThis as unknown as Record<string, unknown>;
|
|
13
|
+
globals.window = dom;
|
|
14
|
+
globals.document = dom.document;
|
|
15
|
+
globals.navigator = dom.navigator;
|
|
16
|
+
globals.HTMLElement = dom.HTMLElement;
|
|
17
|
+
globals.Node = dom.Node;
|
|
18
|
+
globals.Element = dom.Element;
|
|
19
|
+
globals.Event = dom.Event;
|
|
20
|
+
globals.getComputedStyle = dom.getComputedStyle.bind(dom);
|
|
21
|
+
globals.requestAnimationFrame = dom.requestAnimationFrame.bind(dom);
|
|
22
|
+
globals.cancelAnimationFrame = dom.cancelAnimationFrame.bind(dom);
|
|
23
|
+
globals.localStorage = dom.localStorage;
|
|
24
|
+
globals.matchMedia = dom.matchMedia.bind(dom);
|
|
25
|
+
|
|
26
|
+
afterEach(cleanup);
|
|
27
|
+
|
|
28
|
+
const step = (from: string, to: string, symbol: string, annotation?: string) => ({
|
|
29
|
+
from,
|
|
30
|
+
to,
|
|
31
|
+
mechanism: 'calls' as const,
|
|
32
|
+
file: `${from}.ts`,
|
|
33
|
+
line: 1,
|
|
34
|
+
purl: `pkg:github/x#${from}.ts`,
|
|
35
|
+
symbol,
|
|
36
|
+
...(annotation ? { annotation } : {}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const TRAILS: SubsystemTrail[] = [
|
|
40
|
+
{
|
|
41
|
+
id: 't1',
|
|
42
|
+
title: 'First flow',
|
|
43
|
+
steps: [step('a', 'b', 'A.go', 'Read the source seam.'), step('b', 'c', 'B.go')],
|
|
44
|
+
},
|
|
45
|
+
{ id: 't2', title: 'Second flow', steps: [step('c', 'd', 'C.go')] },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function renderNav(props: Record<string, unknown>) {
|
|
49
|
+
return render(
|
|
50
|
+
createElement(
|
|
51
|
+
ThemeProvider,
|
|
52
|
+
{ theme: defaultEditorTheme },
|
|
53
|
+
createElement(TrailStepsNav as never, props as never),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('TrailStepsNav', () => {
|
|
59
|
+
test('lists trails with step counts and focuses on click', () => {
|
|
60
|
+
let focused: string | null = null;
|
|
61
|
+
const { getByText, getByRole } = renderNav({
|
|
62
|
+
trails: TRAILS,
|
|
63
|
+
focusedTrailId: null,
|
|
64
|
+
focusedStepIndex: null,
|
|
65
|
+
hoveredTrailStep: null,
|
|
66
|
+
onFocusFlow: (t: SubsystemTrail) => {
|
|
67
|
+
focused = t.id;
|
|
68
|
+
},
|
|
69
|
+
onClearFocus: () => {},
|
|
70
|
+
onFocusStep: () => {},
|
|
71
|
+
onHoverStep: () => {},
|
|
72
|
+
onHoverFlow: () => {},
|
|
73
|
+
onLeaveStep: () => {},
|
|
74
|
+
});
|
|
75
|
+
expect(getByText('First flow')).toBeTruthy();
|
|
76
|
+
expect(getByText('Second flow')).toBeTruthy();
|
|
77
|
+
fireEvent.click(getByRole('button', { name: /First flow/ }));
|
|
78
|
+
expect(focused).toBe('t1');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('a hovered trail row previews the flow', () => {
|
|
82
|
+
const hovered: string[] = [];
|
|
83
|
+
const { getByRole } = renderNav({
|
|
84
|
+
trails: TRAILS,
|
|
85
|
+
focusedTrailId: null,
|
|
86
|
+
focusedStepIndex: null,
|
|
87
|
+
hoveredTrailStep: null,
|
|
88
|
+
onFocusFlow: () => {},
|
|
89
|
+
onClearFocus: () => {},
|
|
90
|
+
onFocusStep: () => {},
|
|
91
|
+
onHoverStep: () => {},
|
|
92
|
+
onHoverFlow: (t: SubsystemTrail) => hovered.push(t.id),
|
|
93
|
+
onLeaveStep: () => {},
|
|
94
|
+
});
|
|
95
|
+
fireEvent.mouseEnter(getByRole('button', { name: /Second flow/ }));
|
|
96
|
+
expect(hovered).toContain('t2');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('back slides the step pane out before its steps unmount', async () => {
|
|
100
|
+
const base = {
|
|
101
|
+
trails: TRAILS,
|
|
102
|
+
focusedStepIndex: null,
|
|
103
|
+
hoveredTrailStep: null,
|
|
104
|
+
onFocusFlow: () => {},
|
|
105
|
+
onClearFocus: () => {},
|
|
106
|
+
onFocusStep: () => {},
|
|
107
|
+
onHoverStep: () => {},
|
|
108
|
+
onHoverFlow: () => {},
|
|
109
|
+
onLeaveStep: () => {},
|
|
110
|
+
transitionMs: 30,
|
|
111
|
+
};
|
|
112
|
+
const { getByText, queryByText, rerender } = renderNav({ ...base, focusedTrailId: 't1' });
|
|
113
|
+
expect(getByText('A.go')).toBeTruthy();
|
|
114
|
+
// Back clears the focus, but the pane keeps its content while it slides out.
|
|
115
|
+
rerender(
|
|
116
|
+
createElement(
|
|
117
|
+
ThemeProvider,
|
|
118
|
+
{ theme: defaultEditorTheme },
|
|
119
|
+
createElement(TrailStepsNav as never, { ...base, focusedTrailId: null } as never),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
expect(queryByText('A.go')).toBeTruthy();
|
|
123
|
+
// Once the transition has run, the step content is dropped.
|
|
124
|
+
await new Promise((r) => setTimeout(r, 30 + 40));
|
|
125
|
+
expect(queryByText('A.go')).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('a focused trail shows its steps; back clears the focus', () => {
|
|
129
|
+
let cleared = false;
|
|
130
|
+
let focusedStep: number | null = null;
|
|
131
|
+
const { getByText, getByLabelText, getByRole } = renderNav({
|
|
132
|
+
trails: TRAILS,
|
|
133
|
+
focusedTrailId: 't1',
|
|
134
|
+
focusedStepIndex: null,
|
|
135
|
+
hoveredTrailStep: null,
|
|
136
|
+
onFocusFlow: () => {},
|
|
137
|
+
onClearFocus: () => {
|
|
138
|
+
cleared = true;
|
|
139
|
+
},
|
|
140
|
+
onFocusStep: (_t: SubsystemTrail, i: number) => {
|
|
141
|
+
focusedStep = i;
|
|
142
|
+
},
|
|
143
|
+
onHoverStep: () => {},
|
|
144
|
+
onHoverFlow: () => {},
|
|
145
|
+
onLeaveStep: () => {},
|
|
146
|
+
});
|
|
147
|
+
// The step pane is translated in; both steps render by symbol, and an
|
|
148
|
+
// annotated step shows its note as the row's description.
|
|
149
|
+
expect(getByText('A.go')).toBeTruthy();
|
|
150
|
+
expect(getByText('B.go')).toBeTruthy();
|
|
151
|
+
expect(getByText('Read the source seam.')).toBeTruthy();
|
|
152
|
+
fireEvent.click(getByRole('button', { name: /B\.go/ }));
|
|
153
|
+
expect(focusedStep).toBe(1);
|
|
154
|
+
fireEvent.click(getByLabelText('Back to trails'));
|
|
155
|
+
expect(cleared).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('clicking the step-pane header title also goes back', () => {
|
|
159
|
+
let cleared = false;
|
|
160
|
+
const { getByRole } = renderNav({
|
|
161
|
+
trails: TRAILS,
|
|
162
|
+
focusedTrailId: 't1',
|
|
163
|
+
focusedStepIndex: null,
|
|
164
|
+
hoveredTrailStep: null,
|
|
165
|
+
onFocusFlow: () => {},
|
|
166
|
+
onClearFocus: () => {
|
|
167
|
+
cleared = true;
|
|
168
|
+
},
|
|
169
|
+
onFocusStep: () => {},
|
|
170
|
+
onHoverStep: () => {},
|
|
171
|
+
onHoverFlow: () => {},
|
|
172
|
+
onLeaveStep: () => {},
|
|
173
|
+
});
|
|
174
|
+
// The header title is its own button (the list row's accessible name
|
|
175
|
+
// includes the step count, so the exact title matches the header).
|
|
176
|
+
fireEvent.click(getByRole('button', { name: 'First flow' }));
|
|
177
|
+
expect(cleared).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
import { ChevronRight, Route, X } from 'lucide-react';
|
|
4
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
5
|
+
import type { SubsystemTrail } from './model';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TrailStepsNav — the trails side panel as a two-step slide: a list of trails,
|
|
9
|
+
* and a step pane that slides in from the right when a trail is picked (and
|
|
10
|
+
* back out when focus clears). The counterpart to `TrailsPanel`'s inline
|
|
11
|
+
* expand — same data and callbacks, a push instead of a dropdown.
|
|
12
|
+
*
|
|
13
|
+
* Pure presentation: the host owns the focus state and the canvas response.
|
|
14
|
+
* `onFocusFlow` opens the step pane (and, in the C4 drill, hides the elements
|
|
15
|
+
* the trail does not touch); `onClearFocus` closes it. Hovering a row previews
|
|
16
|
+
* that trail (`onHoverFlow`); hovering a step previews the hop
|
|
17
|
+
* (`onHoverStep`), which the host dims to.
|
|
18
|
+
*
|
|
19
|
+
* Back is a slide, not a swap: clearing the focus leaves the last trail's steps
|
|
20
|
+
* rendered until the pane has finished sliding out, so the detail view is what
|
|
21
|
+
* moves, not a blank pane.
|
|
22
|
+
*
|
|
23
|
+
* Each step row leads with its `annotation` (the free-text note anchored to its
|
|
24
|
+
* site line) as the row's description, with the `symbol` beneath it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface TrailStepsNavProps {
|
|
28
|
+
/** Ordered trails — the list pane. */
|
|
29
|
+
trails: SubsystemTrail[];
|
|
30
|
+
/** Focused trail id, or null for the list pane. */
|
|
31
|
+
focusedTrailId: string | null;
|
|
32
|
+
/** Focused step within the focused trail, or null for the whole flow. */
|
|
33
|
+
focusedStepIndex: number | null;
|
|
34
|
+
/** Hovered trail/step, for the preview highlight (`stepIndex: null` = whole). */
|
|
35
|
+
hoveredTrailStep: { trailId: string; stepIndex: number | null } | null;
|
|
36
|
+
/** Pick a trail: focus the whole flow — this opens the step pane. */
|
|
37
|
+
onFocusFlow: (trail: SubsystemTrail) => void;
|
|
38
|
+
/** Back: clear the focus — this closes the step pane. */
|
|
39
|
+
onClearFocus: () => void;
|
|
40
|
+
onFocusStep: (trail: SubsystemTrail, stepIndex: number) => void;
|
|
41
|
+
onHoverStep: (trail: SubsystemTrail, stepIndex: number) => void;
|
|
42
|
+
onHoverFlow: (trail: SubsystemTrail) => void;
|
|
43
|
+
onLeaveStep: () => void;
|
|
44
|
+
/** Aliases of components marked `proposed` — a row whose steps touch one tints. */
|
|
45
|
+
proposedAliases?: ReadonlySet<string>;
|
|
46
|
+
/** Slide duration in ms. @default 340 */
|
|
47
|
+
transitionMs?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PROPOSED_COLOR = 'darkgoldenrod';
|
|
51
|
+
|
|
52
|
+
const PANE: CSSProperties = {
|
|
53
|
+
position: 'absolute',
|
|
54
|
+
inset: 0,
|
|
55
|
+
display: 'flex',
|
|
56
|
+
flexDirection: 'column',
|
|
57
|
+
minHeight: 0,
|
|
58
|
+
willChange: 'transform',
|
|
59
|
+
};
|
|
60
|
+
|
|
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
|
+
export function TrailStepsNav({
|
|
67
|
+
trails,
|
|
68
|
+
focusedTrailId,
|
|
69
|
+
focusedStepIndex,
|
|
70
|
+
hoveredTrailStep,
|
|
71
|
+
onFocusFlow,
|
|
72
|
+
onClearFocus,
|
|
73
|
+
onFocusStep,
|
|
74
|
+
onHoverStep,
|
|
75
|
+
onHoverFlow,
|
|
76
|
+
onLeaveStep,
|
|
77
|
+
proposedAliases,
|
|
78
|
+
transitionMs = 340,
|
|
79
|
+
}: TrailStepsNavProps) {
|
|
80
|
+
const { theme } = useTheme();
|
|
81
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
82
|
+
const hoverBg = theme.colors.backgroundHover ?? theme.colors.backgroundSecondary ?? '#232323';
|
|
83
|
+
const transition = `transform ${transitionMs}ms cubic-bezier(0.2, 0, 0, 1)`;
|
|
84
|
+
|
|
85
|
+
// Hovering a collapsed row previews the whole trail while nothing is focused;
|
|
86
|
+
// the list pane is the only place that happens.
|
|
87
|
+
const [hoveredRow, setHoveredRow] = useState<string | null>(null);
|
|
88
|
+
// Hovering the detail title (or the close) lights the close, so the title
|
|
89
|
+
// reads as the same back action.
|
|
90
|
+
const [headerHover, setHeaderHover] = useState(false);
|
|
91
|
+
|
|
92
|
+
const focused = trails.find((t) => t.id === focusedTrailId) ?? null;
|
|
93
|
+
const open = focused != null;
|
|
94
|
+
|
|
95
|
+
// The step pane is a translated layer, not a mounted/unmounted one: on back,
|
|
96
|
+
// `focused` goes null immediately, which would blank the pane's content the
|
|
97
|
+
// instant the slide-out starts. Hold the last focused trail through the
|
|
98
|
+
// transition, then drop it — slide first, unmount after. A fresh focus lands
|
|
99
|
+
// immediately (and cancels any pending drop).
|
|
100
|
+
const [displayedTrail, setDisplayedTrail] = useState<SubsystemTrail | null>(focused);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (focused) {
|
|
103
|
+
setDisplayedTrail(focused);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const timer = setTimeout(() => setDisplayedTrail(null), transitionMs);
|
|
107
|
+
return () => clearTimeout(timer);
|
|
108
|
+
}, [focused, transitionMs]);
|
|
109
|
+
|
|
110
|
+
// A focused trail (from the graph side) always lands on the step pane.
|
|
111
|
+
const leaveList = useCallback(() => {
|
|
112
|
+
setHoveredRow(null);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (open) setHoveredRow(null);
|
|
117
|
+
setHeaderHover(false);
|
|
118
|
+
}, [open]);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div style={{ flex: 1, minHeight: 0, position: 'relative', overflow: 'hidden' }}>
|
|
122
|
+
{/* List pane. */}
|
|
123
|
+
<div
|
|
124
|
+
aria-hidden={open}
|
|
125
|
+
style={{
|
|
126
|
+
...PANE,
|
|
127
|
+
transform: open ? 'translateX(-100%)' : 'translateX(0)',
|
|
128
|
+
transition,
|
|
129
|
+
}}
|
|
130
|
+
onMouseLeave={leaveList}
|
|
131
|
+
>
|
|
132
|
+
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '0 0 12px' }}>
|
|
133
|
+
{trails.map((trail) => {
|
|
134
|
+
const proposed = touchesProposed(trail, proposedAliases);
|
|
135
|
+
const focusedRow = trail.id === focusedTrailId;
|
|
136
|
+
const hovered = hoveredRow === trail.id;
|
|
137
|
+
return (
|
|
138
|
+
<button
|
|
139
|
+
key={trail.id}
|
|
140
|
+
type="button"
|
|
141
|
+
onMouseEnter={() => {
|
|
142
|
+
setHoveredRow(trail.id);
|
|
143
|
+
onHoverFlow(trail);
|
|
144
|
+
}}
|
|
145
|
+
onMouseLeave={leaveList}
|
|
146
|
+
onClick={() => onFocusFlow(trail)}
|
|
147
|
+
style={{
|
|
148
|
+
display: 'flex',
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
gap: 8,
|
|
151
|
+
width: '100%',
|
|
152
|
+
minWidth: 0,
|
|
153
|
+
padding: '10px 8px 10px 12px',
|
|
154
|
+
border: 'none',
|
|
155
|
+
borderRadius: 0,
|
|
156
|
+
background: focusedRow || hovered ? hoverBg : 'transparent',
|
|
157
|
+
cursor: 'pointer',
|
|
158
|
+
textAlign: 'left',
|
|
159
|
+
transition: 'background 120ms ease',
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<Route
|
|
163
|
+
size={14}
|
|
164
|
+
strokeWidth={2}
|
|
165
|
+
aria-hidden
|
|
166
|
+
style={{
|
|
167
|
+
flexShrink: 0,
|
|
168
|
+
color: focusedRow || hovered ? theme.colors.primary : muted,
|
|
169
|
+
transition: 'color 120ms ease',
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
<span
|
|
173
|
+
style={{
|
|
174
|
+
flex: 1,
|
|
175
|
+
minWidth: 0,
|
|
176
|
+
overflowWrap: 'anywhere',
|
|
177
|
+
fontSize: theme.fontSizes[1],
|
|
178
|
+
fontFamily: theme.fonts.monospace,
|
|
179
|
+
fontWeight: 600,
|
|
180
|
+
color: proposed ? PROPOSED_COLOR : theme.colors.text,
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{trail.title}
|
|
184
|
+
</span>
|
|
185
|
+
<span
|
|
186
|
+
style={{
|
|
187
|
+
flexShrink: 0,
|
|
188
|
+
fontSize: theme.fontSizes[0],
|
|
189
|
+
fontFamily: theme.fonts.monospace,
|
|
190
|
+
color: muted,
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
{trail.steps.length}
|
|
194
|
+
</span>
|
|
195
|
+
<ChevronRight size={14} strokeWidth={2} style={{ flexShrink: 0, color: muted }} />
|
|
196
|
+
</button>
|
|
197
|
+
);
|
|
198
|
+
})}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Step pane. */}
|
|
203
|
+
<div
|
|
204
|
+
aria-hidden={!open}
|
|
205
|
+
style={{
|
|
206
|
+
...PANE,
|
|
207
|
+
transform: open ? 'translateX(0)' : 'translateX(100%)',
|
|
208
|
+
transition,
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{displayedTrail && (
|
|
212
|
+
<>
|
|
213
|
+
<div
|
|
214
|
+
style={{
|
|
215
|
+
flexShrink: 0,
|
|
216
|
+
display: 'flex',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
gap: 8,
|
|
219
|
+
// Left inset + leading cell + gap match the step rows below, so
|
|
220
|
+
// the trail icon and the step numbers share one column.
|
|
221
|
+
padding: '6px 6px 6px 12px',
|
|
222
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
<Route
|
|
226
|
+
size={14}
|
|
227
|
+
strokeWidth={2}
|
|
228
|
+
aria-hidden
|
|
229
|
+
// The focused trail: the icon stays primary, matching the row's
|
|
230
|
+
// hover colour in the list it came from.
|
|
231
|
+
style={{ flexShrink: 0, color: theme.colors.primary }}
|
|
232
|
+
/>
|
|
233
|
+
{/* The title is the back affordance too: clicking it goes back to
|
|
234
|
+
the list, like the trailing close. */}
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={onClearFocus}
|
|
238
|
+
onMouseEnter={() => setHeaderHover(true)}
|
|
239
|
+
onMouseLeave={() => setHeaderHover(false)}
|
|
240
|
+
style={{
|
|
241
|
+
flex: 1,
|
|
242
|
+
minWidth: 0,
|
|
243
|
+
display: 'flex',
|
|
244
|
+
alignItems: 'center',
|
|
245
|
+
padding: 0,
|
|
246
|
+
border: 'none',
|
|
247
|
+
background: 'transparent',
|
|
248
|
+
cursor: 'pointer',
|
|
249
|
+
textAlign: 'left',
|
|
250
|
+
overflowWrap: 'anywhere',
|
|
251
|
+
fontSize: theme.fontSizes[1],
|
|
252
|
+
fontFamily: theme.fonts.monospace,
|
|
253
|
+
fontWeight: 600,
|
|
254
|
+
color: touchesProposed(displayedTrail, proposedAliases) ? PROPOSED_COLOR : theme.colors.text,
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
{displayedTrail.title}
|
|
258
|
+
</button>
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
aria-label="Back to trails"
|
|
262
|
+
title="All trails"
|
|
263
|
+
onClick={onClearFocus}
|
|
264
|
+
onMouseEnter={() => setHeaderHover(true)}
|
|
265
|
+
onMouseLeave={() => setHeaderHover(false)}
|
|
266
|
+
style={{
|
|
267
|
+
display: 'inline-flex',
|
|
268
|
+
alignItems: 'center',
|
|
269
|
+
justifyContent: 'center',
|
|
270
|
+
flexShrink: 0,
|
|
271
|
+
width: 24,
|
|
272
|
+
height: 24,
|
|
273
|
+
padding: 0,
|
|
274
|
+
border: 'none',
|
|
275
|
+
borderRadius: 4,
|
|
276
|
+
background: headerHover ? (theme.colors.border ?? hoverBg) : 'transparent',
|
|
277
|
+
color: headerHover ? theme.colors.text : muted,
|
|
278
|
+
cursor: 'pointer',
|
|
279
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
<X size={16} strokeWidth={2} />
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
<div
|
|
286
|
+
style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '0 0 12px' }}
|
|
287
|
+
onMouseLeave={onLeaveStep}
|
|
288
|
+
>
|
|
289
|
+
{displayedTrail.steps.map((step, i) => {
|
|
290
|
+
const active = focusedStepIndex === i;
|
|
291
|
+
const hovered =
|
|
292
|
+
hoveredTrailStep?.trailId === displayedTrail.id &&
|
|
293
|
+
hoveredTrailStep.stepIndex === i;
|
|
294
|
+
return (
|
|
295
|
+
<button
|
|
296
|
+
key={`${step.from}->${step.to}-${i}`}
|
|
297
|
+
type="button"
|
|
298
|
+
onMouseEnter={() => onHoverStep(displayedTrail, i)}
|
|
299
|
+
onClick={() => onFocusStep(displayedTrail, i)}
|
|
300
|
+
style={{
|
|
301
|
+
display: 'flex',
|
|
302
|
+
alignItems: 'flex-start',
|
|
303
|
+
gap: 8,
|
|
304
|
+
width: '100%',
|
|
305
|
+
minWidth: 0,
|
|
306
|
+
padding: '8px 16px 8px 12px',
|
|
307
|
+
border: 'none',
|
|
308
|
+
borderRadius: 0,
|
|
309
|
+
background: active || hovered ? hoverBg : 'transparent',
|
|
310
|
+
cursor: 'pointer',
|
|
311
|
+
textAlign: 'left',
|
|
312
|
+
transition: 'background 120ms ease',
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
<span
|
|
316
|
+
style={{
|
|
317
|
+
flexShrink: 0,
|
|
318
|
+
// Same 14px leading column as the header's trail icon,
|
|
319
|
+
// so the digit sits under it.
|
|
320
|
+
width: 14,
|
|
321
|
+
textAlign: 'center',
|
|
322
|
+
paddingTop: 1,
|
|
323
|
+
fontSize: theme.fontSizes[0],
|
|
324
|
+
fontFamily: theme.fonts.monospace,
|
|
325
|
+
color: active ? theme.colors.text : muted,
|
|
326
|
+
}}
|
|
327
|
+
>
|
|
328
|
+
{i + 1}
|
|
329
|
+
</span>
|
|
330
|
+
<span
|
|
331
|
+
style={{
|
|
332
|
+
flex: 1,
|
|
333
|
+
minWidth: 0,
|
|
334
|
+
display: 'flex',
|
|
335
|
+
flexDirection: 'column',
|
|
336
|
+
gap: 2,
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
{step.annotation && (
|
|
340
|
+
<span
|
|
341
|
+
style={{
|
|
342
|
+
minWidth: 0,
|
|
343
|
+
overflowWrap: 'anywhere',
|
|
344
|
+
fontSize: theme.fontSizes[1],
|
|
345
|
+
fontFamily: theme.fonts.monospace,
|
|
346
|
+
color: theme.colors.text,
|
|
347
|
+
}}
|
|
348
|
+
>
|
|
349
|
+
{step.annotation}
|
|
350
|
+
</span>
|
|
351
|
+
)}
|
|
352
|
+
<span
|
|
353
|
+
style={{
|
|
354
|
+
fontSize: theme.fontSizes[0],
|
|
355
|
+
fontFamily: theme.fonts.body,
|
|
356
|
+
color: muted,
|
|
357
|
+
lineHeight: 1.35,
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
{step.symbol ?? step.from}
|
|
361
|
+
</span>
|
|
362
|
+
</span>
|
|
363
|
+
</button>
|
|
364
|
+
);
|
|
365
|
+
})}
|
|
366
|
+
</div>
|
|
367
|
+
</>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
@@ -223,7 +223,7 @@ function TrailFlow({
|
|
|
223
223
|
display: 'flex',
|
|
224
224
|
alignItems: 'center',
|
|
225
225
|
gap: 4,
|
|
226
|
-
|
|
226
|
+
minHeight: TRAIL_CONTROL_SIZE,
|
|
227
227
|
padding: '0 0 0 16px',
|
|
228
228
|
background: wholeFlowActive || headerHover ? hoverBg : 'transparent',
|
|
229
229
|
transition: 'background 120ms ease',
|
|
@@ -259,9 +259,8 @@ function TrailFlow({
|
|
|
259
259
|
>
|
|
260
260
|
<span
|
|
261
261
|
style={{
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
whiteSpace: 'nowrap',
|
|
262
|
+
minWidth: 0,
|
|
263
|
+
overflowWrap: 'anywhere',
|
|
265
264
|
fontSize: theme.fontSizes[1],
|
|
266
265
|
fontFamily: theme.fonts.monospace,
|
|
267
266
|
fontWeight: 600,
|