@principal-ai/subsystems-react 0.55.1 → 0.55.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/graphify/index.d.ts +2 -0
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js +1 -0
- package/dist/graphify/index.js.map +1 -1
- package/dist/graphify/symbolFocus.d.ts +49 -0
- package/dist/graphify/symbolFocus.d.ts.map +1 -0
- package/dist/graphify/symbolFocus.js +139 -0
- package/dist/graphify/symbolFocus.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/stories/Canvases/Aggregate/aggregateViewFixture.d.ts.map +1 -0
- package/dist/stories/Canvases/Aggregate/aggregateViewFixture.js.map +1 -0
- package/dist/stories/Canvases/Subsystem/fixtures.d.ts.map +1 -0
- package/dist/stories/Canvases/Subsystem/fixtures.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +35 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +248 -33
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SymbolFocusedView.d.ts +30 -0
- package/dist/subsystem/SymbolFocusedView.d.ts.map +1 -0
- package/dist/subsystem/SymbolFocusedView.js +24 -0
- package/dist/subsystem/SymbolFocusedView.js.map +1 -0
- package/dist/subsystem/TrailsPanel.d.ts +16 -1
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +64 -8
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/graphChrome.d.ts +13 -0
- package/dist/subsystem/graphChrome.d.ts.map +1 -1
- package/dist/subsystem/graphChrome.js +19 -1
- package/dist/subsystem/graphChrome.js.map +1 -1
- package/dist/subsystem/model.d.ts +40 -4
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +38 -7
- package/dist/subsystem/model.js.map +1 -1
- package/package.json +2 -2
- package/src/graphify/index.ts +6 -0
- package/src/graphify/symbolFocus.test.ts +163 -0
- package/src/graphify/symbolFocus.ts +190 -0
- package/src/index.ts +7 -0
- package/src/stories/{Subsystem/AggregateGraph → Canvases/Aggregate}/AggregateFrameGraph.stories.tsx +1 -1
- package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4CardGrow.stories.tsx +1 -1
- package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4Component.stories.tsx +1 -1
- package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4Container.stories.tsx +1 -1
- package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4NodeGallery.stories.tsx +1 -1
- package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4ToComponentDrill.stories.tsx +1 -1
- package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4Transition.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/AnimatedProgression.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Appearance.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Basics.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Captures.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Constructs.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/CustomEntities.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Deprecated.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/DetailPanel.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Diagnostics.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/EdgeViews.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/FrameworkStereotype.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/GraphTitle.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Issues.stories.tsx +1 -1
- package/src/stories/Canvases/Subsystem/LayoutDirection.stories.tsx +122 -0
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/ModuleBadges.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/ModulePathNesting.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Modules.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/NodeAnatomy.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Packages.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Processes.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Proposed.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/ProposedTrails.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Reorder.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Scenarios.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Spotlights.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/StoreValueType.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/TrailAutoplay.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Trails.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/WorkspacePackageFrames.stories.tsx +1 -1
- package/src/stories/Canvases/SymbolFocusedView/SymbolFocusedView.stories.tsx +163 -0
- package/src/stories/{Subsystem → Components}/AgentsPanel.stories.tsx +1 -1
- package/src/stories/{Subsystem/ComponentDeclarationAudit.stories.tsx → Components/ComponentDeclaration/Audit.stories.tsx} +4 -4
- package/src/stories/{Subsystem/ComponentDeclarationSymbols.stories.tsx → Components/ComponentDeclaration/Symbols.stories.tsx} +5 -5
- package/src/stories/{Subsystem → Components}/ConstructsCatalog.stories.tsx +1 -1
- package/src/stories/{Subsystem/Sidebar → Components}/GraphSidebar.stories.tsx +4 -4
- package/src/stories/{Subsystem/IssueIcons.stories.tsx → Components/IssueList.stories.tsx} +1 -1
- package/src/stories/{Subsystem/StoreValueTypeDeclaration.stories.tsx → Components/StoreValueType.stories.tsx} +1 -1
- package/src/subsystem/SubsystemComponentGraph.tsx +331 -25
- package/src/subsystem/SymbolFocusedView.tsx +80 -0
- package/src/subsystem/TrailsPanel.tsx +107 -4
- package/src/subsystem/graphChrome.tsx +22 -1
- package/src/subsystem/layoutDirection.test.ts +26 -0
- package/src/subsystem/model.test.ts +17 -0
- package/src/subsystem/model.ts +70 -10
- package/dist/stories/Subsystem/AggregateGraph/aggregateViewFixture.d.ts.map +0 -1
- package/dist/stories/Subsystem/AggregateGraph/aggregateViewFixture.js.map +0 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +0 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +0 -1
- package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +0 -351
- /package/dist/stories/{Subsystem/AggregateGraph → Canvases/Aggregate}/aggregateViewFixture.d.ts +0 -0
- /package/dist/stories/{Subsystem/AggregateGraph → Canvases/Aggregate}/aggregateViewFixture.js +0 -0
- /package/dist/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/fixtures.d.ts +0 -0
- /package/dist/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/fixtures.js +0 -0
- /package/src/graphify/__fixtures__/{ego-samples.json → symbol-focused-samples.json} +0 -0
- /package/src/stories/{Subsystem/AggregateGraph → Canvases/Aggregate}/aggregateViewFixture.ts +0 -0
- /package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/fixtures.ts +0 -0
|
@@ -2,12 +2,12 @@ import React from 'react';
|
|
|
2
2
|
import '@xyflow/react/dist/style.css';
|
|
3
3
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
4
|
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
5
|
-
import { ComponentDeclaration } from '
|
|
6
|
-
import type { SubsystemComponent } from '
|
|
7
|
-
import type { GraphifyComponentDetail } from '
|
|
5
|
+
import { ComponentDeclaration } from '../../../subsystem/ComponentDeclaration';
|
|
6
|
+
import type { SubsystemComponent } from '../../../subsystem/model';
|
|
7
|
+
import type { GraphifyComponentDetail } from '../../../graphify';
|
|
8
8
|
|
|
9
9
|
const meta = {
|
|
10
|
-
title: '
|
|
10
|
+
title: 'Components/ComponentDeclaration/Audit',
|
|
11
11
|
component: ComponentDeclaration,
|
|
12
12
|
parameters: { layout: 'padded' },
|
|
13
13
|
tags: ['autodocs'],
|
|
@@ -2,13 +2,13 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import '@xyflow/react/dist/style.css';
|
|
3
3
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
4
|
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
5
|
-
import { ComponentDeclaration } from '
|
|
6
|
-
import type { SubsystemComponent } from '
|
|
7
|
-
import type { GraphifyComponentDetail } from '
|
|
8
|
-
import type { SymbolInspection } from '
|
|
5
|
+
import { ComponentDeclaration } from '../../../subsystem/ComponentDeclaration';
|
|
6
|
+
import type { SubsystemComponent } from '../../../subsystem/model';
|
|
7
|
+
import type { GraphifyComponentDetail } from '../../../graphify';
|
|
8
|
+
import type { SymbolInspection } from '../../../subsystem/symbolRefs';
|
|
9
9
|
|
|
10
10
|
const meta = {
|
|
11
|
-
title: '
|
|
11
|
+
title: 'Components/ComponentDeclaration/Symbols',
|
|
12
12
|
component: ComponentDeclaration,
|
|
13
13
|
parameters: { layout: 'padded' },
|
|
14
14
|
tags: ['autodocs'],
|
|
@@ -9,7 +9,7 @@ import type { SymbolInspection } from '../../subsystem/symbolRefs';
|
|
|
9
9
|
import type { GraphifyComponentDetail } from '../../graphify';
|
|
10
10
|
|
|
11
11
|
const meta = {
|
|
12
|
-
title: '
|
|
12
|
+
title: 'Components/ConstructsCatalog',
|
|
13
13
|
component: ConstructsCatalog,
|
|
14
14
|
parameters: { layout: 'fullscreen' },
|
|
15
15
|
tags: ['autodocs'],
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React, { useMemo, useState } from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
4
|
-
import { GraphSidebar } from '
|
|
5
|
-
import { TrailsPanel } from '
|
|
4
|
+
import { GraphSidebar } from '../../subsystem/GraphSidebar';
|
|
5
|
+
import { TrailsPanel } from '../../subsystem/TrailsPanel';
|
|
6
6
|
import {
|
|
7
7
|
DRILLDOWN_COMPONENTS,
|
|
8
8
|
DRILLDOWN_DOC,
|
|
9
9
|
DRILLDOWN_TRAILS,
|
|
10
|
-
} from '
|
|
10
|
+
} from '../../subsystem/__fixtures__/c4Drilldown';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* `GraphSidebar` — the side panel extracted out of `SubsystemComponentGraph`,
|
|
@@ -176,7 +176,7 @@ function PanelDemo({ controlled = false }: { controlled?: boolean }) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
const meta = {
|
|
179
|
-
title: '
|
|
179
|
+
title: 'Components/GraphSidebar',
|
|
180
180
|
component: GraphSidebar,
|
|
181
181
|
parameters: { layout: 'fullscreen' },
|
|
182
182
|
tags: ['autodocs'],
|
|
@@ -17,7 +17,7 @@ import type { SubsystemComponent } from '../../subsystem/model';
|
|
|
17
17
|
* storage backing. The value type is panel-only.
|
|
18
18
|
*/
|
|
19
19
|
const meta = {
|
|
20
|
-
title: '
|
|
20
|
+
title: 'Components/StoreValueType',
|
|
21
21
|
parameters: { layout: 'padded' },
|
|
22
22
|
tags: ['autodocs'],
|
|
23
23
|
decorators: [
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
applyNodeChanges,
|
|
33
33
|
} from '@xyflow/react';
|
|
34
34
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
35
|
-
import { FileText, X } from 'lucide-react';
|
|
35
|
+
import { FileText, ArrowDown, ArrowRight, Unlink, X } from 'lucide-react';
|
|
36
36
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
37
37
|
import {
|
|
38
38
|
buildSubsystemGraph,
|
|
@@ -45,10 +45,12 @@ import {
|
|
|
45
45
|
MECHANISM_DESCRIPTIONS,
|
|
46
46
|
subsystemGraphLayoutKey,
|
|
47
47
|
trailStepGraphEdgeId,
|
|
48
|
+
unroutedEdgeIds,
|
|
48
49
|
type SubsystemComponentEdge,
|
|
49
50
|
type SubsystemComponent,
|
|
50
51
|
type SubsystemEdgeProvenance,
|
|
51
52
|
type SubsystemEdgeView,
|
|
53
|
+
type SubsystemLayoutDirection,
|
|
52
54
|
type SubsystemGraphifyRelation,
|
|
53
55
|
type SubsystemNodeIssue,
|
|
54
56
|
type SubsystemRegionIssue,
|
|
@@ -80,7 +82,7 @@ import {
|
|
|
80
82
|
} from './IssueList';
|
|
81
83
|
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
82
84
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
83
|
-
import { GRAPH_CANVAS_CLASS, GRAPH_NAV_PROPS, GraphChrome, GraphLayerStyle } from './graphChrome';
|
|
85
|
+
import { GRAPH_CANVAS_CLASS, GRAPH_NAV_PROPS, GraphChrome, GraphLayerStyle, LAYOUT_FLIP_CLASS, LAYOUT_FLIP_EDGE_MS, LAYOUT_FLIP_NODE_MS } from './graphChrome';
|
|
84
86
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
85
87
|
import type { ComponentVerificationState } from './ComponentDeclaration';
|
|
86
88
|
import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
|
|
@@ -160,6 +162,8 @@ interface PersistedViewState {
|
|
|
160
162
|
sidebarWidth?: number;
|
|
161
163
|
sidebarView?: 'files' | 'trails';
|
|
162
164
|
diagnosticsTab?: 'issues' | 'agents';
|
|
165
|
+
/** Flow direction chosen with the canvas toggle, restored on the next visit. */
|
|
166
|
+
layoutDirection?: SubsystemLayoutDirection;
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
const VIEW_STATE_PREFIX = 'principal.subsystems.viewState.';
|
|
@@ -216,6 +220,40 @@ export interface SubsystemComponentGraphProps {
|
|
|
216
220
|
* `line` for this to have any effect. @default false
|
|
217
221
|
*/
|
|
218
222
|
orderByLine?: boolean;
|
|
223
|
+
/**
|
|
224
|
+
* Flow direction of the layered layout. `horizontal` (default) runs layers
|
|
225
|
+
* left-to-right; `vertical` runs them top-to-bottom. Edge-less containment
|
|
226
|
+
* graphs keep their DOWN + grid treatment either way.
|
|
227
|
+
*
|
|
228
|
+
* Pass this to control the direction from the host; leave it unset to let
|
|
229
|
+
* the graph own it (seeded by `defaultLayoutDirection`, updated by the
|
|
230
|
+
* built-in toggle, and persisted under `persistKey`).
|
|
231
|
+
*/
|
|
232
|
+
layoutDirection?: SubsystemLayoutDirection;
|
|
233
|
+
/**
|
|
234
|
+
* Initial flow direction when uncontrolled. Ignored when `layoutDirection`
|
|
235
|
+
* is provided. @default 'horizontal'
|
|
236
|
+
*/
|
|
237
|
+
defaultLayoutDirection?: SubsystemLayoutDirection;
|
|
238
|
+
/**
|
|
239
|
+
* Fired when the built-in toggle (or a host control) requests a different
|
|
240
|
+
* direction. Hosts that pass `layoutDirection` must update it here; hosts
|
|
241
|
+
* that don't can use it to observe the change.
|
|
242
|
+
*/
|
|
243
|
+
onLayoutDirectionChange?: (direction: SubsystemLayoutDirection) => void;
|
|
244
|
+
/**
|
|
245
|
+
* Hide the built-in horizontal/vertical toggle in the canvas corner.
|
|
246
|
+
* @default false
|
|
247
|
+
*/
|
|
248
|
+
hideLayoutToggle?: boolean;
|
|
249
|
+
/**
|
|
250
|
+
* Refit the camera to the new layout after a layout flip — a direction
|
|
251
|
+
* change or a trail-focus change. On by default: a vertical layout grows
|
|
252
|
+
* downward and would otherwise leave the graph half off-screen. Turn off for
|
|
253
|
+
* embeds that own their own camera.
|
|
254
|
+
* @default true
|
|
255
|
+
*/
|
|
256
|
+
refocusOnLayoutFlip?: boolean;
|
|
219
257
|
/**
|
|
220
258
|
* Deep-link target: when set, the matching trail is selected on mount
|
|
221
259
|
* — its steps expanded and its flow focused on the canvas. Hosts use this
|
|
@@ -574,7 +612,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
574
612
|
measured: { w: number; h: number } | null;
|
|
575
613
|
}
|
|
576
614
|
|
|
577
|
-
function Inner({ components, trails, graphifyRelations, orderByLine, initialTrailId, onTrailSelect, onTrailStepHover, onReorderTrails, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, title, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, laneStatus, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, codeProcessKeys, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
|
|
615
|
+
function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirection: layoutDirectionProp, defaultLayoutDirection = 'horizontal', onLayoutDirectionChange, hideLayoutToggle = false, refocusOnLayoutFlip = true, initialTrailId, onTrailSelect, onTrailStepHover, onReorderTrails, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, title, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, laneStatus, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, codeProcessKeys, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
|
|
578
616
|
const { theme } = useTheme();
|
|
579
617
|
const { fitView, fitBounds, screenToFlowPosition } = useReactFlow();
|
|
580
618
|
const viewport = useViewport();
|
|
@@ -595,6 +633,151 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
595
633
|
absoluteRects: new Map(),
|
|
596
634
|
});
|
|
597
635
|
const [layoutReady, setLayoutReady] = useState(false);
|
|
636
|
+
|
|
637
|
+
// --- Layout direction -----------------------------------------------------
|
|
638
|
+
// The graph owns direction unless the host passes `layoutDirection`. A change
|
|
639
|
+
// is sequenced so old edge routes never cut across the new layout: edges fade
|
|
640
|
+
// out, ELK re-lays out while the nodes slide to their new slots, then the
|
|
641
|
+
// edges fade back in. Two values keep that honest: `requestedDirection` is
|
|
642
|
+
// what the toggle shows, `committedDirection` is what ELK last ran with.
|
|
643
|
+
const [ownDirection, setOwnDirection] = useState<SubsystemLayoutDirection>(
|
|
644
|
+
() => persisted.layoutDirection ?? defaultLayoutDirection,
|
|
645
|
+
);
|
|
646
|
+
const requestedDirection = layoutDirectionProp ?? ownDirection;
|
|
647
|
+
const [committedDirection, setCommittedDirection] =
|
|
648
|
+
useState<SubsystemLayoutDirection>(requestedDirection);
|
|
649
|
+
const [layoutFlipping, setLayoutFlipping] = useState(false);
|
|
650
|
+
const flipSawPendingLayoutRef = useRef(false);
|
|
651
|
+
/** Pending fade-out timer; non-null only between request and commit. */
|
|
652
|
+
const flipFadeTimerRef = useRef<number | null>(null);
|
|
653
|
+
/** Set when a layout flip starts; the camera refits once layout lands. */
|
|
654
|
+
const refitAfterLayoutRef = useRef(false);
|
|
655
|
+
|
|
656
|
+
/** Solo layout: only this trail's components and edges are laid out. */
|
|
657
|
+
const [layoutTrailId, setLayoutTrailId] = useState<string | null>(null);
|
|
658
|
+
const [committedTrailId, setCommittedTrailId] = useState<string | null>(null);
|
|
659
|
+
|
|
660
|
+
const changeLayoutDirection = useCallback(
|
|
661
|
+
(next: SubsystemLayoutDirection) => {
|
|
662
|
+
if (next === requestedDirection) return;
|
|
663
|
+
if (layoutDirectionProp == null) setOwnDirection(next);
|
|
664
|
+
onLayoutDirectionChange?.(next);
|
|
665
|
+
},
|
|
666
|
+
[layoutDirectionProp, onLayoutDirectionChange, requestedDirection],
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
const toggleLayoutFocus = useCallback((trailId: string) => {
|
|
670
|
+
setLayoutTrailId((cur) => (cur === trailId ? null : trailId));
|
|
671
|
+
}, []);
|
|
672
|
+
|
|
673
|
+
// A focused trail that disappears (model reload) releases the solo layout
|
|
674
|
+
// instead of pinning the canvas to a slice that no longer exists.
|
|
675
|
+
useEffect(() => {
|
|
676
|
+
if (layoutTrailId && !(trails ?? []).some((t) => t.id === layoutTrailId)) {
|
|
677
|
+
setLayoutTrailId(null);
|
|
678
|
+
}
|
|
679
|
+
}, [trails, layoutTrailId]);
|
|
680
|
+
|
|
681
|
+
const requestedLayoutKey = `${requestedDirection}|${layoutTrailId ?? ''}`;
|
|
682
|
+
const committedLayoutKey = `${committedDirection}|${committedTrailId ?? ''}`;
|
|
683
|
+
|
|
684
|
+
// Beat 1: request → fade the edges out, then commit the direction / focus
|
|
685
|
+
// (which re-runs ELK). Reduced-motion users get the new layout immediately.
|
|
686
|
+
useEffect(() => {
|
|
687
|
+
if (requestedLayoutKey === committedLayoutKey) {
|
|
688
|
+
// A host can revert a request before the fade-out commits. Drop the
|
|
689
|
+
// pending flip instead of leaving the canvas edge-less until the safety
|
|
690
|
+
// timeout. A normal commit also lands here (request now equals
|
|
691
|
+
// committed), but by then the timer ref is already null — and the
|
|
692
|
+
// pending refit must survive that pass.
|
|
693
|
+
if (flipFadeTimerRef.current != null) {
|
|
694
|
+
clearTimeout(flipFadeTimerRef.current);
|
|
695
|
+
flipFadeTimerRef.current = null;
|
|
696
|
+
setLayoutFlipping(false);
|
|
697
|
+
refitAfterLayoutRef.current = false;
|
|
698
|
+
}
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
refitAfterLayoutRef.current = refocusOnLayoutFlip;
|
|
702
|
+
const reduceMotion =
|
|
703
|
+
typeof window !== 'undefined' &&
|
|
704
|
+
typeof window.matchMedia === 'function' &&
|
|
705
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
706
|
+
if (reduceMotion) {
|
|
707
|
+
setCommittedDirection(requestedDirection);
|
|
708
|
+
setCommittedTrailId(layoutTrailId);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
flipSawPendingLayoutRef.current = false;
|
|
712
|
+
setLayoutFlipping(true);
|
|
713
|
+
flipFadeTimerRef.current = window.setTimeout(() => {
|
|
714
|
+
flipFadeTimerRef.current = null;
|
|
715
|
+
setCommittedDirection(requestedDirection);
|
|
716
|
+
setCommittedTrailId(layoutTrailId);
|
|
717
|
+
}, LAYOUT_FLIP_EDGE_MS);
|
|
718
|
+
return () => {
|
|
719
|
+
if (flipFadeTimerRef.current != null) {
|
|
720
|
+
clearTimeout(flipFadeTimerRef.current);
|
|
721
|
+
flipFadeTimerRef.current = null;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}, [
|
|
725
|
+
requestedLayoutKey,
|
|
726
|
+
committedLayoutKey,
|
|
727
|
+
refocusOnLayoutFlip,
|
|
728
|
+
requestedDirection,
|
|
729
|
+
layoutTrailId,
|
|
730
|
+
]);
|
|
731
|
+
|
|
732
|
+
// Beat 3: the committed layout has settled → the node slide is over, fade
|
|
733
|
+
// the edges back in. The ref keeps the OLD `layoutReady` (still true until
|
|
734
|
+
// the new pass 1 commits) from ending the flip before the layout swaps.
|
|
735
|
+
useEffect(() => {
|
|
736
|
+
if (!layoutFlipping) return;
|
|
737
|
+
if (!layoutReady) {
|
|
738
|
+
flipSawPendingLayoutRef.current = true;
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (!flipSawPendingLayoutRef.current) return;
|
|
742
|
+
const t = setTimeout(() => setLayoutFlipping(false), LAYOUT_FLIP_NODE_MS);
|
|
743
|
+
return () => clearTimeout(t);
|
|
744
|
+
}, [layoutFlipping, layoutReady]);
|
|
745
|
+
|
|
746
|
+
// Camera: a direction flip moves the whole graph, and a vertical layout grows
|
|
747
|
+
// downward, so keeping the old viewport would leave most of it off-screen.
|
|
748
|
+
// Refit once the new layout is final — for both the animated flip and the
|
|
749
|
+
// reduced-motion instant swap. The edge reveal is separate (beat 3 above).
|
|
750
|
+
// Keyed on `built` as well as `layoutReady`: whichever of the two passes
|
|
751
|
+
// lands last is the one that has the final positions.
|
|
752
|
+
useEffect(() => {
|
|
753
|
+
if (!layoutReady || !refitAfterLayoutRef.current) return;
|
|
754
|
+
refitAfterLayoutRef.current = false;
|
|
755
|
+
requestAnimationFrame(() => {
|
|
756
|
+
fitView({
|
|
757
|
+
padding: 0.1,
|
|
758
|
+
includeHiddenNodes: false,
|
|
759
|
+
minZoom: 0.05,
|
|
760
|
+
maxZoom: 2,
|
|
761
|
+
duration: LAYOUT_FLIP_NODE_MS,
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
}, [built, layoutReady, fitView]);
|
|
765
|
+
|
|
766
|
+
// Persist the choice so a model reopens the way it was left.
|
|
767
|
+
useEffect(() => {
|
|
768
|
+
if (!persistKey) return;
|
|
769
|
+
writeViewState(persistKey, { layoutDirection: requestedDirection });
|
|
770
|
+
}, [persistKey, requestedDirection]);
|
|
771
|
+
|
|
772
|
+
// Safety net: never leave the canvas edge-less if a layout pass stalls
|
|
773
|
+
// (measurement never lands, ELK hangs). The edges come back; the layout
|
|
774
|
+
// stays wherever it got to.
|
|
775
|
+
useEffect(() => {
|
|
776
|
+
if (!layoutFlipping) return;
|
|
777
|
+
const t = setTimeout(() => setLayoutFlipping(false), 5000);
|
|
778
|
+
return () => clearTimeout(t);
|
|
779
|
+
}, [layoutFlipping]);
|
|
780
|
+
|
|
598
781
|
const [selected, setSelected] = useState<SubsystemComponent | null>(null);
|
|
599
782
|
/** Bottom drawer: single file or trail multi-snippet mode. */
|
|
600
783
|
const [drawerTarget, setDrawerTarget] = useState<DrawerTarget | null>(null);
|
|
@@ -717,8 +900,11 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
717
900
|
// Trails draws runtime step edges. Without visible tabs (no trails,
|
|
718
901
|
// or a sidebar-less embed) fall back to trail edges.
|
|
719
902
|
const sidebarTabsVisible = !hideSidebar && (trails?.length ?? 0) > 0;
|
|
720
|
-
|
|
721
|
-
|
|
903
|
+
// A solo trail layout only draws that flow's steps, so the trail edge source
|
|
904
|
+
// is the only one that can render anything.
|
|
905
|
+
const resolvedEdgeView: SubsystemEdgeView = committedTrailId
|
|
906
|
+
? 'trails'
|
|
907
|
+
: edgeView ??
|
|
722
908
|
(sidebarTabsVisible
|
|
723
909
|
? (sidebarView === 'trails' ? 'trails' : 'graphify')
|
|
724
910
|
: (trails?.length ?? 0) > 0
|
|
@@ -903,16 +1089,39 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
903
1089
|
// Pass 1: build with estimated widths so React Flow can measure the DOM.
|
|
904
1090
|
// The pane stays hidden until Pass 2 completes. Key off layout-affecting
|
|
905
1091
|
// fields only — declarationSpan updates after verify must not re-run ELK.
|
|
1092
|
+
// Solo layout: a trail focus filters the model down to that flow's endpoints
|
|
1093
|
+
// (and drops graphify relations) before it reaches ELK. Built from the
|
|
1094
|
+
// COMMITTED trail, not the requested one, so the swap happens behind the
|
|
1095
|
+
// edge fade like every other layout flip.
|
|
1096
|
+
const layoutDoc = useMemo(() => {
|
|
1097
|
+
if (!committedTrailId) return { components, trails };
|
|
1098
|
+
const trail = (trails ?? []).find((t) => t.id === committedTrailId);
|
|
1099
|
+
if (!trail) return { components, trails };
|
|
1100
|
+
const aliases = new Set<string>();
|
|
1101
|
+
for (const step of trail.steps) {
|
|
1102
|
+
aliases.add(step.from);
|
|
1103
|
+
aliases.add(step.to);
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
components: components.filter((c) => aliases.has(c.alias)),
|
|
1107
|
+
trails: [trail],
|
|
1108
|
+
};
|
|
1109
|
+
}, [components, trails, committedTrailId]);
|
|
1110
|
+
const layoutRelations = committedTrailId ? undefined : graphifyRelations;
|
|
1111
|
+
|
|
906
1112
|
const layoutKey = useMemo(
|
|
907
|
-
() =>
|
|
908
|
-
|
|
1113
|
+
() =>
|
|
1114
|
+
subsystemGraphLayoutKey({
|
|
1115
|
+
components: layoutDoc.components,
|
|
1116
|
+
trails: layoutDoc.trails,
|
|
1117
|
+
graphifyRelations: layoutRelations,
|
|
1118
|
+
}),
|
|
1119
|
+
[layoutDoc, layoutRelations],
|
|
909
1120
|
);
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
trailsRef.current = trails;
|
|
915
|
-
graphifyRelationsRef.current = graphifyRelations;
|
|
1121
|
+
const layoutDocRef = useRef(layoutDoc);
|
|
1122
|
+
const layoutRelationsRef = useRef(layoutRelations);
|
|
1123
|
+
layoutDocRef.current = layoutDoc;
|
|
1124
|
+
layoutRelationsRef.current = layoutRelations;
|
|
916
1125
|
|
|
917
1126
|
// Track measured dimensions from React Flow's dimension changes.
|
|
918
1127
|
// These arrive as { type: 'dimensions', id, dimensions } in onNodesChange.
|
|
@@ -929,17 +1138,15 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
929
1138
|
prevMeasuredSigRef.current = '';
|
|
930
1139
|
// Bump generation so any in-flight Pass 2 from the prior layoutKey is ignored.
|
|
931
1140
|
pass2GenRef.current += 1;
|
|
932
|
-
const doc =
|
|
933
|
-
components: componentsRef.current,
|
|
934
|
-
trails: trailsRef.current,
|
|
935
|
-
};
|
|
1141
|
+
const doc = layoutDocRef.current;
|
|
936
1142
|
void buildSubsystemGraph(doc, {
|
|
937
1143
|
maxNodeWidth,
|
|
938
1144
|
showEdgeLabels,
|
|
939
1145
|
showSingletonFrames,
|
|
940
1146
|
moduleNesting,
|
|
941
|
-
graphifyRelations:
|
|
1147
|
+
graphifyRelations: layoutRelationsRef.current,
|
|
942
1148
|
orderByLine,
|
|
1149
|
+
layoutDirection: committedDirection,
|
|
943
1150
|
codeProcessKeys,
|
|
944
1151
|
})
|
|
945
1152
|
.then(({ nodes, edges: e, absoluteRects }) => {
|
|
@@ -963,7 +1170,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
963
1170
|
setLayoutReady(true);
|
|
964
1171
|
});
|
|
965
1172
|
return () => { alive = false; };
|
|
966
|
-
}, [layoutKey, maxNodeWidth, showEdgeLabels, moduleNesting]);
|
|
1173
|
+
}, [layoutKey, maxNodeWidth, showEdgeLabels, moduleNesting, committedDirection]);
|
|
967
1174
|
|
|
968
1175
|
// Pass 2: once every leaf node has a measured dimension, re-run ELK.
|
|
969
1176
|
// Group parents are sized by ELK, not measured — exclude them or pass 2
|
|
@@ -989,8 +1196,8 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
989
1196
|
const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id)!.height]));
|
|
990
1197
|
const gen = ++pass2GenRef.current;
|
|
991
1198
|
void buildSubsystemGraph(
|
|
992
|
-
|
|
993
|
-
{ maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, moduleNesting, graphifyRelations, orderByLine, codeProcessKeys },
|
|
1199
|
+
layoutDoc,
|
|
1200
|
+
{ maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, moduleNesting, graphifyRelations: layoutRelations, orderByLine, layoutDirection: committedDirection, codeProcessKeys },
|
|
994
1201
|
)
|
|
995
1202
|
.then(({ nodes, edges: e, absoluteRects }) => {
|
|
996
1203
|
if (gen !== pass2GenRef.current) return;
|
|
@@ -1003,7 +1210,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
1003
1210
|
// Reveal Pass 1 layout rather than leaving the cover up forever.
|
|
1004
1211
|
setLayoutReady(true);
|
|
1005
1212
|
});
|
|
1006
|
-
}, [built.nodes,
|
|
1213
|
+
}, [built.nodes, layoutDoc, layoutRelations, orderByLine, maxNodeWidth, showEdgeLabels, moduleNesting, committedDirection]);
|
|
1007
1214
|
|
|
1008
1215
|
// After Pass 1 commits, try Pass 2 immediately with retained measurements.
|
|
1009
1216
|
// Same-id live updates often get no new React Flow `dimensions` events, so
|
|
@@ -1652,6 +1859,12 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
1652
1859
|
return proposedStampedEdges.map((e) => ({ ...e, hidden: !edgeInView(e) }));
|
|
1653
1860
|
}, [proposedStampedEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, previewEdgeIds, issueFocusEdgeIds, resolvedEdgeView]);
|
|
1654
1861
|
|
|
1862
|
+
// Gated on layoutReady so the pre-reveal Pass 1 swap never flashes a count.
|
|
1863
|
+
const unroutedIds = useMemo(
|
|
1864
|
+
() => (layoutReady ? unroutedEdgeIds(dispEdges) : []),
|
|
1865
|
+
[dispEdges, layoutReady],
|
|
1866
|
+
);
|
|
1867
|
+
|
|
1655
1868
|
const onNodesChange = useCallback(
|
|
1656
1869
|
(changes: NodeChange[]) => {
|
|
1657
1870
|
// Capture dimension changes (React Flow's measurement callback).
|
|
@@ -1681,6 +1894,9 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
1681
1894
|
);
|
|
1682
1895
|
|
|
1683
1896
|
useEffect(() => {
|
|
1897
|
+
// A layout flip runs its own camera refit when the new layout lands. This
|
|
1898
|
+
// mount/edit fit would otherwise fire mid-flip and fight it.
|
|
1899
|
+
if (layoutFlipping) return;
|
|
1684
1900
|
const t = setTimeout(() => {
|
|
1685
1901
|
fitView({ padding: 0.1, includeHiddenNodes: false, minZoom: 0.05, maxZoom: 2, duration: 200 });
|
|
1686
1902
|
}, 200);
|
|
@@ -2768,7 +2984,13 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2768
2984
|
focusedStepIndex={focusedStepIndex}
|
|
2769
2985
|
hoveredTrailStep={hoveredTrailStep}
|
|
2770
2986
|
onToggleCollapsed={toggleTrailCollapsed}
|
|
2771
|
-
onFocusFlow={
|
|
2987
|
+
onFocusFlow={(tl) => {
|
|
2988
|
+
// While a solo layout is on, following the selection keeps the canvas
|
|
2989
|
+
// and the highlighted flow in agreement instead of leaving the graph
|
|
2990
|
+
// pinned to a flow the user just navigated away from.
|
|
2991
|
+
if (layoutTrailId != null) setLayoutTrailId(tl.id);
|
|
2992
|
+
focusTrailEdges(tl);
|
|
2993
|
+
}}
|
|
2772
2994
|
onClearFocus={clearTrailFocus}
|
|
2773
2995
|
onFocusStep={focusTrailStep}
|
|
2774
2996
|
onHoverStep={(tl, i) => {
|
|
@@ -2785,6 +3007,8 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2785
3007
|
}}
|
|
2786
3008
|
onReorder={onReorderTrails}
|
|
2787
3009
|
proposedAliases={proposedAliases}
|
|
3010
|
+
layoutFocusedTrailId={layoutTrailId}
|
|
3011
|
+
onFocusLayout={toggleLayoutFocus}
|
|
2788
3012
|
/>
|
|
2789
3013
|
) : treeFilePaths.length > 0 ? (
|
|
2790
3014
|
<>
|
|
@@ -2851,6 +3075,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2851
3075
|
overlays shift with the canvas, not the labels. Collapses when the
|
|
2852
3076
|
file drawer fills the column (whole-file reading). */}
|
|
2853
3077
|
<div
|
|
3078
|
+
className={layoutFlipping ? LAYOUT_FLIP_CLASS : undefined}
|
|
2854
3079
|
style={{
|
|
2855
3080
|
position: 'relative',
|
|
2856
3081
|
flex: collapseCanvas ? 0 : 1,
|
|
@@ -2870,6 +3095,8 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2870
3095
|
overflow: 'hidden',
|
|
2871
3096
|
pointerEvents: 'none',
|
|
2872
3097
|
zIndex: 5,
|
|
3098
|
+
opacity: layoutFlipping ? 0 : 1,
|
|
3099
|
+
transition: `opacity ${LAYOUT_FLIP_EDGE_MS}ms ease`,
|
|
2873
3100
|
}}
|
|
2874
3101
|
>
|
|
2875
3102
|
{edgeLabels.map((lbl) => {
|
|
@@ -3044,6 +3271,11 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
3044
3271
|
nodeTypes={nodeTypes}
|
|
3045
3272
|
edgeTypes={edgeTypes}
|
|
3046
3273
|
className={GRAPH_CANVAS_CLASS}
|
|
3274
|
+
// The key above remounts ReactFlow when the node/edge SET changes (a
|
|
3275
|
+
// trail-focus layout, a filtered model). A remount otherwise resets the
|
|
3276
|
+
// viewport to identity — a visible jump to scale 1 before the post-flip
|
|
3277
|
+
// refit. Seed the new instance with the viewport the old one had.
|
|
3278
|
+
defaultViewport={viewport}
|
|
3047
3279
|
{...GRAPH_NAV_PROPS}
|
|
3048
3280
|
onNodeClick={onNodeClick}
|
|
3049
3281
|
onNodeDoubleClick={onNodeDoubleClick}
|
|
@@ -3062,6 +3294,78 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
3062
3294
|
>
|
|
3063
3295
|
<GraphChrome />
|
|
3064
3296
|
</ReactFlow>
|
|
3297
|
+
{!hideLayoutToggle && (
|
|
3298
|
+
<button
|
|
3299
|
+
type="button"
|
|
3300
|
+
onClick={() =>
|
|
3301
|
+
changeLayoutDirection(
|
|
3302
|
+
requestedDirection === 'horizontal' ? 'vertical' : 'horizontal',
|
|
3303
|
+
)
|
|
3304
|
+
}
|
|
3305
|
+
disabled={layoutFlipping}
|
|
3306
|
+
title={
|
|
3307
|
+
requestedDirection === 'horizontal'
|
|
3308
|
+
? 'Switch to a vertical layout (top-to-bottom)'
|
|
3309
|
+
: 'Switch to a horizontal layout (left-to-right)'
|
|
3310
|
+
}
|
|
3311
|
+
aria-label={
|
|
3312
|
+
requestedDirection === 'horizontal'
|
|
3313
|
+
? 'Switch to vertical layout'
|
|
3314
|
+
: 'Switch to horizontal layout'
|
|
3315
|
+
}
|
|
3316
|
+
style={{
|
|
3317
|
+
position: 'absolute',
|
|
3318
|
+
top: 10,
|
|
3319
|
+
right: 10,
|
|
3320
|
+
zIndex: 6,
|
|
3321
|
+
display: 'inline-flex',
|
|
3322
|
+
alignItems: 'center',
|
|
3323
|
+
gap: 5,
|
|
3324
|
+
padding: '4px 9px',
|
|
3325
|
+
border: `1px solid ${theme.colors.border}`,
|
|
3326
|
+
borderRadius: 6,
|
|
3327
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
3328
|
+
color: theme.colors.textSecondary,
|
|
3329
|
+
fontFamily: theme.fonts.monospace,
|
|
3330
|
+
fontSize: theme.fontSizes[0],
|
|
3331
|
+
cursor: layoutFlipping ? 'default' : 'pointer',
|
|
3332
|
+
opacity: layoutFlipping ? 0.55 : 1,
|
|
3333
|
+
}}
|
|
3334
|
+
>
|
|
3335
|
+
{requestedDirection === 'horizontal' ? (
|
|
3336
|
+
<ArrowDown size={13} aria-hidden="true" />
|
|
3337
|
+
) : (
|
|
3338
|
+
<ArrowRight size={13} aria-hidden="true" />
|
|
3339
|
+
)}
|
|
3340
|
+
{requestedDirection === 'horizontal' ? 'Vertical' : 'Horizontal'}
|
|
3341
|
+
</button>
|
|
3342
|
+
)}
|
|
3343
|
+
{unroutedIds.length > 0 && (
|
|
3344
|
+
<div
|
|
3345
|
+
role="status"
|
|
3346
|
+
title={`Edges with no routed path (not drawn):\n${unroutedIds.join('\n')}`}
|
|
3347
|
+
style={{
|
|
3348
|
+
position: 'absolute',
|
|
3349
|
+
top: 10,
|
|
3350
|
+
left: 10,
|
|
3351
|
+
zIndex: 6,
|
|
3352
|
+
display: 'inline-flex',
|
|
3353
|
+
alignItems: 'center',
|
|
3354
|
+
gap: 6,
|
|
3355
|
+
padding: '4px 10px',
|
|
3356
|
+
fontSize: theme.fontSizes[0],
|
|
3357
|
+
fontFamily: theme.fonts.body,
|
|
3358
|
+
color: theme.colors.text,
|
|
3359
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
3360
|
+
border: `1px solid ${theme.colors.warning ?? '#d4a017'}`,
|
|
3361
|
+
borderRadius: 6,
|
|
3362
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
3363
|
+
}}
|
|
3364
|
+
>
|
|
3365
|
+
<Unlink size={13} color={theme.colors.warning ?? '#d4a017'} aria-hidden />
|
|
3366
|
+
{unroutedIds.length} {unroutedIds.length === 1 ? 'edge' : 'edges'} not drawn
|
|
3367
|
+
</div>
|
|
3368
|
+
)}
|
|
3065
3369
|
{/* Graph / trail / step titles — non-interactive chips at the top
|
|
3066
3370
|
of the canvas. Graph-only embeds use these without opening the sidebar. */}
|
|
3067
3371
|
{(graphTitle || overlayTrailTitle || overlayTrailStep) && (
|
|
@@ -3322,8 +3626,10 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
3322
3626
|
</div>
|
|
3323
3627
|
</div>
|
|
3324
3628
|
)}
|
|
3325
|
-
{/* Startup cover — hides measurement, layout swap, and camera settle.
|
|
3326
|
-
|
|
3629
|
+
{/* Startup cover — hides measurement, layout swap, and camera settle.
|
|
3630
|
+
Kept away during a direction flip: the flip is meant to be watched,
|
|
3631
|
+
and the cover would swallow the node slide. */}
|
|
3632
|
+
<GraphLayoutCover revealed={layoutReady || layoutFlipping} />
|
|
3327
3633
|
{canvasOverlay}
|
|
3328
3634
|
{liveEvents ? <MaintainLivePanel {...liveEvents} /> : null}
|
|
3329
3635
|
</div>
|