@mcp-shark/mcp-shark 1.5.13 → 1.7.2

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 (158) hide show
  1. package/README.md +482 -56
  2. package/bin/mcp-shark.js +146 -52
  3. package/core/cli/AutoFixEngine.js +93 -0
  4. package/core/cli/ConfigScanner.js +193 -0
  5. package/core/cli/DataLoader.js +200 -0
  6. package/core/cli/DeclarativeRuleEngine.js +363 -0
  7. package/core/cli/DoctorCommand.js +218 -0
  8. package/core/cli/FixHandlers.js +222 -0
  9. package/core/cli/HtmlReportGenerator.js +203 -0
  10. package/core/cli/IdeConfigPaths.js +175 -0
  11. package/core/cli/ListCommand.js +255 -0
  12. package/core/cli/LockCommand.js +164 -0
  13. package/core/cli/LockDiffEngine.js +152 -0
  14. package/core/cli/RuleRegistryConfig.js +131 -0
  15. package/core/cli/ScanCommand.js +244 -0
  16. package/core/cli/ScanService.js +200 -0
  17. package/core/cli/SecretDetector.js +92 -0
  18. package/core/cli/SharkScoreCalculator.js +109 -0
  19. package/core/cli/ToolClassifications.js +51 -0
  20. package/core/cli/ToxicFlowAnalyzer.js +212 -0
  21. package/core/cli/UpdateCommand.js +188 -0
  22. package/core/cli/WalkthroughGenerator.js +195 -0
  23. package/core/cli/WatchCommand.js +129 -0
  24. package/core/cli/YamlRuleEngine.js +197 -0
  25. package/core/cli/data/rule-packs/aauth-visibility.json +117 -0
  26. package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
  27. package/core/cli/data/rule-packs/general-security.json +173 -0
  28. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  29. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  30. package/core/cli/data/rule-sources.json +5 -0
  31. package/core/cli/data/secret-patterns.json +18 -0
  32. package/core/cli/data/tool-classifications.json +111 -0
  33. package/core/cli/data/toxic-flow-rules.json +47 -0
  34. package/core/cli/index.js +23 -0
  35. package/core/cli/output/Banner.js +52 -0
  36. package/core/cli/output/Formatter.js +183 -0
  37. package/core/cli/output/JsonFormatter.js +106 -0
  38. package/core/cli/output/index.js +16 -0
  39. package/core/cli/secureRegistryFetch.js +157 -0
  40. package/core/cli/symbols.js +16 -0
  41. package/core/configs/environment.js +3 -1
  42. package/core/configs/index.js +3 -64
  43. package/core/container/DependencyContainer.js +4 -1
  44. package/core/mcp-server/index.js +4 -1
  45. package/core/mcp-server/server/external/all.js +10 -3
  46. package/core/mcp-server/server/external/config.js +62 -5
  47. package/core/models/RequestFilters.js +3 -0
  48. package/core/repositories/PacketRepository.js +16 -0
  49. package/core/services/AuditService.js +2 -0
  50. package/core/services/ConfigService.js +9 -1
  51. package/core/services/ConfigTransformService.js +34 -2
  52. package/core/services/RequestService.js +58 -5
  53. package/core/services/ServerManagementService.js +59 -4
  54. package/core/services/security/StaticRulesService.js +69 -13
  55. package/core/services/security/TrafficAnalysisService.js +19 -1
  56. package/core/services/security/TrafficToxicFlowService.js +154 -0
  57. package/core/services/security/aauthGraph.js +199 -0
  58. package/core/services/security/aauthParser.js +274 -0
  59. package/core/services/security/aauthSelfTest.js +346 -0
  60. package/core/services/security/index.js +2 -1
  61. package/core/services/security/rules/index.js +25 -59
  62. package/core/services/security/rules/scans/configPermissions.js +91 -0
  63. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  64. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  65. package/core/services/security/rules/scans/missingContainment.js +123 -0
  66. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  67. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  68. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  69. package/core/tui/App.js +144 -0
  70. package/core/tui/FindingsPanel.js +115 -0
  71. package/core/tui/FixPanel.js +132 -0
  72. package/core/tui/Header.js +51 -0
  73. package/core/tui/HelpBar.js +42 -0
  74. package/core/tui/ServersPanel.js +109 -0
  75. package/core/tui/ToxicFlowsPanel.js +100 -0
  76. package/core/tui/h.js +8 -0
  77. package/core/tui/index.js +11 -0
  78. package/core/tui/render.js +22 -0
  79. package/package.json +24 -16
  80. package/ui/dist/assets/index-D6zDrtMV.js +81 -0
  81. package/ui/dist/index.html +1 -1
  82. package/ui/server/controllers/AauthController.js +279 -0
  83. package/ui/server/controllers/RequestController.js +12 -1
  84. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  85. package/ui/server/routes/aauth.js +18 -0
  86. package/ui/server/routes/requests.js +8 -1
  87. package/ui/server/routes/security.js +5 -1
  88. package/ui/server/setup.js +224 -6
  89. package/ui/server/swagger/paths/components.js +55 -0
  90. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  91. package/ui/server/swagger/paths.js +2 -2
  92. package/ui/server/swagger/swagger.js +5 -2
  93. package/ui/server.js +1 -1
  94. package/ui/src/App.jsx +26 -52
  95. package/ui/src/PacketFilters.jsx +31 -1
  96. package/ui/src/PacketList.jsx +2 -2
  97. package/ui/src/Security.jsx +10 -0
  98. package/ui/src/TabNavigation.jsx +8 -0
  99. package/ui/src/components/AAuthBadge.jsx +92 -0
  100. package/ui/src/components/AauthExplorer/AauthExplorerGraph.jsx +231 -0
  101. package/ui/src/components/AauthExplorer/AauthExplorerView.jsx +387 -0
  102. package/ui/src/components/AauthExplorer/NodeDetailPanel.jsx +272 -0
  103. package/ui/src/components/App/ActionMenu.jsx +4 -31
  104. package/ui/src/components/App/ApiDocsButton.jsx +0 -1
  105. package/ui/src/components/App/ShutdownButton.jsx +0 -1
  106. package/ui/src/components/App/useAppState.js +19 -26
  107. package/ui/src/components/DetailsTab/AAuthIdentitySection.jsx +119 -0
  108. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +2 -0
  109. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +2 -0
  110. package/ui/src/components/DetectedPathsList.jsx +1 -5
  111. package/ui/src/components/FileInput.jsx +0 -1
  112. package/ui/src/components/PacketFilters/AAuthPostureFilter.jsx +81 -0
  113. package/ui/src/components/RequestRow/RequestRowMain.jsx +7 -1
  114. package/ui/src/components/Security/AAuthPosturePanel.jsx +360 -0
  115. package/ui/src/components/Security/ScannerContent.jsx +33 -1
  116. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  117. package/ui/src/components/Security/securityApi.js +15 -0
  118. package/ui/src/components/Security/useSecurity.js +60 -3
  119. package/ui/src/components/ServerControl.jsx +0 -1
  120. package/ui/src/components/TabNavigation/DesktopTabs.jsx +0 -11
  121. package/ui/src/components/TabNavigationIcons.jsx +5 -0
  122. package/ui/src/components/ViewModeTabs.jsx +0 -1
  123. package/ui/src/utils/animations.js +26 -9
  124. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  125. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  126. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  127. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  128. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  129. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  130. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  131. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  132. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  133. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  134. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  135. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  136. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  137. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  138. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  139. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  140. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  141. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  142. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
  143. package/ui/dist/assets/index-CiCSDYf-.js +0 -97
  144. package/ui/server/routes/help.js +0 -44
  145. package/ui/server/swagger/paths/help.js +0 -82
  146. package/ui/src/HelpGuide/HelpGuideContent.jsx +0 -118
  147. package/ui/src/HelpGuide/HelpGuideFooter.jsx +0 -59
  148. package/ui/src/HelpGuide/HelpGuideHeader.jsx +0 -57
  149. package/ui/src/HelpGuide.jsx +0 -78
  150. package/ui/src/IntroTour.jsx +0 -154
  151. package/ui/src/components/App/HelpButton.jsx +0 -90
  152. package/ui/src/components/TourOverlay.jsx +0 -117
  153. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +0 -120
  154. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +0 -71
  155. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +0 -54
  156. package/ui/src/components/TourTooltip/useTooltipPosition.js +0 -135
  157. package/ui/src/components/TourTooltip.jsx +0 -91
  158. package/ui/src/config/tourSteps.jsx +0 -140
@@ -1,44 +0,0 @@
1
- import { readHelpState, writeHelpState } from '#core/configs/index.js';
2
- import { StatusCodes } from '#core/constants/index.js';
3
-
4
- export function createHelpRoutes() {
5
- const router = {};
6
-
7
- router.getState = (_req, res) => {
8
- const state = readHelpState();
9
- res.json({
10
- dismissed: state.dismissed || false,
11
- tourCompleted: state.tourCompleted || false,
12
- });
13
- };
14
-
15
- router.dismiss = (req, res) => {
16
- const { tourCompleted } = req.body || {};
17
- const state = {
18
- dismissed: true,
19
- tourCompleted: tourCompleted || false,
20
- };
21
- const success = writeHelpState(state);
22
- if (success) {
23
- res.json({ success: true });
24
- } else {
25
- res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: 'Failed to save help state' });
26
- }
27
- };
28
-
29
- router.reset = (_req, res) => {
30
- const state = {
31
- dismissed: false,
32
- tourCompleted: false,
33
- dismissedAt: null,
34
- };
35
- const success = writeHelpState(state);
36
- if (success) {
37
- res.json({ success: true });
38
- } else {
39
- res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: 'Failed to reset help state' });
40
- }
41
- };
42
-
43
- return router;
44
- }
@@ -1,82 +0,0 @@
1
- /**
2
- * Help endpoints - Help and tour management
3
- */
4
-
5
- export const helpPaths = {
6
- '/api/help/state': {
7
- get: {
8
- tags: ['Help'],
9
- summary: 'Get help tour state',
10
- description: 'Get the current state of the help tour (dismissed/completed)',
11
- responses: {
12
- 200: {
13
- description: 'Tour state',
14
- content: {
15
- 'application/json': {
16
- schema: {
17
- type: 'object',
18
- properties: {
19
- dismissed: { type: 'boolean' },
20
- tourCompleted: { type: 'boolean' },
21
- },
22
- },
23
- },
24
- },
25
- },
26
- 500: {
27
- description: 'Internal server error',
28
- },
29
- },
30
- },
31
- },
32
- '/api/help/dismiss': {
33
- post: {
34
- tags: ['Help'],
35
- summary: 'Dismiss help tour',
36
- description: 'Mark the help tour as dismissed',
37
- responses: {
38
- 200: {
39
- description: 'Tour dismissed',
40
- content: {
41
- 'application/json': {
42
- schema: {
43
- type: 'object',
44
- properties: {
45
- success: { type: 'boolean' },
46
- },
47
- },
48
- },
49
- },
50
- },
51
- 500: {
52
- description: 'Internal server error',
53
- },
54
- },
55
- },
56
- },
57
- '/api/help/reset': {
58
- post: {
59
- tags: ['Help'],
60
- summary: 'Reset help tour',
61
- description: 'Reset the help tour state to show it again',
62
- responses: {
63
- 200: {
64
- description: 'Tour reset',
65
- content: {
66
- 'application/json': {
67
- schema: {
68
- type: 'object',
69
- properties: {
70
- success: { type: 'boolean' },
71
- },
72
- },
73
- },
74
- },
75
- },
76
- 500: {
77
- description: 'Internal server error',
78
- },
79
- },
80
- },
81
- },
82
- };
@@ -1,118 +0,0 @@
1
- import { colors, fonts } from '../theme';
2
-
3
- export default function HelpGuideContent() {
4
- return (
5
- <div
6
- style={{
7
- color: colors.textPrimary,
8
- lineHeight: '1.6',
9
- fontSize: '14px',
10
- fontFamily: fonts.body,
11
- }}
12
- >
13
- <section style={{ marginBottom: '24px' }}>
14
- <h3
15
- style={{
16
- color: colors.accentBlue,
17
- marginTop: 0,
18
- marginBottom: '12px',
19
- fontSize: '16px',
20
- fontFamily: fonts.body,
21
- }}
22
- >
23
- What is MCP Shark?
24
- </h3>
25
- <p style={{ margin: 0, color: colors.textSecondary }}>
26
- MCP Shark is a powerful tool for monitoring, debugging, and analyzing Model Context
27
- Protocol (MCP) traffic. It captures all communication between MCP clients and servers,
28
- allowing you to inspect requests, responses, and understand the flow of data.
29
- </p>
30
- </section>
31
-
32
- <section style={{ marginBottom: '24px' }}>
33
- <h3
34
- style={{
35
- color: colors.accentBlue,
36
- marginTop: 0,
37
- marginBottom: '12px',
38
- fontSize: '16px',
39
- fontFamily: fonts.body,
40
- }}
41
- >
42
- Getting Started
43
- </h3>
44
- <ol style={{ margin: 0, paddingLeft: '20px', color: colors.textSecondary }}>
45
- <li style={{ marginBottom: '8px' }}>
46
- Go to the <strong>Setup</strong> tab to configure your MCP servers
47
- </li>
48
- <li style={{ marginBottom: '8px' }}>
49
- Start the MCP Shark server to begin capturing traffic
50
- </li>
51
- <li style={{ marginBottom: '8px' }}>
52
- Use your MCP client (Cursor, Claude Desktop, etc.) as normal
53
- </li>
54
- <li style={{ marginBottom: '8px' }}>
55
- View captured traffic in the <strong>Traffic Capture</strong> tab
56
- </li>
57
- </ol>
58
- </section>
59
-
60
- <section style={{ marginBottom: '24px' }}>
61
- <h3
62
- style={{
63
- color: colors.accentBlue,
64
- marginTop: 0,
65
- marginBottom: '12px',
66
- fontSize: '16px',
67
- fontFamily: fonts.body,
68
- }}
69
- >
70
- Traffic Capture Features
71
- </h3>
72
- <ul style={{ margin: 0, paddingLeft: '20px', color: colors.textSecondary }}>
73
- <li style={{ marginBottom: '8px' }}>
74
- <strong>General List:</strong> View all requests in a flat chronological list
75
- </li>
76
- <li style={{ marginBottom: '8px' }}>
77
- <strong>MCP Protocol View:</strong> Organize traffic by MCP protocol categories
78
- </li>
79
- <li style={{ marginBottom: '8px' }}>
80
- <strong>Filters:</strong> Use the search bar and filters to find specific requests,
81
- sessions, or servers
82
- </li>
83
- <li style={{ marginBottom: '8px' }}>
84
- <strong>Details:</strong> Click any request to view full headers, body, and metadata
85
- </li>
86
- </ul>
87
- </section>
88
-
89
- <section style={{ marginBottom: '24px' }}>
90
- <h3
91
- style={{
92
- color: colors.accentBlue,
93
- marginTop: 0,
94
- marginBottom: '12px',
95
- fontSize: '16px',
96
- fontFamily: fonts.body,
97
- }}
98
- >
99
- Tips
100
- </h3>
101
- <ul style={{ margin: 0, paddingLeft: '20px', color: colors.textSecondary }}>
102
- <li style={{ marginBottom: '8px' }}>
103
- Use the search bar to find requests by method, URL, JSON-RPC method, or any text content
104
- </li>
105
- <li style={{ marginBottom: '8px' }}>
106
- Filter by session ID to track a specific conversation
107
- </li>
108
- <li style={{ marginBottom: '8px' }}>
109
- Filter by server name to see all traffic for a specific MCP server
110
- </li>
111
- <li style={{ marginBottom: '8px' }}>
112
- Click on request/response rows to see detailed packet information
113
- </li>
114
- </ul>
115
- </section>
116
- </div>
117
- );
118
- }
@@ -1,59 +0,0 @@
1
- import { colors, fonts } from '../theme';
2
-
3
- export default function HelpGuideFooter({ dontShowAgain, setDontShowAgain, onClose }) {
4
- return (
5
- <div
6
- style={{
7
- marginTop: '24px',
8
- paddingTop: '20px',
9
- borderTop: `1px solid ${colors.borderLight}`,
10
- display: 'flex',
11
- alignItems: 'center',
12
- justifyContent: 'space-between',
13
- }}
14
- >
15
- <label
16
- style={{
17
- display: 'flex',
18
- alignItems: 'center',
19
- gap: '8px',
20
- color: colors.textTertiary,
21
- cursor: 'pointer',
22
- fontSize: '13px',
23
- fontFamily: fonts.body,
24
- }}
25
- >
26
- <input
27
- type="checkbox"
28
- checked={dontShowAgain}
29
- onChange={(e) => setDontShowAgain(e.target.checked)}
30
- style={{ cursor: 'pointer' }}
31
- />
32
- Don't show this again
33
- </label>
34
- <button
35
- type="button"
36
- onClick={onClose}
37
- style={{
38
- background: colors.buttonPrimary,
39
- border: 'none',
40
- color: colors.textInverse,
41
- padding: '8px 20px',
42
- borderRadius: '4px',
43
- cursor: 'pointer',
44
- fontSize: '14px',
45
- fontWeight: '500',
46
- fontFamily: fonts.body,
47
- }}
48
- onMouseEnter={(e) => {
49
- e.currentTarget.style.background = colors.buttonPrimaryHover;
50
- }}
51
- onMouseLeave={(e) => {
52
- e.currentTarget.style.background = colors.buttonPrimary;
53
- }}
54
- >
55
- Got it!
56
- </button>
57
- </div>
58
- );
59
- }
@@ -1,57 +0,0 @@
1
- import { IconHelp, IconX } from '@tabler/icons-react';
2
- import { colors, fonts } from '../theme';
3
-
4
- export default function HelpGuideHeader({ onClose }) {
5
- return (
6
- <div
7
- style={{
8
- padding: '20px 24px',
9
- borderBottom: `1px solid ${colors.borderLight}`,
10
- display: 'flex',
11
- alignItems: 'center',
12
- justifyContent: 'space-between',
13
- }}
14
- >
15
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
16
- <div style={{ color: colors.accentBlue }}>
17
- <IconHelp size={24} stroke={1.5} />
18
- </div>
19
- <h2
20
- style={{
21
- margin: 0,
22
- color: colors.textPrimary,
23
- fontSize: '20px',
24
- fontWeight: '600',
25
- fontFamily: fonts.body,
26
- }}
27
- >
28
- Welcome to MCP Shark
29
- </h2>
30
- </div>
31
- <button
32
- type="button"
33
- onClick={onClose}
34
- style={{
35
- background: 'transparent',
36
- border: 'none',
37
- color: colors.textTertiary,
38
- cursor: 'pointer',
39
- padding: '4px',
40
- display: 'flex',
41
- alignItems: 'center',
42
- borderRadius: '8px',
43
- }}
44
- onMouseEnter={(e) => {
45
- e.currentTarget.style.background = colors.bgHover;
46
- e.currentTarget.style.color = colors.textPrimary;
47
- }}
48
- onMouseLeave={(e) => {
49
- e.currentTarget.style.background = 'transparent';
50
- e.currentTarget.style.color = colors.textTertiary;
51
- }}
52
- >
53
- <IconX size={20} stroke={1.5} />
54
- </button>
55
- </div>
56
- );
57
- }
@@ -1,78 +0,0 @@
1
- import { useState } from 'react';
2
- import HelpGuideContent from './HelpGuide/HelpGuideContent';
3
- import HelpGuideFooter from './HelpGuide/HelpGuideFooter';
4
- import HelpGuideHeader from './HelpGuide/HelpGuideHeader';
5
- import { colors } from './theme';
6
-
7
- function HelpGuide({ onClose }) {
8
- const [dontShowAgain, setDontShowAgain] = useState(false);
9
-
10
- const handleClose = async () => {
11
- if (dontShowAgain) {
12
- try {
13
- await fetch('/api/help/dismiss', { method: 'POST' });
14
- } catch (error) {
15
- console.error('Failed to save help state:', error);
16
- }
17
- }
18
- onClose();
19
- };
20
-
21
- return (
22
- <dialog
23
- open
24
- aria-modal="true"
25
- style={{
26
- position: 'fixed',
27
- top: 0,
28
- left: 0,
29
- right: 0,
30
- bottom: 0,
31
- background: 'rgba(0, 0, 0, 0.7)',
32
- zIndex: 1000,
33
- display: 'flex',
34
- alignItems: 'center',
35
- justifyContent: 'center',
36
- padding: '20px',
37
- border: 'none',
38
- margin: 0,
39
- width: '100%',
40
- height: '100%',
41
- }}
42
- onClick={handleClose}
43
- onKeyDown={(e) => {
44
- if (e.key === 'Escape') {
45
- handleClose();
46
- }
47
- }}
48
- >
49
- <div
50
- role="document"
51
- style={{
52
- background: colors.bgCard,
53
- border: `1px solid ${colors.borderLight}`,
54
- borderRadius: '12px',
55
- maxWidth: '700px',
56
- width: '100%',
57
- maxHeight: '90vh',
58
- overflow: 'auto',
59
- boxShadow: '0 8px 32px rgba(0, 0, 0, 0.5)',
60
- }}
61
- onClick={(e) => e.stopPropagation()}
62
- onKeyDown={(e) => e.stopPropagation()}
63
- >
64
- <HelpGuideHeader onClose={handleClose} />
65
- <div style={{ padding: '24px' }}>
66
- <HelpGuideContent />
67
- <HelpGuideFooter
68
- dontShowAgain={dontShowAgain}
69
- setDontShowAgain={setDontShowAgain}
70
- onClose={handleClose}
71
- />
72
- </div>
73
- </div>
74
- </dialog>
75
- );
76
- }
77
-
78
- export default HelpGuide;
@@ -1,154 +0,0 @@
1
- import { useEffect, useRef, useState } from 'react';
2
- import TourOverlay from './components/TourOverlay';
3
- import TourTooltip from './components/TourTooltip';
4
-
5
- function IntroTour({ steps, onComplete, onSkip, onStepChange }) {
6
- const [currentStep, setCurrentStep] = useState(0);
7
- const [highlightedElement, setHighlightedElement] = useState(null);
8
- const [elementRect, setElementRect] = useState(null);
9
- const overlayRef = useRef(null);
10
-
11
- useEffect(() => {
12
- if (!highlightedElement) {
13
- return;
14
- }
15
-
16
- const updatePosition = () => {
17
- if (highlightedElement) {
18
- const rect = highlightedElement.getBoundingClientRect();
19
- setElementRect(rect);
20
- }
21
- };
22
-
23
- updatePosition();
24
-
25
- const options = { passive: true, capture: true };
26
- window.addEventListener('scroll', updatePosition, options);
27
- window.addEventListener('resize', updatePosition, { passive: true });
28
-
29
- return () => {
30
- window.removeEventListener('scroll', updatePosition, options);
31
- window.removeEventListener('resize', updatePosition, { passive: true });
32
- };
33
- }, [highlightedElement]);
34
-
35
- useEffect(() => {
36
- if (steps.length === 0) {
37
- return;
38
- }
39
-
40
- const step = steps[currentStep];
41
- if (!step) {
42
- return;
43
- }
44
-
45
- if (onStepChange) {
46
- onStepChange(currentStep);
47
- }
48
-
49
- const timer = setTimeout(() => {
50
- const element = document.querySelector(step.target);
51
- if (element) {
52
- setHighlightedElement(element);
53
- element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
54
- setTimeout(() => {
55
- const rect = element.getBoundingClientRect();
56
- setElementRect(rect);
57
- }, 300);
58
- } else {
59
- setHighlightedElement(null);
60
- setElementRect(null);
61
- }
62
- }, 200);
63
-
64
- return () => clearTimeout(timer);
65
- }, [currentStep, steps, onStepChange]);
66
-
67
- const handleNext = () => {
68
- if (currentStep < steps.length - 1) {
69
- const nextStep = currentStep + 1;
70
- setCurrentStep(nextStep);
71
- if (onStepChange) {
72
- onStepChange(nextStep);
73
- }
74
- } else {
75
- handleComplete();
76
- }
77
- };
78
-
79
- const handlePrevious = () => {
80
- if (currentStep > 0) {
81
- const prevStep = currentStep - 1;
82
- setCurrentStep(prevStep);
83
- if (onStepChange) {
84
- onStepChange(prevStep);
85
- }
86
- }
87
- };
88
-
89
- const handleComplete = async () => {
90
- try {
91
- await fetch('/api/help/dismiss', {
92
- method: 'POST',
93
- headers: { 'Content-Type': 'application/json' },
94
- body: JSON.stringify({ tourCompleted: true }),
95
- });
96
- } catch (error) {
97
- console.error('Failed to save tour state:', error);
98
- }
99
- onComplete();
100
- };
101
-
102
- const handleSkip = () => {
103
- handleComplete();
104
- if (onSkip) {
105
- onSkip();
106
- }
107
- };
108
-
109
- if (steps.length === 0 || currentStep >= steps.length) {
110
- return null;
111
- }
112
-
113
- const step = steps[currentStep];
114
-
115
- return (
116
- <>
117
- <div
118
- ref={overlayRef}
119
- role="presentation"
120
- style={{
121
- position: 'fixed',
122
- top: 0,
123
- left: 0,
124
- right: 0,
125
- bottom: 0,
126
- zIndex: 9998,
127
- pointerEvents: 'auto',
128
- }}
129
- onClick={handleSkip}
130
- onKeyDown={(e) => {
131
- if (e.key === 'Escape') {
132
- handleSkip();
133
- }
134
- }}
135
- >
136
- <TourOverlay elementRect={elementRect} />
137
- </div>
138
-
139
- {elementRect && (
140
- <TourTooltip
141
- elementRect={elementRect}
142
- step={step}
143
- currentStep={currentStep}
144
- totalSteps={steps.length}
145
- onNext={handleNext}
146
- onPrevious={handlePrevious}
147
- onSkip={handleSkip}
148
- />
149
- )}
150
- </>
151
- );
152
- }
153
-
154
- export default IntroTour;
@@ -1,90 +0,0 @@
1
- import { colors, fonts } from '../../theme';
2
-
3
- const TourIcon = ({ size = 16, color = 'currentColor' }) => (
4
- <svg
5
- width={size}
6
- height={size}
7
- viewBox="0 0 24 24"
8
- fill="none"
9
- stroke={color}
10
- strokeWidth="2"
11
- strokeLinecap="round"
12
- strokeLinejoin="round"
13
- role="img"
14
- aria-label="Tour icon"
15
- >
16
- <title>Tour icon</title>
17
- <path d="M12 2L2 7l10 5 10-5-10-5z" />
18
- <path d="M2 17l10 5 10-5" />
19
- <path d="M2 12l10 5 10-5" />
20
- </svg>
21
- );
22
-
23
- /**
24
- * Help button component
25
- * Starts the application tour
26
- * @param {Object} props
27
- * @param {Function} props.onClick - Callback when button is clicked
28
- * @param {Object} props.style - Custom styles for the button
29
- * @param {Function} props.onMouseEnter - Optional mouse enter handler
30
- * @param {Function} props.onMouseLeave - Optional mouse leave handler
31
- */
32
- export default function HelpButton({ onClick, style, onMouseEnter, onMouseLeave }) {
33
- const defaultStyle = {
34
- position: 'fixed',
35
- bottom: '20px',
36
- right: '20px',
37
- background: colors.bgCard,
38
- border: `1px solid ${colors.borderLight}`,
39
- borderRadius: '50%',
40
- width: '48px',
41
- height: '48px',
42
- padding: 0,
43
- color: colors.textSecondary,
44
- cursor: 'pointer',
45
- fontFamily: fonts.body,
46
- boxShadow: `0 4px 12px ${colors.shadowMd}`,
47
- display: 'flex',
48
- alignItems: 'center',
49
- justifyContent: 'center',
50
- zIndex: 9999,
51
- transition: 'all 0.2s ease',
52
- ...style,
53
- };
54
-
55
- const handleMouseEnter = (e) => {
56
- e.currentTarget.style.background = colors.bgHover;
57
- e.currentTarget.style.color = colors.textPrimary;
58
- e.currentTarget.style.borderColor = colors.borderMedium;
59
- e.currentTarget.style.transform = 'scale(1.1)';
60
- e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
61
- if (onMouseEnter) {
62
- onMouseEnter(e);
63
- }
64
- };
65
-
66
- const handleMouseLeave = (e) => {
67
- e.currentTarget.style.background = colors.bgCard;
68
- e.currentTarget.style.color = colors.textSecondary;
69
- e.currentTarget.style.borderColor = colors.borderLight;
70
- e.currentTarget.style.transform = 'scale(1)';
71
- e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
72
- if (onMouseLeave) {
73
- onMouseLeave(e);
74
- }
75
- };
76
-
77
- return (
78
- <button
79
- type="button"
80
- onClick={onClick}
81
- data-tour="help-button"
82
- style={defaultStyle}
83
- onMouseEnter={handleMouseEnter}
84
- onMouseLeave={handleMouseLeave}
85
- title="Start tour"
86
- >
87
- <TourIcon size={20} />
88
- </button>
89
- );
90
- }