@anlyx/ui 0.1.1 → 0.1.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.
Files changed (126) hide show
  1. package/dist/components/AnalysisEvidenceList.d.ts +5 -0
  2. package/dist/components/AnalysisEvidenceList.js +61 -0
  3. package/dist/components/AnlyxAppShell.d.ts +1 -1
  4. package/dist/components/AnlyxAppShell.js +159 -15
  5. package/dist/components/ApiCallList.d.ts +3 -3
  6. package/dist/components/ApiCallList.js +12 -3
  7. package/dist/components/CanvasPlaceholder.d.ts +0 -1
  8. package/dist/components/CanvasPlaceholder.js +0 -1
  9. package/dist/components/CaptureStatusEmptyState.d.ts +0 -1
  10. package/dist/components/CaptureStatusEmptyState.js +5 -4
  11. package/dist/components/EndpointList.d.ts +0 -1
  12. package/dist/components/EndpointList.js +1 -2
  13. package/dist/components/EndpointMapCanvas.d.ts +5 -2
  14. package/dist/components/EndpointMapCanvas.js +93 -13
  15. package/dist/components/FlowLegend.d.ts +4 -2
  16. package/dist/components/FlowLegend.js +4 -2
  17. package/dist/components/FlowNodeCard.d.ts +1 -2
  18. package/dist/components/FlowNodeCard.js +25 -3
  19. package/dist/components/FlowStoryView.d.ts +22 -0
  20. package/dist/components/FlowStoryView.js +117 -0
  21. package/dist/components/InspectorPanel.d.ts +8 -3
  22. package/dist/components/InspectorPanel.js +36 -3
  23. package/dist/components/PageList.d.ts +0 -1
  24. package/dist/components/PageList.js +1 -2
  25. package/dist/components/PageStoryboardCard.d.ts +0 -1
  26. package/dist/components/PageStoryboardCard.js +4 -2
  27. package/dist/components/PageStoryboardView.d.ts +4 -3
  28. package/dist/components/PageStoryboardView.js +13 -3
  29. package/dist/components/ProcessFlowView.d.ts +21 -0
  30. package/dist/components/ProcessFlowView.js +16 -0
  31. package/dist/components/ProcessTimeline.d.ts +9 -0
  32. package/dist/components/ProcessTimeline.js +46 -0
  33. package/dist/components/ReplayControls.d.ts +6 -2
  34. package/dist/components/ReplayControls.js +31 -3
  35. package/dist/components/ScreenshotSegmentCard.d.ts +0 -1
  36. package/dist/components/ScreenshotSegmentCard.js +0 -1
  37. package/dist/components/Sidebar.d.ts +5 -4
  38. package/dist/components/Sidebar.js +19 -4
  39. package/dist/components/StatusBadge.d.ts +0 -1
  40. package/dist/components/StatusBadge.js +0 -1
  41. package/dist/flow/build-react-flow-model.d.ts +6 -2
  42. package/dist/flow/build-react-flow-model.js +15 -18
  43. package/dist/flow/layout/elk-layout.d.ts +7 -0
  44. package/dist/flow/layout/elk-layout.js +74 -0
  45. package/dist/index.d.ts +0 -1
  46. package/dist/index.js +0 -1
  47. package/dist/mock-data.d.ts +0 -1
  48. package/dist/mock-data.js +50 -5
  49. package/dist/overlay/AnlyxFlowEdge.d.ts +2 -0
  50. package/dist/overlay/AnlyxFlowEdge.js +15 -0
  51. package/dist/overlay/AnlyxFlowNode.d.ts +13 -0
  52. package/dist/overlay/AnlyxFlowNode.js +28 -0
  53. package/dist/overlay/FlowDrawer.d.ts +2 -0
  54. package/dist/overlay/FlowDrawer.js +59 -0
  55. package/dist/overlay/MainFlowCanvas.d.ts +20 -0
  56. package/dist/overlay/MainFlowCanvas.js +285 -0
  57. package/dist/overlay/RecentApiEventsTable.d.ts +5 -0
  58. package/dist/overlay/RecentApiEventsTable.js +19 -0
  59. package/dist/overlay/overlay-entry.d.ts +8 -0
  60. package/dist/overlay/overlay-entry.js +14 -0
  61. package/dist/overlay/overlay-ui.css +2 -0
  62. package/dist/overlay/overlay-ui.js +14 -0
  63. package/dist/overlay/types.d.ts +38 -0
  64. package/dist/overlay/types.js +1 -0
  65. package/dist/overlay/ui.d.ts +18 -0
  66. package/dist/overlay/ui.js +13 -0
  67. package/dist/readme-demo/ReadmeDemoApp.d.ts +4 -0
  68. package/dist/readme-demo/ReadmeDemoApp.js +126 -0
  69. package/dist/readme-demo/readme-demo-entry.d.ts +1 -0
  70. package/dist/readme-demo/readme-demo-entry.js +8 -0
  71. package/dist/replay/build-replay-steps.d.ts +0 -1
  72. package/dist/replay/build-replay-steps.js +0 -1
  73. package/dist/replay/use-replay-lite.d.ts +2 -2
  74. package/dist/replay/use-replay-lite.js +1 -1
  75. package/dist/styles.css +2018 -178
  76. package/dist/viewer/ViewerApp.d.ts +0 -1
  77. package/dist/viewer/ViewerApp.js +13 -7
  78. package/dist/viewer/viewer-entry.d.ts +1 -1
  79. package/dist/viewer/viewer-entry.js +1 -1
  80. package/package.json +7 -3
  81. package/dist/components/AnlyxAppShell.d.ts.map +0 -1
  82. package/dist/components/AnlyxAppShell.js.map +0 -1
  83. package/dist/components/ApiCallList.d.ts.map +0 -1
  84. package/dist/components/ApiCallList.js.map +0 -1
  85. package/dist/components/CanvasPlaceholder.d.ts.map +0 -1
  86. package/dist/components/CanvasPlaceholder.js.map +0 -1
  87. package/dist/components/CaptureStatusEmptyState.d.ts.map +0 -1
  88. package/dist/components/CaptureStatusEmptyState.js.map +0 -1
  89. package/dist/components/EndpointList.d.ts.map +0 -1
  90. package/dist/components/EndpointList.js.map +0 -1
  91. package/dist/components/EndpointMapCanvas.d.ts.map +0 -1
  92. package/dist/components/EndpointMapCanvas.js.map +0 -1
  93. package/dist/components/FlowLegend.d.ts.map +0 -1
  94. package/dist/components/FlowLegend.js.map +0 -1
  95. package/dist/components/FlowNodeCard.d.ts.map +0 -1
  96. package/dist/components/FlowNodeCard.js.map +0 -1
  97. package/dist/components/InspectorPanel.d.ts.map +0 -1
  98. package/dist/components/InspectorPanel.js.map +0 -1
  99. package/dist/components/PageList.d.ts.map +0 -1
  100. package/dist/components/PageList.js.map +0 -1
  101. package/dist/components/PageStoryboardCard.d.ts.map +0 -1
  102. package/dist/components/PageStoryboardCard.js.map +0 -1
  103. package/dist/components/PageStoryboardView.d.ts.map +0 -1
  104. package/dist/components/PageStoryboardView.js.map +0 -1
  105. package/dist/components/ReplayControls.d.ts.map +0 -1
  106. package/dist/components/ReplayControls.js.map +0 -1
  107. package/dist/components/ScreenshotSegmentCard.d.ts.map +0 -1
  108. package/dist/components/ScreenshotSegmentCard.js.map +0 -1
  109. package/dist/components/Sidebar.d.ts.map +0 -1
  110. package/dist/components/Sidebar.js.map +0 -1
  111. package/dist/components/StatusBadge.d.ts.map +0 -1
  112. package/dist/components/StatusBadge.js.map +0 -1
  113. package/dist/flow/build-react-flow-model.d.ts.map +0 -1
  114. package/dist/flow/build-react-flow-model.js.map +0 -1
  115. package/dist/index.d.ts.map +0 -1
  116. package/dist/index.js.map +0 -1
  117. package/dist/mock-data.d.ts.map +0 -1
  118. package/dist/mock-data.js.map +0 -1
  119. package/dist/replay/build-replay-steps.d.ts.map +0 -1
  120. package/dist/replay/build-replay-steps.js.map +0 -1
  121. package/dist/replay/use-replay-lite.d.ts.map +0 -1
  122. package/dist/replay/use-replay-lite.js.map +0 -1
  123. package/dist/viewer/ViewerApp.d.ts.map +0 -1
  124. package/dist/viewer/ViewerApp.js.map +0 -1
  125. package/dist/viewer/viewer-entry.d.ts.map +0 -1
  126. package/dist/viewer/viewer-entry.js.map +0 -1
@@ -0,0 +1,5 @@
1
+ import type { FlowNode } from "@anlyx/core";
2
+ export type AnalysisEvidenceListProps = {
3
+ node: FlowNode;
4
+ };
5
+ export declare function AnalysisEvidenceList({ node }: AnalysisEvidenceListProps): JSX.Element;
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CheckCircle2, CircleHelp, Info, TriangleAlert } from "lucide-react";
3
+ import { StatusBadge } from "./StatusBadge.js";
4
+ export function AnalysisEvidenceList({ node }) {
5
+ const evidence = getEvidence(node);
6
+ return (_jsxs("section", { className: "anlyx-inspector-group", "aria-label": "Analysis evidence", children: [_jsx("h3", { children: "Analysis evidence" }), _jsx("ul", { className: "anlyx-evidence-list", children: evidence.map((item, index) => {
7
+ const Icon = getEvidenceIcon(item.confidence ?? node.confidence ?? "unknown");
8
+ return (_jsxs("li", { children: [_jsx(Icon, { size: 14, strokeWidth: 2.5 }), _jsxs("span", { children: [_jsx("strong", { children: item.label }), item.detail ? _jsx("em", { children: item.detail }) : null] }), _jsx(StatusBadge, { tone: item.confidence ?? node.confidence ?? "unknown", children: item.confidence ?? node.confidence ?? "unknown" })] }, `${item.label}:${index}`));
9
+ }) })] }));
10
+ }
11
+ function getEvidence(node) {
12
+ if (node.evidence && node.evidence.length > 0) {
13
+ return node.evidence;
14
+ }
15
+ if (node.type === "unknown") {
16
+ return [
17
+ {
18
+ label: "Analysis stopped",
19
+ detail: "Anlyx could not resolve this code element from the scanned source.",
20
+ confidence: "unknown"
21
+ }
22
+ ];
23
+ }
24
+ if (node.type === "database") {
25
+ return [
26
+ {
27
+ label: "Database table inferred",
28
+ detail: "Derived from repository entity metadata or entity naming fallback.",
29
+ confidence: node.confidence ?? "unknown"
30
+ }
31
+ ];
32
+ }
33
+ if (node.type === "endpoint") {
34
+ return [
35
+ {
36
+ label: "Endpoint matched",
37
+ detail: "Derived from the backend adapter endpoint list.",
38
+ confidence: node.confidence ?? "unknown"
39
+ }
40
+ ];
41
+ }
42
+ return [
43
+ {
44
+ label: "Code node resolved",
45
+ detail: "Resolved from the scanned static flow graph.",
46
+ confidence: node.confidence ?? "unknown"
47
+ }
48
+ ];
49
+ }
50
+ function getEvidenceIcon(confidence) {
51
+ if (confidence === "high") {
52
+ return CheckCircle2;
53
+ }
54
+ if (confidence === "medium") {
55
+ return Info;
56
+ }
57
+ if (confidence === "low") {
58
+ return TriangleAlert;
59
+ }
60
+ return CircleHelp;
61
+ }
@@ -2,5 +2,5 @@ import type { ScanResult } from "@anlyx/core";
2
2
  export type AnlyxAppShellProps = {
3
3
  data: ScanResult;
4
4
  };
5
+ export type ViewMode = "flowStory" | "structure" | "frontend" | "process";
5
6
  export declare function AnlyxAppShell({ data }: AnlyxAppShellProps): JSX.Element;
6
- //# sourceMappingURL=AnlyxAppShell.d.ts.map
@@ -1,39 +1,102 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useState } from "react";
3
+ import { Group, Panel, Separator, usePanelRef } from "react-resizable-panels";
3
4
  import { EndpointMapCanvas } from "./EndpointMapCanvas.js";
5
+ import { FlowStoryView } from "./FlowStoryView.js";
4
6
  import { InspectorPanel } from "./InspectorPanel.js";
5
7
  import { PageStoryboardView } from "./PageStoryboardView.js";
6
- import { ReplayControls } from "./ReplayControls.js";
8
+ import { ProcessFlowView } from "./ProcessFlowView.js";
7
9
  import { Sidebar } from "./Sidebar.js";
8
10
  import { useReplayLite } from "../replay/use-replay-lite.js";
11
+ const STORAGE_KEYS = {
12
+ leftCollapsed: "anlyx:ui:v2:leftCollapsed",
13
+ panelLayout: "anlyx:ui:v2:panelLayout",
14
+ rightCollapsed: "anlyx:ui:v2:rightCollapsed",
15
+ selectedEndpointId: "anlyx:ui:selectedEndpointId",
16
+ selectedPageId: "anlyx:ui:selectedPageId"
17
+ };
18
+ const DEFAULT_PANEL_LAYOUT = {
19
+ left: 22,
20
+ center: 52,
21
+ right: 26
22
+ };
9
23
  export function AnlyxAppShell({ data }) {
10
- const [activeView, setActiveView] = useState("endpoint");
11
- const [selectedEndpointId, setSelectedEndpointId] = useState(data.endpoints[0]?.id);
12
- const [selectedPageId, setSelectedPageId] = useState(data.pages[0]?.id);
24
+ const [activeView, setActiveView] = useState("flowStory");
25
+ const [selectedEndpointId, setSelectedEndpointId] = usePersistentString(STORAGE_KEYS.selectedEndpointId, selectInitialEndpointId(data));
26
+ const [selectedPageId, setSelectedPageId] = usePersistentString(STORAGE_KEYS.selectedPageId, data.pages[0]?.id);
27
+ const leftPanelRef = usePanelRef();
28
+ const rightPanelRef = usePanelRef();
29
+ const [leftCollapsed, setLeftCollapsed] = usePersistentBoolean(STORAGE_KEYS.leftCollapsed, false);
30
+ const [rightCollapsed, setRightCollapsed] = usePersistentBoolean(STORAGE_KEYS.rightCollapsed, false);
31
+ const panelLayout = useMemo(() => readPanelLayout(), []);
32
+ const [replaySpeed, setReplaySpeed] = useState(800);
13
33
  const selectedEndpoint = data.endpoints.find((endpoint) => endpoint.id === selectedEndpointId) ?? data.endpoints[0];
14
34
  const selectedPage = data.pages.find((page) => page.id === selectedPageId) ?? data.pages[0];
15
35
  const selectedFlow = useMemo(() => data.flows.find((flow) => flow.endpointId === selectedEndpoint?.id), [data.flows, selectedEndpoint?.id]);
16
36
  const replayMainPath = useMemo(() => getReplayMainPath(selectedFlow, selectedEndpoint?.id), [selectedEndpoint?.id, selectedFlow]);
17
- const replay = useReplayLite({ mainPath: replayMainPath });
37
+ const replay = useReplayLite({ intervalMs: replaySpeed, mainPath: replayMainPath });
18
38
  const replayUnavailable = replayMainPath.length === 0;
19
39
  const [selectedNodeId, setSelectedNodeId] = useState(() => findDefaultNode(selectedFlow)?.id);
20
40
  const selectedNode = findFlowNode(selectedFlow, selectedNodeId) ?? findDefaultNode(selectedFlow);
21
41
  useEffect(() => {
22
- setSelectedEndpointId((current) => data.endpoints.some((endpoint) => endpoint.id === current) ? current : data.endpoints[0]?.id);
23
- }, [data.endpoints]);
42
+ setSelectedEndpointId((current) => data.endpoints.some((endpoint) => endpoint.id === current)
43
+ ? current
44
+ : selectInitialEndpointId(data));
45
+ }, [data, setSelectedEndpointId]);
24
46
  useEffect(() => {
25
47
  setSelectedPageId((current) => data.pages.some((page) => page.id === current) ? current : data.pages[0]?.id);
26
48
  }, [data.pages]);
27
49
  useEffect(() => {
28
50
  setSelectedNodeId(findDefaultNode(selectedFlow)?.id);
29
51
  }, [selectedFlow]);
30
- return (_jsxs("div", { className: "anlyx-shell", role: "application", "aria-label": "Anlyx application shell", children: [_jsx(Sidebar, { data: data, activeView: activeView, selectedEndpointId: selectedEndpoint?.id, selectedPageId: selectedPage?.id, onSelectView: setActiveView, onSelectEndpoint: (endpoint) => {
31
- setSelectedEndpointId(endpoint.id);
32
- setActiveView("endpoint");
33
- }, onSelectPage: (page) => {
34
- setSelectedPageId(page.id);
35
- setActiveView("pages");
36
- } }), _jsxs("div", { className: "anlyx-main", children: [activeView === "endpoint" ? (_jsx(EndpointMapCanvas, { endpoint: selectedEndpoint, flow: selectedFlow, replayState: replay.state, selectedNodeId: selectedNode?.id, onSelectNode: (node) => setSelectedNodeId(node.id) })) : null, activeView === "pages" ? _jsx(PageStoryboardView, { page: selectedPage }) : null, activeView === "replay" ? (_jsx(EndpointMapCanvas, { endpoint: selectedEndpoint, flow: selectedFlow, replayState: replay.state, selectedNodeId: selectedNode?.id, onSelectNode: (node) => setSelectedNodeId(node.id) })) : null, _jsx(ReplayControls, { disabled: replayUnavailable, loop: replay.loop, state: replay.state, unavailableReason: "Replay is unavailable because this endpoint has no main flow.", onPause: replay.pause, onPlay: replay.play, onRestart: replay.restart, onToggleLoop: replay.toggleLoop })] }), _jsx(InspectorPanel, { data: data, selectedFlow: selectedFlow, selectedNode: selectedNode }), _jsxs("div", { className: "anlyx-generated-at", children: ["Generated ", data.generatedAt] })] }));
52
+ useEffect(() => {
53
+ if (leftCollapsed) {
54
+ leftPanelRef.current?.collapse();
55
+ }
56
+ }, [leftCollapsed, leftPanelRef]);
57
+ useEffect(() => {
58
+ if (rightCollapsed) {
59
+ rightPanelRef.current?.collapse();
60
+ }
61
+ }, [rightCollapsed, rightPanelRef]);
62
+ const toggleLeftPanel = () => {
63
+ if (leftCollapsed) {
64
+ leftPanelRef.current?.expand();
65
+ setLeftCollapsed(false);
66
+ }
67
+ else {
68
+ leftPanelRef.current?.collapse();
69
+ setLeftCollapsed(true);
70
+ }
71
+ };
72
+ const toggleRightPanel = () => {
73
+ if (rightCollapsed) {
74
+ rightPanelRef.current?.expand();
75
+ setRightCollapsed(false);
76
+ }
77
+ else {
78
+ rightPanelRef.current?.collapse();
79
+ setRightCollapsed(true);
80
+ }
81
+ };
82
+ return (_jsxs("div", { className: "anlyx-shell", role: "application", "aria-label": "Anlyx application shell", children: [_jsxs(Group, { className: "anlyx-panel-group", defaultLayout: panelLayout, id: "anlyx-main-panels", orientation: "horizontal", onLayoutChanged: (layout) => writeLocalStorage(STORAGE_KEYS.panelLayout, JSON.stringify(layout)), children: [_jsx(Panel, { className: "anlyx-panel anlyx-panel--sidebar", collapsedSize: "52px", collapsible: true, defaultSize: "300px", id: "left", maxSize: "420px", minSize: "240px", panelRef: leftPanelRef, children: _jsx(Sidebar, { data: data, activeView: activeView, collapsed: leftCollapsed, selectedEndpointId: selectedEndpoint?.id, selectedPageId: selectedPage?.id, onSelectView: setActiveView, onToggleCollapsed: toggleLeftPanel, onSelectEndpoint: (endpoint) => {
83
+ setSelectedEndpointId(endpoint.id);
84
+ if (activeView !== "structure" && activeView !== "process") {
85
+ setActiveView("flowStory");
86
+ }
87
+ }, onSelectPage: (page) => {
88
+ setSelectedPageId(page.id);
89
+ const linkedEndpointId = page.apiCalls.find((apiCall) => apiCall.endpointId)?.endpointId;
90
+ if (linkedEndpointId) {
91
+ setSelectedEndpointId(linkedEndpointId);
92
+ }
93
+ if (activeView !== "frontend") {
94
+ setActiveView("flowStory");
95
+ }
96
+ } }) }), _jsx(Separator, { "aria-label": "Resize navigation panel", className: "anlyx-resize-handle", children: _jsx("span", { "aria-hidden": "true" }) }), _jsx(Panel, { className: "anlyx-panel anlyx-panel--main", id: "center", minSize: "420px", children: _jsxs("div", { className: activeView === "process" ? "anlyx-main anlyx-main--process" : "anlyx-main", "aria-live": "polite", children: [activeView === "flowStory" ? (_jsx(FlowStoryView, { data: data, endpoint: selectedEndpoint, flow: selectedFlow, page: selectedPage, replayDisabled: replayUnavailable, replayLoop: replay.loop, replaySpeed: replaySpeed, replayState: replay.state, replaySteps: replay.steps, selectedNodeId: selectedNode?.id, onPause: replay.pause, onPlay: replay.play, onRestart: replay.restart, onSelectNode: (node) => setSelectedNodeId(node.id), onSpeedChange: setReplaySpeed, onToggleLoop: replay.toggleLoop })) : null, activeView === "structure" ? (_jsx(EndpointMapCanvas, { eyebrow: "Backend API Structure", endpoint: selectedEndpoint, flow: selectedFlow, replayState: replay.state, selectedNodeId: selectedNode?.id, toolbar: _jsx(StructureToolbar, {}), onSelectNode: (node) => setSelectedNodeId(node.id) })) : null, activeView === "frontend" ? (_jsx(PageStoryboardView, { data: data, page: selectedPage, onViewProcessFlow: setActiveView })) : null, activeView === "process" ? (_jsx(ProcessFlowView, { endpoint: selectedEndpoint, flow: selectedFlow, replayDisabled: replayUnavailable, replayLoop: replay.loop, replaySpeed: replaySpeed, replayState: replay.state, replaySteps: replay.steps, selectedNodeId: selectedNode?.id, onPause: replay.pause, onPlay: replay.play, onRestart: replay.restart, onSelectNode: (node) => setSelectedNodeId(node.id), onSpeedChange: setReplaySpeed, onToggleLoop: replay.toggleLoop, onViewStructure: () => setActiveView("structure") })) : null] }) }), _jsx(Separator, { "aria-label": "Resize inspector panel", className: "anlyx-resize-handle", children: _jsx("span", { "aria-hidden": "true" }) }), _jsx(Panel, { className: "anlyx-panel anlyx-panel--inspector", collapsedSize: "52px", collapsible: true, defaultSize: "360px", id: "right", maxSize: "520px", minSize: "300px", panelRef: rightPanelRef, children: _jsx(InspectorPanel, { activeView: activeView, collapsed: rightCollapsed, data: data, replayState: replay.state, selectedFlow: selectedFlow, selectedNode: selectedNode, selectedPage: selectedPage, onToggleCollapsed: toggleRightPanel }) })] }), _jsxs("div", { className: "anlyx-generated-at", children: ["Generated ", data.generatedAt] })] }));
97
+ }
98
+ function StructureToolbar() {
99
+ return (_jsxs("div", { className: "anlyx-toolbar", "aria-label": "Structure view actions", children: [_jsx("button", { className: "anlyx-toolbar-button", type: "button", children: "Fit view" }), _jsxs("select", { "aria-label": "Zoom level", className: "anlyx-toolbar-select", defaultValue: "100", children: [_jsx("option", { value: "75", children: "75%" }), _jsx("option", { value: "100", children: "100%" }), _jsx("option", { value: "125", children: "125%" })] }), _jsx("button", { className: "anlyx-toolbar-button anlyx-toolbar-button--icon", type: "button", children: "More" })] }));
37
100
  }
38
101
  function getReplayMainPath(flow, endpointId) {
39
102
  if (!flow || flow.mainPath.length === 0) {
@@ -45,6 +108,20 @@ function getReplayMainPath(flow, endpointId) {
45
108
  }
46
109
  return flow.mainPath;
47
110
  }
111
+ function selectInitialEndpointId(data) {
112
+ const flowByEndpointId = new Map(data.flows.map((flow) => [flow.endpointId, flow]));
113
+ const endpointsByScore = [...data.endpoints].sort((left, right) => scoreEndpoint(right, flowByEndpointId) - scoreEndpoint(left, flowByEndpointId));
114
+ return endpointsByScore[0]?.id;
115
+ }
116
+ function scoreEndpoint(endpoint, flowByEndpointId) {
117
+ const flow = flowByEndpointId.get(endpoint.id);
118
+ const hasDatabase = flow?.nodes.some((node) => node.type === "database") ? 1 : 0;
119
+ const confidenceScore = endpoint.confidence === "high" ? 4 : endpoint.confidence === "medium" ? 2 : 0;
120
+ return ((flow?.mainPath.length ?? 0) * 8 +
121
+ (flow?.subFlows.length ?? 0) * 5 +
122
+ hasDatabase * 6 +
123
+ confidenceScore);
124
+ }
48
125
  function findDefaultNode(flow) {
49
126
  if (!flow) {
50
127
  return undefined;
@@ -58,4 +135,71 @@ function findFlowNode(flow, nodeId) {
58
135
  return (flow.nodes.find((node) => node.id === nodeId) ??
59
136
  flow.subFlows.flatMap((subFlow) => subFlow.nodes).find((node) => node.id === nodeId));
60
137
  }
61
- //# sourceMappingURL=AnlyxAppShell.js.map
138
+ function usePersistentBoolean(key, defaultValue) {
139
+ const [value, setValue] = useState(() => {
140
+ const storedValue = readLocalStorage(key);
141
+ if (storedValue === "true") {
142
+ return true;
143
+ }
144
+ if (storedValue === "false") {
145
+ return false;
146
+ }
147
+ return defaultValue;
148
+ });
149
+ useEffect(() => {
150
+ writeLocalStorage(key, String(value));
151
+ }, [key, value]);
152
+ return [value, setValue];
153
+ }
154
+ function usePersistentString(key, fallback) {
155
+ const [value, setValue] = useState(() => {
156
+ if (typeof window === "undefined") {
157
+ return fallback;
158
+ }
159
+ return window.localStorage.getItem(key) ?? fallback;
160
+ });
161
+ useEffect(() => {
162
+ if (typeof window === "undefined") {
163
+ return;
164
+ }
165
+ if (value) {
166
+ window.localStorage.setItem(key, value);
167
+ }
168
+ else {
169
+ window.localStorage.removeItem(key);
170
+ }
171
+ }, [key, value]);
172
+ return [value, setValue];
173
+ }
174
+ function readLocalStorage(key) {
175
+ if (typeof window === "undefined") {
176
+ return null;
177
+ }
178
+ return window.localStorage.getItem(key);
179
+ }
180
+ function writeLocalStorage(key, value) {
181
+ if (typeof window === "undefined") {
182
+ return;
183
+ }
184
+ window.localStorage.setItem(key, value);
185
+ }
186
+ function readPanelLayout() {
187
+ const storedValue = readLocalStorage(STORAGE_KEYS.panelLayout);
188
+ if (!storedValue) {
189
+ return DEFAULT_PANEL_LAYOUT;
190
+ }
191
+ try {
192
+ const parsedValue = JSON.parse(storedValue);
193
+ return {
194
+ left: sanitizePanelSize(parsedValue.left, DEFAULT_PANEL_LAYOUT.left),
195
+ center: sanitizePanelSize(parsedValue.center, DEFAULT_PANEL_LAYOUT.center),
196
+ right: sanitizePanelSize(parsedValue.right, DEFAULT_PANEL_LAYOUT.right)
197
+ };
198
+ }
199
+ catch {
200
+ return DEFAULT_PANEL_LAYOUT;
201
+ }
202
+ }
203
+ function sanitizePanelSize(value, fallback) {
204
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
205
+ }
@@ -1,6 +1,6 @@
1
- import type { ApiCall } from "@anlyx/core";
1
+ import type { ApiCall, Endpoint } from "@anlyx/core";
2
2
  export type ApiCallListProps = {
3
3
  apiCalls: ApiCall[];
4
+ endpoints?: Endpoint[];
4
5
  };
5
- export declare function ApiCallList({ apiCalls }: ApiCallListProps): JSX.Element;
6
- //# sourceMappingURL=ApiCallList.d.ts.map
6
+ export declare function ApiCallList({ apiCalls, endpoints }: ApiCallListProps): JSX.Element;
@@ -1,7 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { StatusBadge } from "./StatusBadge.js";
3
- export function ApiCallList({ apiCalls }) {
4
- return (_jsxs("section", { className: "anlyx-storyboard-panel", "aria-label": "API Calls", children: [_jsxs("div", { className: "anlyx-storyboard-section-heading", children: [_jsx("h2", { children: "API Calls" }), _jsx("span", { children: apiCalls.length })] }), apiCalls.length > 0 ? (_jsx("ul", { className: "anlyx-api-call-list", children: apiCalls.map((apiCall, index) => (_jsxs("li", { className: "anlyx-api-call", children: [_jsxs("div", { className: "anlyx-api-call__line", children: [_jsx(StatusBadge, { tone: apiCall.method, children: apiCall.method }), _jsx("span", { className: "anlyx-api-call__path", children: apiCall.path })] }), _jsxs("div", { className: "anlyx-api-call__meta", children: [_jsx(StatusBadge, { tone: statusTone(apiCall.status), children: apiCall.status === undefined ? "unknown" : String(apiCall.status) }), _jsx(StatusBadge, { tone: apiCall.endpointId ? "success" : "unknown", children: apiCall.endpointId ? "Linked endpoint" : "Unmatched" })] }), apiCall.endpointId ? (_jsx("div", { className: "anlyx-api-call__endpoint", children: apiCall.endpointId })) : null] }, `${apiCall.method}:${apiCall.path}:${index}`))) })) : (_jsx("p", { className: "anlyx-empty-inline", children: "No API calls captured yet." }))] }));
3
+ export function ApiCallList({ apiCalls, endpoints = [] }) {
4
+ const endpointById = new Map(endpoints.map((endpoint) => [endpoint.id, endpoint]));
5
+ return (_jsxs("section", { className: "anlyx-storyboard-panel", "aria-label": "API Calls", children: [_jsxs("div", { className: "anlyx-storyboard-section-heading", children: [_jsx("h2", { children: "API Calls" }), _jsx("span", { children: apiCalls.length })] }), apiCalls.length > 0 ? (_jsx("ul", { className: "anlyx-api-call-list", children: apiCalls.map((apiCall, index) => {
6
+ const endpoint = apiCall.endpointId ? endpointById.get(apiCall.endpointId) : undefined;
7
+ return (_jsxs("li", { className: "anlyx-api-call", children: [_jsxs("div", { className: "anlyx-api-call__line", children: [_jsx(StatusBadge, { tone: apiCall.method, children: apiCall.method }), _jsx("span", { className: "anlyx-api-call__path", children: apiCall.path })] }), _jsxs("div", { className: "anlyx-api-call__meta", children: [_jsx(StatusBadge, { tone: statusTone(apiCall.status), children: apiCall.status === undefined ? "unknown" : String(apiCall.status) }), _jsx(StatusBadge, { tone: apiCall.endpointId ? "success" : "unknown", children: apiCall.endpointId ? "Linked endpoint" : "Unmatched" })] }), endpoint ? (_jsxs("div", { className: "anlyx-api-call__endpoint", children: [_jsx("span", { children: "Matched endpoint" }), _jsxs("strong", { children: [endpoint.method, " ", endpoint.path] }), endpoint.controller || endpoint.handler ? (_jsx("em", { children: formatEndpointHandler(endpoint) })) : null] })) : apiCall.endpointId ? (_jsxs("div", { className: "anlyx-api-call__endpoint", children: [_jsx("span", { children: "Matched endpoint" }), _jsx("strong", { children: apiCall.endpointId })] })) : null] }, `${apiCall.method}:${apiCall.path}:${index}`));
8
+ }) })) : (_jsx("p", { className: "anlyx-empty-inline", children: "No API calls captured yet." }))] }));
9
+ }
10
+ function formatEndpointHandler(endpoint) {
11
+ if (endpoint.controller && endpoint.handler) {
12
+ return `${endpoint.controller}#${endpoint.handler}`;
13
+ }
14
+ return endpoint.controller ?? endpoint.handler ?? "Unknown handler";
5
15
  }
6
16
  function statusTone(status) {
7
17
  if (status === undefined) {
@@ -15,4 +25,3 @@ function statusTone(status) {
15
25
  }
16
26
  return "pending";
17
27
  }
18
- //# sourceMappingURL=ApiCallList.js.map
@@ -4,4 +4,3 @@ type CanvasPlaceholderProps = {
4
4
  };
5
5
  export declare function CanvasPlaceholder({ endpoint }: CanvasPlaceholderProps): JSX.Element;
6
6
  export {};
7
- //# sourceMappingURL=CanvasPlaceholder.d.ts.map
@@ -3,4 +3,3 @@ import { StatusBadge } from "./StatusBadge.js";
3
3
  export function CanvasPlaceholder({ endpoint }) {
4
4
  return (_jsxs("main", { className: "anlyx-workspace", "aria-label": "Workspace", children: [_jsxs("header", { className: "anlyx-workspace-header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Selected endpoint" }), _jsx("h1", { children: endpoint ? `${endpoint.method} ${endpoint.path}` : "No endpoint selected" })] }), endpoint ? (_jsx(StatusBadge, { tone: endpoint.confidence ?? "unknown", children: endpoint.confidence ?? "unknown" })) : null] }), _jsx("section", { className: "anlyx-canvas-placeholder", "aria-label": "Endpoint map canvas placeholder", children: _jsxs("div", { className: "anlyx-canvas-placeholder__content", children: [_jsx("p", { className: "anlyx-eyebrow", children: "Canvas" }), _jsx("h2", { children: "Endpoint Map will render here" })] }) })] }));
5
5
  }
6
- //# sourceMappingURL=CanvasPlaceholder.js.map
@@ -4,4 +4,3 @@ export type CaptureStatusEmptyStateProps = {
4
4
  reason?: string;
5
5
  };
6
6
  export declare function CaptureStatusEmptyState({ status, reason }: CaptureStatusEmptyStateProps): JSX.Element | null;
7
- //# sourceMappingURL=CaptureStatusEmptyState.d.ts.map
@@ -3,8 +3,9 @@ export function CaptureStatusEmptyState({ status, reason }) {
3
3
  if (status === "success") {
4
4
  return null;
5
5
  }
6
- const title = status === "failed" ? "Capture failed" : "Capture pending";
7
- const fallbackReason = status === "pending" ? "Waiting for capture input." : "Unknown";
8
- return (_jsxs("section", { className: `anlyx-capture-state anlyx-capture-state--${status}`, children: [_jsx("h2", { children: title }), _jsxs("p", { children: ["Reason: ", reason ?? fallbackReason] })] }));
6
+ const title = status === "failed" ? "Capture failed" : "Capture was skipped.";
7
+ const fallbackReason = status === "pending"
8
+ ? "Run `anlyx scan` without `--skip-capture` while your frontend server is running."
9
+ : "Unknown";
10
+ return (_jsxs("section", { className: `anlyx-capture-state anlyx-capture-state--${status}`, children: [_jsx("h2", { children: title }), _jsxs("p", { children: ["Reason: ", reason ?? fallbackReason] }), status === "pending" ? (_jsx("p", { children: "Dynamic routes may require `sampleParams` in `anlyx.config.ts`." })) : null] }));
9
11
  }
10
- //# sourceMappingURL=CaptureStatusEmptyState.js.map
@@ -6,4 +6,3 @@ type EndpointListProps = {
6
6
  };
7
7
  export declare function EndpointList({ endpoints, selectedEndpointId, onSelectEndpoint }: EndpointListProps): JSX.Element;
8
8
  export {};
9
- //# sourceMappingURL=EndpointList.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { StatusBadge } from "./StatusBadge.js";
3
3
  export function EndpointList({ endpoints, selectedEndpointId, onSelectEndpoint }) {
4
- return (_jsxs("section", { className: "anlyx-sidebar-section", "aria-labelledby": "anlyx-endpoints-heading", children: [_jsx("div", { className: "anlyx-section-heading", id: "anlyx-endpoints-heading", children: "Endpoints" }), _jsx("ul", { className: "anlyx-list", "aria-label": "Endpoint list", children: endpoints.map((endpoint) => (_jsx("li", { className: "anlyx-list-item", children: _jsxs("button", { className: "anlyx-endpoint-button", type: "button", "aria-label": `${endpoint.method} ${endpoint.path} ${formatEndpointHandler(endpoint)}`, "aria-current": endpoint.id === selectedEndpointId ? "true" : undefined, onClick: () => onSelectEndpoint(endpoint), children: [_jsxs("span", { className: "anlyx-list-item__line", children: [_jsx(StatusBadge, { tone: endpoint.method, children: endpoint.method }), _jsx("span", { className: "anlyx-list-item__primary", children: endpoint.path })] }), _jsx("span", { className: "anlyx-list-item__meta", children: formatEndpointHandler(endpoint) })] }) }, endpoint.id))) })] }));
4
+ return (_jsxs("section", { className: "anlyx-sidebar-section", "aria-labelledby": "anlyx-endpoints-heading", children: [_jsx("div", { className: "anlyx-section-heading", id: "anlyx-endpoints-heading", children: "Backend Endpoints" }), _jsx("ul", { className: "anlyx-list", "aria-label": "Endpoint list", children: endpoints.map((endpoint) => (_jsx("li", { className: "anlyx-list-item", children: _jsxs("button", { className: "anlyx-endpoint-button", type: "button", "aria-label": `${endpoint.method} ${endpoint.path} ${formatEndpointHandler(endpoint)}`, "aria-current": endpoint.id === selectedEndpointId ? "true" : undefined, onClick: () => onSelectEndpoint(endpoint), children: [_jsxs("span", { className: "anlyx-list-item__line", children: [_jsx(StatusBadge, { tone: endpoint.method, children: endpoint.method }), _jsx("span", { className: "anlyx-list-item__primary", children: endpoint.path })] }), _jsx("span", { className: "anlyx-list-item__meta", children: formatEndpointHandler(endpoint) })] }) }, endpoint.id))) })] }));
5
5
  }
6
6
  function formatEndpointHandler(endpoint) {
7
7
  if (endpoint.controller && endpoint.handler) {
@@ -9,4 +9,3 @@ function formatEndpointHandler(endpoint) {
9
9
  }
10
10
  return endpoint.handler ?? endpoint.controller ?? endpoint.id;
11
11
  }
12
- //# sourceMappingURL=EndpointList.js.map
@@ -4,8 +4,11 @@ export type EndpointMapCanvasProps = {
4
4
  endpoint: Endpoint | undefined;
5
5
  flow: EndpointFlow | undefined;
6
6
  selectedNodeId: string | undefined;
7
+ title?: string;
8
+ eyebrow?: string;
9
+ variant?: "structure" | "process";
7
10
  replayState?: ReplayLiteState;
11
+ toolbar?: JSX.Element;
8
12
  onSelectNode: (node: FlowNode) => void;
9
13
  };
10
- export declare function EndpointMapCanvas({ endpoint, flow, selectedNodeId, replayState, onSelectNode }: EndpointMapCanvasProps): JSX.Element;
11
- //# sourceMappingURL=EndpointMapCanvas.d.ts.map
14
+ export declare function EndpointMapCanvas({ endpoint, flow, selectedNodeId, title, eyebrow, variant, replayState, toolbar, onSelectNode }: EndpointMapCanvasProps): JSX.Element;
@@ -1,15 +1,42 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Background, BackgroundVariant, Controls, ReactFlow } from "@xyflow/react";
3
- import { useMemo } from "react";
2
+ import { Background, BackgroundVariant, BaseEdge, Controls, ReactFlow, getSmoothStepPath } from "@xyflow/react";
3
+ import { useEffect, useMemo, useState } from "react";
4
4
  import { FlowLegend } from "./FlowLegend.js";
5
5
  import { FlowNodeCard } from "./FlowNodeCard.js";
6
6
  import { StatusBadge } from "./StatusBadge.js";
7
7
  import { buildReactFlowModel } from "../flow/build-react-flow-model.js";
8
+ import { layoutWithElk } from "../flow/layout/elk-layout.js";
8
9
  const nodeTypes = {
9
10
  anlyxNode: FlowNodeCard
10
11
  };
11
- export function EndpointMapCanvas({ endpoint, flow, selectedNodeId, replayState, onSelectNode }) {
12
- const model = useMemo(() => (flow ? buildReactFlowModel(flow) : undefined), [flow]);
12
+ const edgeTypes = {
13
+ anlyxEdge: FlowEdgeLine
14
+ };
15
+ export function EndpointMapCanvas({ endpoint, flow, selectedNodeId, title, eyebrow = "Backend Endpoint Map", variant = "structure", replayState, toolbar, onSelectNode }) {
16
+ const fallbackModel = useMemo(() => (flow ? buildReactFlowModel(flow, { variant }) : undefined), [flow, variant]);
17
+ const [layoutedModel, setLayoutedModel] = useState(fallbackModel);
18
+ const model = layoutedModel ?? fallbackModel;
19
+ useEffect(() => {
20
+ let cancelled = false;
21
+ setLayoutedModel(fallbackModel);
22
+ if (!fallbackModel || isUnitTestRuntime()) {
23
+ return;
24
+ }
25
+ void layoutWithElk(fallbackModel, { variant })
26
+ .then((nextModel) => {
27
+ if (!cancelled) {
28
+ setLayoutedModel(isUsableLayout(nextModel, variant) ? nextModel : fallbackModel);
29
+ }
30
+ })
31
+ .catch(() => {
32
+ if (!cancelled) {
33
+ setLayoutedModel(fallbackModel);
34
+ }
35
+ });
36
+ return () => {
37
+ cancelled = true;
38
+ };
39
+ }, [fallbackModel, variant]);
13
40
  const nodes = useMemo(() => model?.nodes.map((node) => ({
14
41
  ...node,
15
42
  selected: node.id === selectedNodeId,
@@ -21,21 +48,75 @@ export function EndpointMapCanvas({ endpoint, flow, selectedNodeId, replayState,
21
48
  })) ?? [], [model, onSelectNode, replayState?.activeNodeId, selectedNodeId]);
22
49
  const edges = useMemo(() => model?.edges.map((edge) => {
23
50
  const isReplayActive = isReplayEdgeActive(edge, replayState?.activeEdge);
51
+ const replayClass = isReplayActive && replayState?.phase === "response"
52
+ ? "anlyx-flow-edge--replay-response"
53
+ : isReplayActive
54
+ ? "anlyx-flow-edge--replay-request"
55
+ : "";
24
56
  const edgeData = edge.data;
57
+ const nextData = {
58
+ edge: edgeData.edge,
59
+ flowRole: edgeData.flowRole,
60
+ ...(edgeData.confidence ? { confidence: edgeData.confidence } : {}),
61
+ isReplayActive,
62
+ ...(replayState?.phase ? { replayPhase: replayState.phase } : {})
63
+ };
25
64
  return {
26
65
  ...edge,
27
- className: [edge.className, isReplayActive ? "anlyx-flow-edge--replay-active" : ""]
66
+ className: [
67
+ edge.className,
68
+ isReplayActive ? "anlyx-flow-edge--replay-active" : "",
69
+ replayClass
70
+ ]
28
71
  .filter(Boolean)
29
72
  .join(" "),
30
- data: {
31
- edge: edgeData.edge,
32
- flowRole: edgeData.flowRole,
33
- ...(edgeData.confidence ? { confidence: edgeData.confidence } : {}),
34
- isReplayActive
35
- }
73
+ data: nextData
36
74
  };
37
75
  }) ?? [], [model, replayState?.activeEdge]);
38
- return (_jsxs("main", { className: "anlyx-workspace", children: [_jsxs("header", { className: "anlyx-workspace-header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: "Endpoint Map" }), _jsx("h1", { children: endpoint ? `${endpoint.method} ${endpoint.path}` : "No endpoint selected" })] }), endpoint ? (_jsx(StatusBadge, { tone: endpoint.confidence ?? "unknown", children: endpoint.confidence ?? "unknown" })) : null] }), _jsx("section", { className: "anlyx-endpoint-map", role: "region", "aria-label": "Endpoint Map", children: flow && model && model.nodes.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(FlowLegend, {}), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Endpoint map node list", children: nodes.map((node) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onSelectNode(node.data.node), children: ["Select node ", node.data.label] }) }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay node state", children: nodes.map((node) => (_jsx("li", { "data-replay-active": String(Boolean(node.data.isReplayActive)), "data-testid": `replay-node-${node.id}`, children: node.id }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay edge state", children: edges.map((edge) => (_jsxs("li", { "data-replay-active": String(Boolean(edge.data?.isReplayActive)), "data-testid": `replay-edge-${edge.source}-${edge.target}`, children: [edge.source, " to ", edge.target] }, edge.id))) }), _jsxs(ReactFlow, { className: "anlyx-react-flow", edges: edges, fitView: true, maxZoom: 1.25, minZoom: 0.45, nodes: nodes, nodesConnectable: false, nodesDraggable: false, nodeTypes: nodeTypes, onNodeClick: (_, node) => onSelectNode(node.data.node), panOnScroll: true, proOptions: { hideAttribution: true }, zoomOnDoubleClick: false, zoomOnScroll: false, children: [_jsx(Background, { color: "#dfe5ee", gap: 24, variant: BackgroundVariant.Dots }), _jsx(Controls, { showInteractive: false })] })] })) : (_jsx("div", { className: "anlyx-endpoint-map-empty", children: _jsx("p", { children: "No flow available for this endpoint yet." }) })) })] }));
76
+ return (_jsxs("main", { className: `anlyx-workspace anlyx-workspace--${variant}`, children: [_jsxs("header", { className: "anlyx-workspace-header", children: [_jsxs("div", { children: [_jsx("p", { className: "anlyx-eyebrow", children: eyebrow }), _jsx("h1", { children: title ?? (endpoint ? `${endpoint.method} ${endpoint.path}` : "No endpoint selected") })] }), _jsxs("div", { className: "anlyx-workspace-actions", children: [toolbar, endpoint ? (_jsx(StatusBadge, { tone: endpoint.confidence ?? "unknown", children: endpoint.confidence ?? "unknown" })) : null] })] }), _jsx("section", { className: `anlyx-endpoint-map anlyx-endpoint-map--${variant}`, role: "region", "aria-label": variant === "process" ? "Process Flow map" : "Endpoint Map", children: flow && model && model.nodes.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(FlowLegend, { variant: variant }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Endpoint map node list", children: nodes.map((node) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onSelectNode(node.data.node), children: ["Select node ", node.data.label] }) }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay node state", children: nodes.map((node) => (_jsx("li", { "data-replay-active": String(Boolean(node.data.isReplayActive)), "data-testid": `replay-node-${node.id}`, children: node.id }, node.id))) }), _jsx("ul", { className: "anlyx-sr-only", "aria-label": "Replay edge state", children: edges.map((edge) => (_jsxs("li", { "data-replay-active": String(Boolean(edge.data?.isReplayActive)), "data-testid": `replay-edge-${edge.source}-${edge.target}`, children: [edge.source, " to ", edge.target] }, edge.id))) }), _jsxs(ReactFlow, { className: "anlyx-react-flow", edges: edges, fitView: true, fitViewOptions: { padding: 0.18 }, maxZoom: 1.35, minZoom: 0.62, nodes: nodes, nodesConnectable: false, nodesDraggable: false, edgeTypes: edgeTypes, nodeTypes: nodeTypes, onNodeClick: (_, node) => onSelectNode(node.data.node), panOnScroll: true, proOptions: { hideAttribution: true }, zoomOnDoubleClick: false, zoomOnScroll: false, children: [_jsx(Background, { color: "#dfe5ee", gap: 24, variant: BackgroundVariant.Dots }), _jsx(Controls, { showInteractive: false })] })] })) : (_jsxs("div", { className: "anlyx-endpoint-map-empty", role: "status", "aria-label": "Flow unavailable", children: [_jsx("span", { children: "Flow unavailable" }), _jsx("h2", { children: "No scanned flow for this endpoint yet" }), _jsx("p", { children: "Anlyx can list this endpoint, but no Controller -> Service -> Repository path was found." }), _jsx("p", { children: "Check that the backend source directory is configured, then run `anlyx scan` again." })] })) })] }));
77
+ }
78
+ function isUnitTestRuntime() {
79
+ return typeof process !== "undefined" && process.env.NODE_ENV === "test";
80
+ }
81
+ function isUsableLayout(model, variant) {
82
+ const positions = model.nodes.map((node) => node.position);
83
+ const minY = Math.min(...positions.map((position) => position.y));
84
+ const maxY = Math.max(...positions.map((position) => position.y));
85
+ const minX = Math.min(...positions.map((position) => position.x));
86
+ const maxX = Math.max(...positions.map((position) => position.x));
87
+ const height = maxY - minY;
88
+ const width = maxX - minX;
89
+ if (!Number.isFinite(height) || !Number.isFinite(width)) {
90
+ return false;
91
+ }
92
+ const maxHeight = variant === "process" ? 360 : 300;
93
+ return height <= maxHeight && width > 0;
94
+ }
95
+ function FlowEdgeLine({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, data }) {
96
+ const [edgePath] = getSmoothStepPath({
97
+ sourceX,
98
+ sourceY,
99
+ sourcePosition,
100
+ targetX,
101
+ targetY,
102
+ targetPosition
103
+ });
104
+ const isReplayActive = Boolean(data?.isReplayActive);
105
+ const flowRole = data?.flowRole ?? "secondary";
106
+ const particleRole = data?.replayPhase === "response" ? "response" : flowRole;
107
+ const className = [
108
+ "anlyx-flow-edge",
109
+ `anlyx-flow-edge--${flowRole}`,
110
+ isReplayActive ? "anlyx-flow-edge--replay-active" : "",
111
+ isReplayActive && data?.replayPhase === "response"
112
+ ? "anlyx-flow-edge--replay-response"
113
+ : isReplayActive
114
+ ? "anlyx-flow-edge--replay-request"
115
+ : ""
116
+ ]
117
+ .filter(Boolean)
118
+ .join(" ");
119
+ return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { className: className, path: edgePath, style: style }), isReplayActive ? (_jsx("circle", { className: `anlyx-flow-particle anlyx-flow-particle--${particleRole}`, r: flowRole === "sub" ? 4 : 5, children: _jsx("animateMotion", { dur: "1.25s", repeatCount: "indefinite", path: edgePath }) })) : null] }));
39
120
  }
40
121
  function isReplayEdgeActive(edge, activeEdge) {
41
122
  if (!activeEdge) {
@@ -44,4 +125,3 @@ function isReplayEdgeActive(edge, activeEdge) {
44
125
  return ((edge.source === activeEdge.from && edge.target === activeEdge.to) ||
45
126
  (edge.source === activeEdge.to && edge.target === activeEdge.from));
46
127
  }
47
- //# sourceMappingURL=EndpointMapCanvas.js.map
@@ -1,2 +1,4 @@
1
- export declare function FlowLegend(): JSX.Element;
2
- //# sourceMappingURL=FlowLegend.d.ts.map
1
+ export type FlowLegendProps = {
2
+ variant?: "structure" | "process";
3
+ };
4
+ export declare function FlowLegend({ variant }: FlowLegendProps): JSX.Element;
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- export function FlowLegend() {
2
+ export function FlowLegend({ variant = "structure" }) {
3
+ if (variant === "process") {
4
+ return (_jsxs("div", { className: "anlyx-flow-legend", "aria-label": "Flow legend", children: [_jsxs("span", { children: [_jsx("i", { className: "anlyx-flow-legend__mark anlyx-flow-legend__mark--main" }), "Request"] }), _jsxs("span", { children: [_jsx("i", { className: "anlyx-flow-legend__mark anlyx-flow-legend__mark--response" }), "Response"] }), _jsxs("span", { children: [_jsx("i", { className: "anlyx-flow-legend__mark anlyx-flow-legend__mark--sub" }), "Branch calls"] })] }));
5
+ }
3
6
  return (_jsxs("div", { className: "anlyx-flow-legend", "aria-label": "Flow legend", children: [_jsxs("span", { children: [_jsx("i", { className: "anlyx-flow-legend__mark anlyx-flow-legend__mark--main" }), "Main Flow"] }), _jsxs("span", { children: [_jsx("i", { className: "anlyx-flow-legend__mark anlyx-flow-legend__mark--sub" }), "Sub Flow"] }), _jsxs("span", { children: [_jsx("i", { className: "anlyx-flow-legend__mark anlyx-flow-legend__mark--unknown" }), "Unknown"] })] }));
4
7
  }
5
- //# sourceMappingURL=FlowLegend.js.map
@@ -1,4 +1,3 @@
1
- import type { NodeProps } from "@xyflow/react";
1
+ import { type NodeProps } from "@xyflow/react";
2
2
  import type { AnlyxReactFlowNode } from "../flow/build-react-flow-model.js";
3
3
  export declare function FlowNodeCard({ data, selected }: NodeProps<AnlyxReactFlowNode>): JSX.Element;
4
- //# sourceMappingURL=FlowNodeCard.d.ts.map
@@ -1,13 +1,35 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Handle, Position } from "@xyflow/react";
3
+ import { Box, Braces, Code2, Database, GitBranch, Globe2, Layers3 } from "lucide-react";
4
+ import { motion } from "motion/react";
2
5
  import { StatusBadge } from "./StatusBadge.js";
3
6
  export function FlowNodeCard({ data, selected }) {
4
7
  const confidence = data.confidence ?? "unknown";
5
- return (_jsxs("button", { className: [
8
+ const Icon = getTypeIcon(data.type, data.flowRole);
9
+ return (_jsxs(motion.button, { animate: data.isReplayActive ? { scale: [1, 1.018, 1] } : { scale: 1 }, className: [
6
10
  "anlyx-flow-node",
7
11
  `anlyx-flow-node--${data.type}`,
8
12
  `anlyx-flow-node--${data.flowRole}`,
9
13
  data.isReplayActive ? "anlyx-flow-node--replay-active" : "",
10
14
  selected ? "anlyx-flow-node--selected" : ""
11
- ].join(" "), onClick: () => data.onSelectNode?.(data.node), type: "button", "aria-label": `Select node ${data.label}`, "data-replay-active": String(Boolean(data.isReplayActive)), "data-testid": `flow-node-${data.node.id}`, children: [_jsx("span", { className: "anlyx-flow-node__type", children: data.type }), _jsx("span", { className: "anlyx-flow-node__label", children: data.label }), _jsx(StatusBadge, { tone: confidence, label: "confidence", children: confidence })] }));
15
+ ].join(" "), onClick: () => data.onSelectNode?.(data.node), type: "button", "aria-label": `Select node ${data.label}`, "data-replay-active": String(Boolean(data.isReplayActive)), "data-testid": `flow-node-${data.node.id}`, transition: { duration: 1.35, repeat: data.isReplayActive ? Infinity : 0 }, children: [_jsx(Handle, { className: "anlyx-flow-handle", position: Position.Left, type: "target" }), data.isReplayActive ? _jsx("span", { className: "anlyx-flow-node__pulse", "aria-hidden": "true" }) : null, _jsxs("span", { className: "anlyx-flow-node__header", children: [_jsx("span", { className: "anlyx-flow-node__icon", "aria-hidden": "true", children: _jsx(Icon, { size: 14, strokeWidth: 2.4 }) }), _jsx("span", { className: "anlyx-flow-node__type", children: data.type })] }), _jsx("span", { className: "anlyx-flow-node__label", children: data.label }), _jsx(StatusBadge, { tone: confidence, label: "confidence", children: confidence }), _jsx(Handle, { className: "anlyx-flow-handle", position: Position.Right, type: "source" })] }));
16
+ }
17
+ function getTypeIcon(type, flowRole) {
18
+ if (flowRole === "sub") {
19
+ return GitBranch;
20
+ }
21
+ switch (type) {
22
+ case "endpoint":
23
+ return Globe2;
24
+ case "controller":
25
+ return Code2;
26
+ case "service":
27
+ return Layers3;
28
+ case "repository":
29
+ return Braces;
30
+ case "database":
31
+ return Database;
32
+ default:
33
+ return Box;
34
+ }
12
35
  }
13
- //# sourceMappingURL=FlowNodeCard.js.map