@principal-ai/subsystems-react 0.56.11 → 0.56.13
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/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +10 -4
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/DescriptionOverlay.d.ts +6 -0
- package/dist/subsystem/DescriptionOverlay.d.ts.map +1 -0
- package/dist/subsystem/DescriptionOverlay.js +58 -0
- package/dist/subsystem/DescriptionOverlay.js.map +1 -0
- package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
- package/dist/subsystem/DiagnosticToggle.js +2 -1
- package/dist/subsystem/DiagnosticToggle.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +307 -284
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -1
- package/dist/subsystem/SubsystemFileTree.js +2 -1
- package/dist/subsystem/SubsystemFileTree.js.map +1 -1
- package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
- package/dist/subsystem/TrailStepsNav.js +11 -8
- package/dist/subsystem/TrailStepsNav.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts +1 -1
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +42 -66
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/package.json +1 -1
- package/src/stories/Canvases/C4/C4Draft.stories.tsx +238 -0
- package/src/stories/Canvases/Subsystem/TrailSidebarViews.stories.tsx +2 -2
- package/src/subsystem/C4Graph.tsx +10 -4
- package/src/subsystem/DescriptionOverlay.test.tsx +76 -0
- package/src/subsystem/DescriptionOverlay.tsx +101 -0
- package/src/subsystem/DiagnosticToggle.tsx +2 -1
- package/src/subsystem/SubsystemComponentGraph.sidebar.test.tsx +81 -10
- package/src/subsystem/SubsystemComponentGraph.tsx +68 -26
- package/src/subsystem/SubsystemFileTree.tsx +2 -1
- package/src/subsystem/TrailStepsNav.test.tsx +38 -0
- package/src/subsystem/TrailStepsNav.tsx +11 -8
- package/src/subsystem/TrailsPanel.test.tsx +99 -35
- package/src/subsystem/TrailsPanel.tsx +38 -85
|
@@ -89,6 +89,7 @@ import type { ComponentVerificationState } from './ComponentDeclaration';
|
|
|
89
89
|
import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
|
|
90
90
|
import { FileDrawer, FILE_DRAWER_HEIGHT_MS } from './FileDrawer';
|
|
91
91
|
import { GraphWithSidebar } from './GraphWithSidebar';
|
|
92
|
+
import { DescriptionOverlay } from './DescriptionOverlay';
|
|
92
93
|
import type { GraphSidebarTab } from './GraphSidebar';
|
|
93
94
|
import { TrailsPanel, TRAIL_PLAY_PAUSE_MS } from './TrailsPanel';
|
|
94
95
|
import type { StepEmphasisMap } from './stepEmphasis';
|
|
@@ -404,7 +405,8 @@ export interface SubsystemComponentGraphProps {
|
|
|
404
405
|
/**
|
|
405
406
|
* Verification issues for this graph (audit findings). When diagnostics are
|
|
406
407
|
* active the sidebar's bottom panel becomes the issue list instead of the
|
|
407
|
-
* file tree / trails.
|
|
408
|
+
* file tree / trails. Graph issue badges and issue-driven dimming are only
|
|
409
|
+
* shown in that mode.
|
|
408
410
|
*/
|
|
409
411
|
issues?: SubsystemIssue[];
|
|
410
412
|
/**
|
|
@@ -617,6 +619,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
617
619
|
function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirection: layoutDirectionProp, defaultLayoutDirection = 'horizontal', onLayoutDirectionChange, hideLayoutToggle = false, refocusOnLayoutFlip = true, initialTrailId, onTrailSelect, onTrailStepHover, onReorderTrails, stepEmphasis, onRemoveStepEmphasis, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, issues, showIssues, filesActive, sidebarScope, onSidebarTrailChange, focusIssueCategory, laneStatus, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, codeProcessKeys, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
|
|
618
620
|
const { theme } = useTheme();
|
|
619
621
|
const { fitView, fitBounds } = useReactFlow();
|
|
622
|
+
const [descriptionOpen, setDescriptionOpen] = useState(false);
|
|
620
623
|
const viewport = useViewport();
|
|
621
624
|
// Restored once per mount from `localStorage` (see `readViewState`). Each
|
|
622
625
|
// graph is its own tab/mount, so `persistKey` is stable for a mount.
|
|
@@ -1262,7 +1265,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1262
1265
|
// node says "diagnostics here" for every finding the sidebar lists.
|
|
1263
1266
|
const issueBadgeByAlias = useMemo(() => {
|
|
1264
1267
|
const map = new Map<string, SubsystemNodeIssue>();
|
|
1265
|
-
if (!issues?.length) return map;
|
|
1268
|
+
if (showIssues !== true || !issues?.length) return map;
|
|
1266
1269
|
for (const issue of issues) {
|
|
1267
1270
|
const rung = issueRung(issue.kind);
|
|
1268
1271
|
if (!rung && !ISSUE_KIND_ICON[issue.kind]) continue;
|
|
@@ -1297,7 +1300,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1297
1300
|
if (kinded) map.set(alias, { ...badge, kind: kinded.kind });
|
|
1298
1301
|
}
|
|
1299
1302
|
return map;
|
|
1300
|
-
}, [issues, issueComponent]);
|
|
1303
|
+
}, [showIssues, issues, issueComponent]);
|
|
1301
1304
|
|
|
1302
1305
|
// Per-frame diagnostics badge: the same fold, but for findings about a
|
|
1303
1306
|
// BOUNDARY rather than a construct. Keyed by the region's React Flow node
|
|
@@ -1308,7 +1311,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1308
1311
|
// borrowing its layer's icon would imply the frame is at fault for it.
|
|
1309
1312
|
const regionIssueByNodeId = useMemo(() => {
|
|
1310
1313
|
const map = new Map<string, SubsystemRegionIssue>();
|
|
1311
|
-
if (!issues?.length) return map;
|
|
1314
|
+
if (showIssues !== true || !issues?.length) return map;
|
|
1312
1315
|
for (const issue of issues) {
|
|
1313
1316
|
const target = issue.target;
|
|
1314
1317
|
if (!target || !ISSUE_KIND_ICON[issue.kind]) continue;
|
|
@@ -1329,7 +1332,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1329
1332
|
});
|
|
1330
1333
|
}
|
|
1331
1334
|
return map;
|
|
1332
|
-
}, [issues]);
|
|
1335
|
+
}, [showIssues, issues]);
|
|
1333
1336
|
|
|
1334
1337
|
// Per-edge step verification badge: maps edge IDs to step verification status.
|
|
1335
1338
|
// Keyed by the React Flow edge id (from → mechanism → to). An edge can have
|
|
@@ -1347,7 +1350,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1347
1350
|
step_span_stale: 1,
|
|
1348
1351
|
};
|
|
1349
1352
|
const map = new Map<string, { kind: StepIssueKind; severity: 'info' | 'error'; count: number }>();
|
|
1350
|
-
if (!issues?.length || !trails?.length) {
|
|
1353
|
+
if (showIssues !== true || !issues?.length || !trails?.length) {
|
|
1351
1354
|
return map;
|
|
1352
1355
|
}
|
|
1353
1356
|
for (const issue of issues) {
|
|
@@ -1374,7 +1377,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1374
1377
|
});
|
|
1375
1378
|
}
|
|
1376
1379
|
return map;
|
|
1377
|
-
}, [issues, trails, issueStep]);
|
|
1380
|
+
}, [showIssues, issues, trails, issueStep]);
|
|
1378
1381
|
|
|
1379
1382
|
// Expanded verification layers in the diagnostics list. When any layer with
|
|
1380
1383
|
// findings is expanded, the canvas dims everything that layer does not
|
|
@@ -1423,7 +1426,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1423
1426
|
// Returns null when nothing is expanded, no expanded layer has findings, or
|
|
1424
1427
|
// the findings implicate nothing — so no dimming is applied.
|
|
1425
1428
|
const issueFocus = useMemo(() => {
|
|
1426
|
-
if (!issues?.length || expandedIssueCategories.length === 0) return null;
|
|
1429
|
+
if (showIssues !== true || !issues?.length || expandedIssueCategories.length === 0) return null;
|
|
1427
1430
|
const active = new Set(expandedIssueCategories);
|
|
1428
1431
|
const activeIssues = issues.filter((i) => active.has(issueCategory(i)));
|
|
1429
1432
|
if (activeIssues.length === 0) return null;
|
|
@@ -1478,7 +1481,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1478
1481
|
if (nodeIds.has(e.source) && nodeIds.has(e.target)) edgeIds.add(e.id);
|
|
1479
1482
|
}
|
|
1480
1483
|
return { nodeIds, edgeIds };
|
|
1481
|
-
}, [issues, expandedIssueCategories, components, trails, resolveAlias]);
|
|
1484
|
+
}, [showIssues, issues, expandedIssueCategories, components, trails, resolveAlias]);
|
|
1482
1485
|
const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
|
|
1483
1486
|
const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
|
|
1484
1487
|
|
|
@@ -2729,17 +2732,45 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2729
2732
|
{sidebarExtra}
|
|
2730
2733
|
{description && (
|
|
2731
2734
|
<div style={{ fontSize: theme.fontSizes[0], lineHeight: 1.5 }}>
|
|
2732
|
-
<
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2735
|
+
<div style={{
|
|
2736
|
+
display: '-webkit-box',
|
|
2737
|
+
WebkitBoxOrient: 'vertical',
|
|
2738
|
+
WebkitLineClamp: 6,
|
|
2739
|
+
maxHeight: '9em',
|
|
2740
|
+
overflow: 'hidden',
|
|
2741
|
+
}}>
|
|
2742
|
+
<IndustryMarkdownSlide
|
|
2743
|
+
content={description}
|
|
2744
|
+
slideIdPrefix="subsystem-desc"
|
|
2745
|
+
slideIndex={0}
|
|
2746
|
+
isVisible={true}
|
|
2747
|
+
theme={theme}
|
|
2748
|
+
disableScroll={true}
|
|
2749
|
+
disableBasePadding
|
|
2750
|
+
enableKeyboardScrolling={false}
|
|
2751
|
+
autoFocusOnVisible={false}
|
|
2752
|
+
/>
|
|
2753
|
+
</div>
|
|
2754
|
+
<button
|
|
2755
|
+
type="button"
|
|
2756
|
+
aria-label="Read full description"
|
|
2757
|
+
aria-haspopup="dialog"
|
|
2758
|
+
aria-expanded={descriptionOpen}
|
|
2759
|
+
onClick={() => setDescriptionOpen(true)}
|
|
2760
|
+
style={{
|
|
2761
|
+
marginTop: 6,
|
|
2762
|
+
padding: 0,
|
|
2763
|
+
border: 'none',
|
|
2764
|
+
background: 'transparent',
|
|
2765
|
+
color: theme.colors.textSecondary,
|
|
2766
|
+
fontFamily: theme.fonts.body,
|
|
2767
|
+
fontSize: theme.fontSizes[0],
|
|
2768
|
+
textDecoration: 'underline',
|
|
2769
|
+
cursor: 'pointer',
|
|
2770
|
+
}}
|
|
2771
|
+
>
|
|
2772
|
+
More
|
|
2773
|
+
</button>
|
|
2743
2774
|
</div>
|
|
2744
2775
|
)}
|
|
2745
2776
|
{sidebarAfterDescription}
|
|
@@ -2765,6 +2796,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2765
2796
|
<div style={{ padding: 16, color: theme.colors.textSecondary }}>No files for this trail.</div>
|
|
2766
2797
|
) : (
|
|
2767
2798
|
<SubsystemFileTree
|
|
2799
|
+
headerless
|
|
2768
2800
|
files={treeFilePaths}
|
|
2769
2801
|
selectedFile={openFile ? treePaths.toDisplay.get(openFile) : null}
|
|
2770
2802
|
onSelectFile={(path) => {
|
|
@@ -2862,7 +2894,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2862
2894
|
<GraphWithSidebar
|
|
2863
2895
|
hidden={!sidebarVisible}
|
|
2864
2896
|
bodyHidden={!sidebarBodyVisible}
|
|
2865
|
-
headerMaxHeight={sidebarBodyVisible ? '30%' : '100%'}
|
|
2866
2897
|
header={sidebarHeader}
|
|
2867
2898
|
tabs={sidebarTabs}
|
|
2868
2899
|
activeTab={diagnosticsTab}
|
|
@@ -2875,6 +2906,9 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2875
2906
|
persistKey={persistKey}
|
|
2876
2907
|
onResizeEnd={() => requestAnimationFrame(() => fitView())}
|
|
2877
2908
|
>
|
|
2909
|
+
{!hideSidebar && description && descriptionOpen && (
|
|
2910
|
+
<DescriptionOverlay description={description} onClose={() => setDescriptionOpen(false)} />
|
|
2911
|
+
)}
|
|
2878
2912
|
{/* Graph area */}
|
|
2879
2913
|
<div style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: committedDirection === 'vertical' ? 'row' : 'column', minHeight: 0, minWidth: 0 }}>
|
|
2880
2914
|
{/* Canvas box — the edge-label overlay and legend anchor here, so
|
|
@@ -3413,6 +3447,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3413
3447
|
);
|
|
3414
3448
|
const fileCount = sidebarScope.treePaths.entries.length;
|
|
3415
3449
|
const trailViewLabel = sidebarScope.trail ? 'Trail' : 'Trails';
|
|
3450
|
+
const trailCount = props.trails?.length ?? 0;
|
|
3416
3451
|
const hasFiles = fileCount > 0 || sidebarScope.trail != null;
|
|
3417
3452
|
const sidebarDiagnostic: SubsystemDiagnostic = sidebarScope.trail ? {
|
|
3418
3453
|
...props.diagnostic,
|
|
@@ -3422,18 +3457,22 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3422
3457
|
fixableCount: sidebarScope.issues.filter((issue) => issue.fix != null).length,
|
|
3423
3458
|
title: undefined,
|
|
3424
3459
|
} : props.diagnostic ?? { status: 'unknown', issueCount: props.issues?.length };
|
|
3460
|
+
const hasIssues = (props.issues != null
|
|
3461
|
+
? sidebarScope.issues.length
|
|
3462
|
+
: sidebarDiagnostic.issueCount ?? 0) > 0;
|
|
3425
3463
|
useEffect(() => {
|
|
3426
3464
|
if (sidebarTrailId != null && !sidebarScope.trail) setSidebarTrailId(null);
|
|
3427
3465
|
}, [sidebarTrailId, sidebarScope.trail]);
|
|
3428
3466
|
const [diagnosticsOpen, setDiagnosticsOpen] = useState<boolean>(
|
|
3429
3467
|
props.showIssues ?? (props.issues?.length ?? 0) > 0,
|
|
3430
3468
|
);
|
|
3431
|
-
const
|
|
3469
|
+
const issuesRequested = props.showIssues ?? diagnosticsOpen;
|
|
3470
|
+
const issuesActive = hasIssues && issuesRequested;
|
|
3432
3471
|
const filesActive = filesOpen && hasFiles && !issuesActive;
|
|
3433
3472
|
const selectSidebarView = (view: 'trails' | 'files' | 'issues') => {
|
|
3434
3473
|
const nextIssuesActive = view === 'issues';
|
|
3435
3474
|
setFilesOpen(view === 'files');
|
|
3436
|
-
if (nextIssuesActive !==
|
|
3475
|
+
if (nextIssuesActive !== issuesRequested) {
|
|
3437
3476
|
if (props.showIssues === undefined) setDiagnosticsOpen(nextIssuesActive);
|
|
3438
3477
|
props.diagnostic?.onToggle?.();
|
|
3439
3478
|
}
|
|
@@ -3444,7 +3483,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3444
3483
|
justifyContent: 'center',
|
|
3445
3484
|
gap: 4,
|
|
3446
3485
|
height: 28,
|
|
3447
|
-
padding: '4px
|
|
3486
|
+
padding: '4px 16px',
|
|
3448
3487
|
fontFamily: theme.fonts.monospace,
|
|
3449
3488
|
fontSize: theme.fontSizes[0],
|
|
3450
3489
|
border: 'none',
|
|
@@ -3535,13 +3574,14 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3535
3574
|
>
|
|
3536
3575
|
<button
|
|
3537
3576
|
type="button"
|
|
3538
|
-
aria-label={
|
|
3577
|
+
aria-label={sidebarScope.trail ? 'Trail' : `Trails (${trailCount} ${trailCount === 1 ? 'trail' : 'trails'})`}
|
|
3539
3578
|
aria-pressed={!filesActive && !issuesActive}
|
|
3540
3579
|
onClick={() => selectSidebarView('trails')}
|
|
3541
3580
|
style={viewButtonStyle(!filesActive && !issuesActive)}
|
|
3542
3581
|
>
|
|
3543
3582
|
<Route size={14} aria-hidden />
|
|
3544
3583
|
<span>{trailViewLabel}</span>
|
|
3584
|
+
{!sidebarScope.trail && <span style={{ fontWeight: 600, marginLeft: 4 }}>{trailCount}</span>}
|
|
3545
3585
|
</button>
|
|
3546
3586
|
{hasFiles && (
|
|
3547
3587
|
<button
|
|
@@ -3554,9 +3594,10 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3554
3594
|
>
|
|
3555
3595
|
<Files size={14} aria-hidden />
|
|
3556
3596
|
<span>Files</span>
|
|
3557
|
-
<span style={{ fontWeight: 600 }}>{fileCount}</span>
|
|
3597
|
+
<span style={{ fontWeight: 600, marginLeft: 4 }}>{fileCount}</span>
|
|
3558
3598
|
</button>
|
|
3559
3599
|
)}
|
|
3600
|
+
{hasIssues && (
|
|
3560
3601
|
<SubsystemDiagnosticToggle
|
|
3561
3602
|
{...sidebarDiagnostic}
|
|
3562
3603
|
labelText="Issues"
|
|
@@ -3564,6 +3605,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3564
3605
|
active={issuesActive}
|
|
3565
3606
|
onToggle={() => selectSidebarView('issues')}
|
|
3566
3607
|
/>
|
|
3608
|
+
)}
|
|
3567
3609
|
</div>
|
|
3568
3610
|
</header>
|
|
3569
3611
|
)}
|
|
@@ -143,6 +143,7 @@ export function SubsystemFileTree({ files, selectedFile, hoveredFile, hoveredFol
|
|
|
143
143
|
ref={treeRef}
|
|
144
144
|
onClickCapture={handleContainerClick}
|
|
145
145
|
style={{
|
|
146
|
+
paddingTop: headerless ? 8 : undefined,
|
|
146
147
|
flex: 1,
|
|
147
148
|
minHeight: 0,
|
|
148
149
|
display: 'flex',
|
|
@@ -187,7 +188,7 @@ export function SubsystemFileTree({ files, selectedFile, hoveredFile, hoveredFol
|
|
|
187
188
|
'--trees-fg-muted-override': muted,
|
|
188
189
|
'--trees-accent-override': theme.colors.primary,
|
|
189
190
|
'--trees-font-family-override': theme.fonts.monospace,
|
|
190
|
-
'--trees-font-size-override': `${theme.fontSizes[
|
|
191
|
+
'--trees-font-size-override': `${theme.fontSizes[1]}px`,
|
|
191
192
|
'--trees-theme-list-hover-bg': theme.colors.border,
|
|
192
193
|
'--trees-theme-list-active-selection-bg': theme.colors.border,
|
|
193
194
|
} as CSSProperties
|
|
@@ -56,6 +56,39 @@ function renderNav(props: Record<string, unknown>) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
describe('TrailStepsNav', () => {
|
|
59
|
+
for (const title of ['Short flow', 'Generate the account report and deliver it to every subscribed recipient']) {
|
|
60
|
+
for (const selected of [false, true]) {
|
|
61
|
+
test(`keeps explicit title spacing in the ${selected ? 'selected header' : 'list'} for ${title}`, () => {
|
|
62
|
+
const { getByRole } = renderNav({
|
|
63
|
+
trails: [{ ...TRAILS[0], title }],
|
|
64
|
+
focusedTrailId: selected ? 't1' : null,
|
|
65
|
+
focusedStepIndex: null,
|
|
66
|
+
hoveredTrailStep: null,
|
|
67
|
+
onFocusFlow: () => {},
|
|
68
|
+
onClearFocus: () => {},
|
|
69
|
+
onFocusStep: () => {},
|
|
70
|
+
onHoverStep: () => {},
|
|
71
|
+
onHoverFlow: () => {},
|
|
72
|
+
onLeaveStep: () => {},
|
|
73
|
+
});
|
|
74
|
+
const button = getByRole('button', { name: selected ? title : new RegExp(title), exact: selected });
|
|
75
|
+
const row = selected ? button.parentElement! : button;
|
|
76
|
+
const label = selected ? button : button.children[1] as HTMLElement;
|
|
77
|
+
expect(row.style.paddingTop).toBe('14px');
|
|
78
|
+
expect(row.style.paddingBottom).toBe('14px');
|
|
79
|
+
expect(row.style.paddingLeft).toBe('12px');
|
|
80
|
+
expect(row.style.paddingRight).toBe(row.style.paddingLeft);
|
|
81
|
+
expect(row.style.alignItems).toBe('flex-start');
|
|
82
|
+
expect(label.style.lineHeight).toBe('20px');
|
|
83
|
+
expect((row.firstElementChild as SVGElement).style.marginTop).toBe('3px');
|
|
84
|
+
if (selected) {
|
|
85
|
+
expect(row.style.borderBottomStyle).toBe('');
|
|
86
|
+
expect(getByRole('button', { name: 'Back to trails' }).style.height).toBe('20px');
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
test('keeps the selected trail header when files or issues replace the step body', () => {
|
|
60
93
|
let cleared = false;
|
|
61
94
|
const { getByRole, getByText, queryByText } = renderNav({
|
|
@@ -183,6 +216,11 @@ describe('TrailStepsNav', () => {
|
|
|
183
216
|
expect(getByText('B.go').style.fontFamily).toBe(defaultEditorTheme.fonts.body);
|
|
184
217
|
expect(getByRole('button', { name: /A\.go/ }).style.paddingLeft).toBe('12px');
|
|
185
218
|
expect(getByRole('button', { name: /B\.go/ }).style.paddingLeft).toBe('22px');
|
|
219
|
+
for (const name of [/A\.go/, /B\.go/]) {
|
|
220
|
+
const row = getByRole('button', { name });
|
|
221
|
+
expect(row.style.paddingTop).toBe('14px');
|
|
222
|
+
expect(row.style.paddingBottom).toBe('14px');
|
|
223
|
+
}
|
|
186
224
|
fireEvent.click(getByRole('button', { name: /B\.go/ }));
|
|
187
225
|
expect(focusedStep).toBe(1);
|
|
188
226
|
fireEvent.click(getByLabelText('Back to trails'));
|
|
@@ -152,11 +152,11 @@ export function TrailStepsNav({
|
|
|
152
152
|
onClick={() => onFocusFlow(trail)}
|
|
153
153
|
style={{
|
|
154
154
|
display: 'flex',
|
|
155
|
-
alignItems: '
|
|
155
|
+
alignItems: 'flex-start',
|
|
156
156
|
gap: 8,
|
|
157
157
|
width: '100%',
|
|
158
158
|
minWidth: 0,
|
|
159
|
-
padding: '
|
|
159
|
+
padding: '14px 12px',
|
|
160
160
|
border: 'none',
|
|
161
161
|
borderRadius: 0,
|
|
162
162
|
background: focusedRow || hovered ? hoverBg : 'transparent',
|
|
@@ -171,6 +171,7 @@ export function TrailStepsNav({
|
|
|
171
171
|
aria-hidden
|
|
172
172
|
style={{
|
|
173
173
|
flexShrink: 0,
|
|
174
|
+
marginTop: 3,
|
|
174
175
|
color: focusedRow || hovered ? theme.colors.primary : muted,
|
|
175
176
|
transition: 'color 120ms ease',
|
|
176
177
|
}}
|
|
@@ -183,6 +184,7 @@ export function TrailStepsNav({
|
|
|
183
184
|
fontSize: theme.fontSizes[1],
|
|
184
185
|
fontFamily: theme.fonts.monospace,
|
|
185
186
|
fontWeight: 600,
|
|
187
|
+
lineHeight: '20px',
|
|
186
188
|
color: theme.colors.text,
|
|
187
189
|
}}
|
|
188
190
|
>
|
|
@@ -192,6 +194,7 @@ export function TrailStepsNav({
|
|
|
192
194
|
style={{
|
|
193
195
|
flexShrink: 0,
|
|
194
196
|
fontSize: theme.fontSizes[0],
|
|
197
|
+
lineHeight: '20px',
|
|
195
198
|
fontFamily: theme.fonts.monospace,
|
|
196
199
|
color: muted,
|
|
197
200
|
}}
|
|
@@ -220,12 +223,11 @@ export function TrailStepsNav({
|
|
|
220
223
|
style={{
|
|
221
224
|
flexShrink: 0,
|
|
222
225
|
display: 'flex',
|
|
223
|
-
alignItems: '
|
|
226
|
+
alignItems: 'flex-start',
|
|
224
227
|
gap: 8,
|
|
225
228
|
// Left inset + leading cell + gap match the step rows below, so
|
|
226
229
|
// the trail icon and the step numbers share one column.
|
|
227
|
-
padding: '
|
|
228
|
-
borderBottom: `1px solid ${theme.colors.border}`,
|
|
230
|
+
padding: '14px 12px',
|
|
229
231
|
}}
|
|
230
232
|
>
|
|
231
233
|
<Route
|
|
@@ -234,7 +236,7 @@ export function TrailStepsNav({
|
|
|
234
236
|
aria-hidden
|
|
235
237
|
// The focused trail: the icon stays primary, matching the row's
|
|
236
238
|
// hover colour in the list it came from.
|
|
237
|
-
style={{ flexShrink: 0, color: theme.colors.primary }}
|
|
239
|
+
style={{ flexShrink: 0, marginTop: 3, color: theme.colors.primary }}
|
|
238
240
|
/>
|
|
239
241
|
{/* The title is the back affordance too: clicking it goes back to
|
|
240
242
|
the list, like the trailing close. */}
|
|
@@ -257,6 +259,7 @@ export function TrailStepsNav({
|
|
|
257
259
|
fontSize: theme.fontSizes[1],
|
|
258
260
|
fontFamily: theme.fonts.monospace,
|
|
259
261
|
fontWeight: 600,
|
|
262
|
+
lineHeight: '20px',
|
|
260
263
|
color: theme.colors.text,
|
|
261
264
|
}}
|
|
262
265
|
>
|
|
@@ -275,7 +278,7 @@ export function TrailStepsNav({
|
|
|
275
278
|
justifyContent: 'center',
|
|
276
279
|
flexShrink: 0,
|
|
277
280
|
width: 24,
|
|
278
|
-
height:
|
|
281
|
+
height: 20,
|
|
279
282
|
padding: 0,
|
|
280
283
|
border: 'none',
|
|
281
284
|
borderRadius: 4,
|
|
@@ -311,7 +314,7 @@ export function TrailStepsNav({
|
|
|
311
314
|
gap: 8,
|
|
312
315
|
width: '100%',
|
|
313
316
|
minWidth: 0,
|
|
314
|
-
padding: `
|
|
317
|
+
padding: `14px 16px 14px ${12 + Math.max(0, stackDepth - 1) * 10}px`,
|
|
315
318
|
border: 'none',
|
|
316
319
|
borderRadius: 0,
|
|
317
320
|
background: active || hovered ? hoverBg : 'transparent',
|
|
@@ -115,8 +115,6 @@ function renderPanel(overrides: Partial<TrailsPanelProps> = {}) {
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
const COPY_LABEL = 'Copy step 1 of Auth flow for an agent';
|
|
119
|
-
|
|
120
118
|
describe('TrailsPanel drilldown navigation', () => {
|
|
121
119
|
test('clicking a trail opens its steps, and Back restores the list without playback', () => {
|
|
122
120
|
const onFocusFlow = mock(() => {});
|
|
@@ -165,14 +163,17 @@ describe('TrailsPanel drilldown navigation', () => {
|
|
|
165
163
|
expect(queryByRole('button', { name: /Login.submit/ })).toBeNull();
|
|
166
164
|
});
|
|
167
165
|
|
|
168
|
-
test('
|
|
169
|
-
const
|
|
166
|
+
test('supports Command-click copy in the focused pane without extra controls', async () => {
|
|
167
|
+
const onFocusStep = mock(() => {});
|
|
168
|
+
const { getByText, getByLabelText, queryByLabelText, queryByRole } = renderPanel({
|
|
170
169
|
drilldown: true,
|
|
171
170
|
focusedTrailId: 'auth-flow',
|
|
171
|
+
onFocusStep,
|
|
172
172
|
});
|
|
173
|
-
fireEvent.
|
|
174
|
-
fireEvent.click(getByLabelText(COPY_LABEL));
|
|
173
|
+
fireEvent.click(getByText('Login.submit').closest('button')!, { metaKey: true });
|
|
175
174
|
await waitFor(() => expect(writeText).toHaveBeenCalledWith(buildStepBrief(trails[0]!, 0)));
|
|
175
|
+
expect(onFocusStep).not.toHaveBeenCalled();
|
|
176
|
+
expect(queryByRole('button', { name: /Copy step/ })).toBeNull();
|
|
176
177
|
expect(queryByLabelText('Play Auth flow')).toBeNull();
|
|
177
178
|
expect(queryByLabelText('Collapse Auth flow')).toBeNull();
|
|
178
179
|
expect(getByLabelText('Back to trails')).toBeTruthy();
|
|
@@ -188,6 +189,35 @@ describe('TrailsPanel drilldown navigation', () => {
|
|
|
188
189
|
});
|
|
189
190
|
|
|
190
191
|
describe('TrailsPanel trail titles', () => {
|
|
192
|
+
for (const title of ['Short flow', 'Validate every selected checkout item and persist the complete customer order']) {
|
|
193
|
+
for (const collapsed of [true, false]) {
|
|
194
|
+
test(`keeps explicit title spacing for ${collapsed ? 'collapsed' : 'expanded'} ${title}`, () => {
|
|
195
|
+
const { getByRole } = renderPanel({
|
|
196
|
+
trails: [{ ...trails[0]!, title }],
|
|
197
|
+
expandedTrails: collapsed ? new Set() : new Set(['auth-flow']),
|
|
198
|
+
onReorder: () => {},
|
|
199
|
+
});
|
|
200
|
+
const button = getByRole('button', { name: title, exact: true });
|
|
201
|
+
const row = button.parentElement!;
|
|
202
|
+
expect(row.style.paddingTop).toBe('14px');
|
|
203
|
+
expect(row.style.paddingBottom).toBe('14px');
|
|
204
|
+
expect(row.style.paddingLeft).toBe('16px');
|
|
205
|
+
expect(row.style.paddingRight).toBe(row.style.paddingLeft);
|
|
206
|
+
expect(row.style.minHeight).toBe('');
|
|
207
|
+
expect(row.style.alignItems).toBe('flex-start');
|
|
208
|
+
expect(button.style.padding).toBe('0px');
|
|
209
|
+
expect((button.lastElementChild as HTMLElement).style.lineHeight).toBe('20px');
|
|
210
|
+
expect((button.firstElementChild as SVGElement).style.marginTop).toBe('3px');
|
|
211
|
+
if (collapsed) {
|
|
212
|
+
expect(getByRole('button', { name: `Reorder ${title}` }).style.height).toBe('20px');
|
|
213
|
+
expect(getByRole('button', { name: `Reorder ${title}` }).style.marginRight).toBe('');
|
|
214
|
+
} else {
|
|
215
|
+
expect(getByRole('button', { name: `Play ${title}` }).style.height).toBe('20px');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
191
221
|
test('highlights immediately without a CSS fade while graph preview waits', async () => {
|
|
192
222
|
const onHoverFlow = mock(() => {});
|
|
193
223
|
const onLeaveStep = mock(() => {});
|
|
@@ -253,30 +283,28 @@ describe('TrailsPanel trail titles', () => {
|
|
|
253
283
|
});
|
|
254
284
|
|
|
255
285
|
describe('TrailsPanel step copy', () => {
|
|
256
|
-
test('
|
|
257
|
-
const { getByText,
|
|
258
|
-
expect(queryByLabelText(COPY_LABEL)).toBeNull();
|
|
286
|
+
test('does not show a copy button on hover or keyboard focus', () => {
|
|
287
|
+
const { getByText, queryByRole, container } = renderPanel();
|
|
259
288
|
const row = getByText('Login.submit').closest('button')!;
|
|
260
289
|
fireEvent.mouseEnter(row);
|
|
261
|
-
expect(queryByLabelText(COPY_LABEL)).not.toBeNull();
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test('copy icon is revealed on keyboard focus', () => {
|
|
265
|
-
const { getByText, queryByLabelText } = renderPanel();
|
|
266
|
-
const row = getByText('Login.submit').closest('button')!;
|
|
267
290
|
fireEvent.focus(row);
|
|
268
|
-
expect(
|
|
291
|
+
expect(queryByRole('button', { name: /Copy step/ })).toBeNull();
|
|
292
|
+
expect(container.querySelector('.lucide-copy')).toBeNull();
|
|
293
|
+
expect(row.hasAttribute('title')).toBe(false);
|
|
294
|
+
expect(row.firstElementChild?.textContent).toBe('1');
|
|
269
295
|
});
|
|
270
296
|
|
|
271
|
-
test('
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
fireEvent.
|
|
276
|
-
|
|
277
|
-
|
|
297
|
+
test('Command-click copies the brief without focusing and replaces the number with a checkmark', async () => {
|
|
298
|
+
const onFocusStep = mock(() => {});
|
|
299
|
+
const { getByText, getByRole } = renderPanel({ onFocusStep });
|
|
300
|
+
const row = getByText('Login.submit').closest('button')!;
|
|
301
|
+
fireEvent.click(row, { metaKey: true });
|
|
302
|
+
await waitFor(() => expect(getByRole('status', { name: 'Copied' })).toBeTruthy());
|
|
303
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
278
304
|
expect(writeText).toHaveBeenCalledWith(buildStepBrief(trails[0]!, 0));
|
|
279
|
-
expect(
|
|
305
|
+
expect(onFocusStep).not.toHaveBeenCalled();
|
|
306
|
+
expect(row.hasAttribute('title')).toBe(false);
|
|
307
|
+
expect(row.firstElementChild?.querySelector('.lucide-check')).not.toBeNull();
|
|
280
308
|
});
|
|
281
309
|
|
|
282
310
|
test('hovering the row still focuses the step on click', () => {
|
|
@@ -286,26 +314,40 @@ describe('TrailsPanel step copy', () => {
|
|
|
286
314
|
fireEvent.mouseEnter(row);
|
|
287
315
|
fireEvent.click(row);
|
|
288
316
|
expect(onFocusStep).toHaveBeenCalledWith(trails[0]!, 0);
|
|
317
|
+
expect(writeText).not.toHaveBeenCalled();
|
|
289
318
|
});
|
|
290
319
|
|
|
291
320
|
test('the copied checkmark clears after the feedback window', async () => {
|
|
292
|
-
const { getByText,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
await waitFor(() => expect(copy.getAttribute('title')).toBe('Copied'));
|
|
321
|
+
const { getByText, getByRole, queryByRole } = renderPanel();
|
|
322
|
+
const row = getByText('Login.submit').closest('button')!;
|
|
323
|
+
fireEvent.click(row, { metaKey: true });
|
|
324
|
+
await waitFor(() => expect(getByRole('status', { name: 'Copied' })).toBeTruthy());
|
|
297
325
|
await new Promise((r) => setTimeout(r, STEP_COPY_FEEDBACK_MS + 20));
|
|
298
|
-
await waitFor(() => expect(
|
|
326
|
+
await waitFor(() => expect(queryByRole('status', { name: 'Copied' })).toBeNull());
|
|
327
|
+
expect(row.firstElementChild?.textContent).toBe('1');
|
|
299
328
|
});
|
|
300
329
|
|
|
301
330
|
test('a failed copy shows no copied state', async () => {
|
|
302
331
|
writeText.mockImplementationOnce(() => Promise.reject(new Error('denied')));
|
|
303
|
-
const { getByText,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
fireEvent.click(copy);
|
|
332
|
+
const { getByText, queryByRole } = renderPanel();
|
|
333
|
+
const row = getByText('Login.submit').closest('button')!;
|
|
334
|
+
fireEvent.click(row, { metaKey: true });
|
|
307
335
|
await new Promise((r) => setTimeout(r, 10));
|
|
308
|
-
expect(
|
|
336
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
337
|
+
expect(queryByRole('status', { name: 'Copied' })).toBeNull();
|
|
338
|
+
expect(row.firstElementChild?.textContent).toBe('1');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('an unavailable clipboard leaves the number and does not focus the step', () => {
|
|
342
|
+
Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true });
|
|
343
|
+
const onFocusStep = mock(() => {});
|
|
344
|
+
const { getByText, queryByRole } = renderPanel({ onFocusStep });
|
|
345
|
+
const row = getByText('Login.submit').closest('button')!;
|
|
346
|
+
fireEvent.click(row, { metaKey: true });
|
|
347
|
+
expect(writeText).not.toHaveBeenCalled();
|
|
348
|
+
expect(onFocusStep).not.toHaveBeenCalled();
|
|
349
|
+
expect(queryByRole('status', { name: 'Copied' })).toBeNull();
|
|
350
|
+
expect(row.firstElementChild?.textContent).toBe('1');
|
|
309
351
|
});
|
|
310
352
|
});
|
|
311
353
|
|
|
@@ -334,9 +376,31 @@ describe('TrailsPanel step semantics', () => {
|
|
|
334
376
|
expect(symbol.style.color).toBe(normal.style.color);
|
|
335
377
|
expect(symbol.style.fontFamily).toBe(defaultEditorTheme.fonts.body);
|
|
336
378
|
expect(symbol.previousElementSibling).toBe(annotation);
|
|
379
|
+
const row = symbol.closest('button')!;
|
|
380
|
+
expect(row.style.paddingTop).toBe('14px');
|
|
381
|
+
expect(row.style.paddingBottom).toBe('14px');
|
|
382
|
+
expect(row.style.paddingLeft).toBe(drilldown ? '12px' : '16px');
|
|
383
|
+
expect((row.firstElementChild as HTMLElement).style.width).toBe('14px');
|
|
384
|
+
expect((row.firstElementChild as HTMLElement).style.textAlign).toBe('center');
|
|
337
385
|
fireEvent.click(getByRole('button', { name: /Submit the login request.*Login.submit/ }));
|
|
338
386
|
expect(onFocusStep).toHaveBeenCalledWith(trail, 0);
|
|
339
387
|
});
|
|
388
|
+
|
|
389
|
+
test(`keeps nested steps offset from the ${drilldown ? 'selected' : 'inline'} header column`, () => {
|
|
390
|
+
const trail: SubsystemTrail = {
|
|
391
|
+
...trails[0]!,
|
|
392
|
+
steps: [
|
|
393
|
+
{ ...trails[0]!.steps[0]!, from: 'a', to: 'b', symbol: 'A.go' },
|
|
394
|
+
{ ...trails[0]!.steps[0]!, from: 'b', to: 'c', symbol: 'B.go' },
|
|
395
|
+
{ ...trails[0]!.steps[0]!, from: 'a', to: 'd', symbol: 'A.next' },
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
const { getByText } = renderPanel({ trails: [trail], drilldown, focusedTrailId: trail.id });
|
|
399
|
+
const inset = drilldown ? 12 : 16;
|
|
400
|
+
expect(getByText('A.go').closest('button')!.style.paddingLeft).toBe(`${inset}px`);
|
|
401
|
+
expect(getByText('B.go').closest('button')!.style.paddingLeft).toBe(`${inset + 10}px`);
|
|
402
|
+
expect(getByText('A.next').closest('button')!.style.paddingLeft).toBe(`${inset}px`);
|
|
403
|
+
});
|
|
340
404
|
}
|
|
341
405
|
|
|
342
406
|
test('calls leads with the caller frame', () => {
|