@eventcatalog/core 3.12.7 → 3.12.9-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-RZOSO7BH.js → chunk-2UUS3AQ2.js} +1 -1
  6. package/dist/{chunk-UFF5Q7GJ.js → chunk-EDX2LGRV.js} +1 -1
  7. package/dist/{chunk-2EFYBMLH.js → chunk-RJOB6XEC.js} +1 -1
  8. package/dist/{chunk-YO6IQXB3.js → chunk-V7YMKA4P.js} +1 -1
  9. package/dist/{chunk-PLQAZDJI.js → chunk-WTCJKTEF.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/src/components/MDX/Design/Design.astro +2 -2
  19. package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +2 -2
  20. package/eventcatalog/src/components/MDX/Flow/Flow.astro +2 -2
  21. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +104 -0
  22. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -2
  23. package/eventcatalog/src/components/MDX/NodeGraph/README.md +85 -0
  24. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -2
  25. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +66 -17
  26. package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +14 -5
  27. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +1 -1
  28. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +133 -18
  29. package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +36 -14
  30. package/eventcatalog/src/utils/node-graphs/utils/utils.ts +115 -4
  31. package/package.json +4 -4
  32. package/eventcatalog/src/components/MDX/NodeGraph/DownloadButton.tsx +0 -62
  33. package/eventcatalog/src/components/MDX/NodeGraph/Edges/AnimatedMessageEdge.tsx +0 -110
  34. package/eventcatalog/src/components/MDX/NodeGraph/Edges/FlowEdge.tsx +0 -96
  35. package/eventcatalog/src/components/MDX/NodeGraph/Edges/MultilineEdgeLabel.tsx +0 -52
  36. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeContent.tsx +0 -294
  37. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeNodeActions.tsx +0 -92
  38. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModePlaceholder.tsx +0 -26
  39. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/utils.ts +0 -163
  40. package/eventcatalog/src/components/MDX/NodeGraph/FocusModeModal.tsx +0 -99
  41. package/eventcatalog/src/components/MDX/NodeGraph/MermaidView.tsx +0 -242
  42. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +0 -1181
  43. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Actor.tsx +0 -46
  44. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Channel.tsx +0 -55
  45. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Command.tsx +0 -27
  46. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Custom.tsx +0 -159
  47. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Data.tsx +0 -63
  48. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/DataProduct.tsx +0 -132
  49. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Domain.tsx +0 -155
  50. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx +0 -154
  51. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Event.tsx +0 -29
  52. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem.tsx +0 -79
  53. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem2.tsx +0 -24
  54. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Flow.tsx +0 -107
  55. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/MessageContextMenu.tsx +0 -63
  56. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Query.tsx +0 -28
  57. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Service.tsx +0 -127
  58. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Step.tsx +0 -64
  59. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/User.tsx +0 -76
  60. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/View.tsx +0 -24
  61. package/eventcatalog/src/components/MDX/NodeGraph/StepWalkthrough.tsx +0 -296
  62. package/eventcatalog/src/components/MDX/NodeGraph/StudioModal.tsx +0 -129
  63. package/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx +0 -258
  64. package/eventcatalog/src/components/MDX/NodeGraph/VisualizerDropdownContent.tsx +0 -313
@@ -1,258 +0,0 @@
1
- import { useState, useCallback, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
2
- import type { Node } from '@xyflow/react';
3
-
4
- // Define interfaces for different node data structures
5
- interface MessageData {
6
- name: string;
7
- version?: string;
8
- }
9
-
10
- interface ServiceData {
11
- name: string;
12
- version?: string;
13
- }
14
-
15
- interface DomainData {
16
- name: string;
17
- version?: string;
18
- }
19
-
20
- interface EntityData {
21
- name: string;
22
- version?: string;
23
- }
24
-
25
- interface NodeDataContent extends Record<string, unknown> {
26
- message?: {
27
- data: MessageData;
28
- };
29
- service?: {
30
- data: ServiceData;
31
- };
32
- domain?: {
33
- data: DomainData;
34
- };
35
- entity?: {
36
- data: EntityData;
37
- };
38
- name?: string;
39
- version?: string;
40
- }
41
-
42
- // Extend the Node type with our custom data structure
43
- type CustomNode = Node<NodeDataContent>;
44
-
45
- interface VisualiserSearchProps {
46
- nodes: CustomNode[];
47
- onNodeSelect: (node: CustomNode) => void;
48
- onClear: () => void;
49
- onPaneClick?: () => void;
50
- }
51
-
52
- export interface VisualiserSearchRef {
53
- hideSuggestions: () => void;
54
- }
55
-
56
- const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
57
- ({ nodes, onNodeSelect, onClear, onPaneClick }, ref) => {
58
- const [searchQuery, setSearchQuery] = useState('');
59
- const [filteredSuggestions, setFilteredSuggestions] = useState<CustomNode[]>([]);
60
- const [showSuggestions, setShowSuggestions] = useState(false);
61
- const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
62
- const searchInputRef = useRef<HTMLInputElement>(null);
63
- const containerRef = useRef<HTMLDivElement>(null);
64
-
65
- const hideSuggestions = useCallback(() => {
66
- setShowSuggestions(false);
67
- setSelectedSuggestionIndex(-1);
68
- }, []);
69
-
70
- useImperativeHandle(
71
- ref,
72
- () => ({
73
- hideSuggestions,
74
- }),
75
- [hideSuggestions]
76
- );
77
-
78
- const getNodeDisplayName = useCallback((node: CustomNode) => {
79
- const name =
80
- node.data?.message?.data?.name ||
81
- node.data?.service?.data?.name ||
82
- node.data?.domain?.data?.name ||
83
- node.data?.entity?.data?.name ||
84
- node.data?.name ||
85
- node.id;
86
- const version =
87
- node.data?.message?.data?.version ||
88
- node.data?.service?.data?.version ||
89
- node.data?.domain?.data?.version ||
90
- node.data?.entity?.data?.version ||
91
- node.data?.version;
92
- return version ? `${name} (v${version})` : name;
93
- }, []);
94
-
95
- const getNodeTypeColorClass = useCallback((nodeType: string) => {
96
- const colorClasses: { [key: string]: string } = {
97
- events: 'bg-orange-600 text-white',
98
- services: 'bg-pink-600 text-white',
99
- flows: 'bg-teal-600 text-white',
100
- commands: 'bg-blue-600 text-white',
101
- queries: 'bg-green-600 text-white',
102
- channels: 'bg-gray-600 text-white',
103
- domains: 'bg-yellow-500 text-white',
104
- externalSystem: 'bg-pink-600 text-white',
105
- actor: 'bg-yellow-500 text-white',
106
- step: 'bg-gray-700 text-white',
107
- user: 'bg-yellow-500 text-white',
108
- custom: 'bg-gray-500 text-white',
109
- };
110
- return colorClasses[nodeType] || 'bg-gray-100 text-gray-700';
111
- }, []);
112
-
113
- const handleSearchChange = useCallback(
114
- (event: React.ChangeEvent<HTMLInputElement>) => {
115
- const query = event.target.value;
116
- setSearchQuery(query);
117
-
118
- if (query.length > 0) {
119
- const filtered = nodes.filter((node) => {
120
- const nodeName = getNodeDisplayName(node);
121
- return nodeName.toLowerCase().includes(query.toLowerCase());
122
- });
123
- setFilteredSuggestions(filtered);
124
- setShowSuggestions(true);
125
- setSelectedSuggestionIndex(-1);
126
- } else {
127
- setFilteredSuggestions(nodes);
128
- setShowSuggestions(true);
129
- setSelectedSuggestionIndex(-1);
130
- }
131
- },
132
- [nodes, getNodeDisplayName]
133
- );
134
-
135
- const handleSearchFocus = useCallback(() => {
136
- if (searchQuery.length === 0) {
137
- setFilteredSuggestions(nodes);
138
- }
139
- setShowSuggestions(true);
140
- setSelectedSuggestionIndex(-1);
141
- }, [nodes, searchQuery]);
142
-
143
- const handleSuggestionClick = useCallback(
144
- (node: CustomNode) => {
145
- setSearchQuery(getNodeDisplayName(node));
146
- setShowSuggestions(false);
147
- onNodeSelect(node);
148
- },
149
- [onNodeSelect, getNodeDisplayName]
150
- );
151
-
152
- const handleSearchKeyDown = useCallback(
153
- (event: React.KeyboardEvent<HTMLInputElement>) => {
154
- if (!showSuggestions || filteredSuggestions.length === 0) return;
155
-
156
- switch (event.key) {
157
- case 'ArrowDown':
158
- event.preventDefault();
159
- setSelectedSuggestionIndex((prev) => (prev < filteredSuggestions.length - 1 ? prev + 1 : 0));
160
- break;
161
- case 'ArrowUp':
162
- event.preventDefault();
163
- setSelectedSuggestionIndex((prev) => (prev > 0 ? prev - 1 : filteredSuggestions.length - 1));
164
- break;
165
- case 'Enter':
166
- event.preventDefault();
167
- if (selectedSuggestionIndex >= 0) {
168
- handleSuggestionClick(filteredSuggestions[selectedSuggestionIndex]);
169
- }
170
- break;
171
- case 'Escape':
172
- setShowSuggestions(false);
173
- setSelectedSuggestionIndex(-1);
174
- break;
175
- }
176
- },
177
- [showSuggestions, filteredSuggestions, selectedSuggestionIndex, handleSuggestionClick]
178
- );
179
-
180
- const clearSearch = useCallback(() => {
181
- setSearchQuery('');
182
- setShowSuggestions(false);
183
- setFilteredSuggestions([]);
184
- setSelectedSuggestionIndex(-1);
185
- onClear();
186
- if (searchInputRef.current) {
187
- searchInputRef.current.focus();
188
- }
189
- }, [onClear]);
190
-
191
- // Close suggestions when clicking outside
192
- useEffect(() => {
193
- const handleClickOutside = (event: MouseEvent) => {
194
- if (containerRef.current && !containerRef.current.contains(event.target as any)) {
195
- setShowSuggestions(false);
196
- setSelectedSuggestionIndex(-1);
197
- }
198
- };
199
-
200
- document.addEventListener('mousedown', handleClickOutside);
201
- return () => {
202
- document.removeEventListener('mousedown', handleClickOutside);
203
- };
204
- }, []);
205
-
206
- return (
207
- <div ref={containerRef} className="w-full max-w-md mx-auto relative">
208
- <div className="relative">
209
- <input
210
- ref={searchInputRef}
211
- type="text"
212
- placeholder="Search nodes..."
213
- value={searchQuery}
214
- onChange={handleSearchChange}
215
- onKeyDown={handleSearchKeyDown}
216
- onFocus={handleSearchFocus}
217
- className="w-full px-4 py-2 pr-10 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent))] focus:border-transparent"
218
- />
219
- {searchQuery && (
220
- <button
221
- onClick={clearSearch}
222
- className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
223
- aria-label="Clear search"
224
- >
225
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
226
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
227
- </svg>
228
- </button>
229
- )}
230
- </div>
231
- {showSuggestions && filteredSuggestions.length > 0 && (
232
- <div className="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-300 rounded-md shadow-lg z-50 max-h-60 overflow-y-auto">
233
- {filteredSuggestions.map((node, index) => {
234
- const nodeName = getNodeDisplayName(node);
235
- const nodeType = node.type || 'unknown';
236
- return (
237
- <div
238
- key={node.id}
239
- onClick={() => handleSuggestionClick(node)}
240
- className={`px-4 py-2 cursor-pointer flex items-center justify-between hover:bg-gray-100 ${
241
- index === selectedSuggestionIndex ? 'bg-[rgb(var(--ec-accent-subtle))]' : ''
242
- }`}
243
- >
244
- <span className="text-sm font-medium text-gray-900">{nodeName}</span>
245
- <span className={`text-xs capitalize px-2 py-1 rounded ${getNodeTypeColorClass(nodeType)}`}>{nodeType}</span>
246
- </div>
247
- );
248
- })}
249
- </div>
250
- )}
251
- </div>
252
- );
253
- }
254
- );
255
-
256
- VisualiserSearch.displayName = 'VisualiserSearch';
257
-
258
- export default VisualiserSearch;
@@ -1,313 +0,0 @@
1
- import React, { type RefObject, useState } from 'react';
2
- import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
3
- import {
4
- Code,
5
- Share2,
6
- Search,
7
- Grid3x3,
8
- Maximize2,
9
- Map,
10
- Sparkles,
11
- Zap,
12
- EyeOff,
13
- ExternalLink,
14
- Save,
15
- RotateCcw,
16
- Loader2,
17
- } from 'lucide-react';
18
- import { DocumentArrowDownIcon, PresentationChartLineIcon } from '@heroicons/react/24/outline';
19
- import type { VisualiserSearchRef } from './VisualiserSearch';
20
-
21
- interface VisualizerDropdownContentProps {
22
- isMermaidView: boolean;
23
- setIsMermaidView: (value: boolean) => void;
24
- animateMessages: boolean;
25
- toggleAnimateMessages: () => void;
26
- hideChannels: boolean;
27
- toggleChannelsVisibility: () => void;
28
- hasChannels: boolean;
29
- showMinimap: boolean;
30
- setShowMinimap: (value: boolean) => void;
31
- handleFitView: () => void;
32
- searchRef: RefObject<VisualiserSearchRef | null>;
33
- isChatEnabled: boolean;
34
- openChat: () => void;
35
- handleCopyArchitectureCode: () => void;
36
- handleExportVisual: () => void;
37
- setIsShareModalOpen: (value: boolean) => void;
38
- toggleFullScreen: () => void;
39
- openStudioModal: () => void;
40
- isDevMode?: boolean;
41
- onSaveLayout?: () => Promise<boolean>;
42
- onResetLayout?: () => Promise<boolean>;
43
- }
44
-
45
- const VisualizerDropdownContent: React.FC<VisualizerDropdownContentProps> = ({
46
- isMermaidView,
47
- setIsMermaidView,
48
- animateMessages,
49
- toggleAnimateMessages,
50
- hideChannels,
51
- toggleChannelsVisibility,
52
- hasChannels,
53
- showMinimap,
54
- setShowMinimap,
55
- handleFitView,
56
- searchRef,
57
- isChatEnabled,
58
- openChat,
59
- handleCopyArchitectureCode,
60
- handleExportVisual,
61
- setIsShareModalOpen,
62
- toggleFullScreen,
63
- openStudioModal,
64
- isDevMode = false,
65
- onSaveLayout,
66
- onResetLayout,
67
- }) => {
68
- const [layoutStatus, setLayoutStatus] = useState<'idle' | 'saving' | 'resetting'>('idle');
69
-
70
- const handleSaveLayout = async () => {
71
- if (!onSaveLayout) return;
72
- setLayoutStatus('saving');
73
- await onSaveLayout();
74
- setLayoutStatus('idle');
75
- };
76
-
77
- const handleResetLayout = async () => {
78
- if (!onResetLayout) return;
79
- if (!window.confirm('Reset layout to auto-positioning? This will delete your saved layout.')) {
80
- return;
81
- }
82
- setLayoutStatus('resetting');
83
- const success = await onResetLayout();
84
- if (success) {
85
- window.location.reload();
86
- } else {
87
- setLayoutStatus('idle');
88
- }
89
- };
90
-
91
- return (
92
- <>
93
- {/* Canvas Settings Submenu */}
94
- <DropdownMenu.Sub>
95
- <DropdownMenu.SubTrigger className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2 outline-none">
96
- <Grid3x3 className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
97
- <span className="flex-1 font-normal">Canvas</span>
98
- <svg className="w-3 h-3 text-[rgb(var(--ec-page-text-muted))]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
99
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
100
- </svg>
101
- </DropdownMenu.SubTrigger>
102
- <DropdownMenu.Portal>
103
- <DropdownMenu.SubContent
104
- className="min-w-[200px] bg-[rgb(var(--ec-card-bg))] rounded-lg shadow-xl border border-[rgb(var(--ec-page-border))] py-1.5 z-[60]"
105
- sideOffset={8}
106
- alignOffset={-8}
107
- >
108
- <DropdownMenu.CheckboxItem
109
- checked={isMermaidView}
110
- onCheckedChange={setIsMermaidView}
111
- className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2"
112
- >
113
- <Code className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
114
- <span className="flex-1 font-normal">Render as mermaid</span>
115
- <div
116
- className={`w-7 h-4 rounded-full transition-all duration-200 flex-shrink-0 relative ${isMermaidView ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'}`}
117
- >
118
- <div
119
- className={`absolute top-0.5 w-3 h-3 rounded-full bg-white shadow-sm transition-all duration-200 ${isMermaidView ? 'left-3.5' : 'left-0.5'}`}
120
- />
121
- </div>
122
- </DropdownMenu.CheckboxItem>
123
-
124
- <DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
125
-
126
- <DropdownMenu.CheckboxItem
127
- checked={animateMessages}
128
- onCheckedChange={toggleAnimateMessages}
129
- className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2"
130
- >
131
- <Zap className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
132
- <span className="flex-1 font-normal">Simulate Messages</span>
133
- <div
134
- className={`w-7 h-4 rounded-full transition-all duration-200 flex-shrink-0 relative ${animateMessages ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'}`}
135
- >
136
- <div
137
- className={`absolute top-0.5 w-3 h-3 rounded-full bg-white shadow-sm transition-all duration-200 ${animateMessages ? 'left-3.5' : 'left-0.5'}`}
138
- />
139
- </div>
140
- </DropdownMenu.CheckboxItem>
141
-
142
- {hasChannels && (
143
- <DropdownMenu.CheckboxItem
144
- checked={hideChannels}
145
- onCheckedChange={toggleChannelsVisibility}
146
- className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2"
147
- >
148
- <EyeOff className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
149
- <span className="flex-1 font-normal">Hide channels</span>
150
- <div
151
- className={`w-7 h-4 rounded-full transition-all duration-200 flex-shrink-0 relative ${hideChannels ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'}`}
152
- >
153
- <div
154
- className={`absolute top-0.5 w-3 h-3 rounded-full bg-white shadow-sm transition-all duration-200 ${hideChannels ? 'left-3.5' : 'left-0.5'}`}
155
- />
156
- </div>
157
- </DropdownMenu.CheckboxItem>
158
- )}
159
-
160
- <DropdownMenu.CheckboxItem
161
- checked={showMinimap}
162
- onCheckedChange={setShowMinimap}
163
- className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2"
164
- >
165
- <Map className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
166
- <span className="flex-1 font-normal">Show minimap</span>
167
- <div
168
- className={`w-7 h-4 rounded-full transition-all duration-200 flex-shrink-0 relative ${showMinimap ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'}`}
169
- >
170
- <div
171
- className={`absolute top-0.5 w-3 h-3 rounded-full bg-white shadow-sm transition-all duration-200 ${showMinimap ? 'left-3.5' : 'left-0.5'}`}
172
- />
173
- </div>
174
- </DropdownMenu.CheckboxItem>
175
-
176
- <DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
177
-
178
- <DropdownMenu.Item
179
- onClick={handleFitView}
180
- className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
181
- >
182
- <Maximize2 className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
183
- <span className="flex-1 font-normal">Fit to view</span>
184
- </DropdownMenu.Item>
185
-
186
- <DropdownMenu.Item
187
- onClick={() => {
188
- searchRef.current?.hideSuggestions();
189
- setTimeout(() => {
190
- const searchInput = document.querySelector('input[placeholder="Search nodes..."]') as HTMLInputElement;
191
- searchInput?.focus();
192
- }, 50);
193
- }}
194
- className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
195
- >
196
- <Search className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
197
- <span className="flex-1 font-normal">Find on canvas</span>
198
- </DropdownMenu.Item>
199
- </DropdownMenu.SubContent>
200
- </DropdownMenu.Portal>
201
- </DropdownMenu.Sub>
202
-
203
- {/* Dev Mode: Layout Submenu */}
204
- {isDevMode && onSaveLayout && (
205
- <DropdownMenu.Sub>
206
- <DropdownMenu.SubTrigger className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2 outline-none">
207
- <Save className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
208
- <span className="flex-1 font-normal">Layout</span>
209
- <span className="text-[10px] text-amber-600 font-medium">DEV</span>
210
- <svg className="w-3 h-3 text-[rgb(var(--ec-page-text-muted))]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
211
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
212
- </svg>
213
- </DropdownMenu.SubTrigger>
214
- <DropdownMenu.Portal>
215
- <DropdownMenu.SubContent
216
- className="min-w-[180px] bg-[rgb(var(--ec-card-bg))] rounded-lg shadow-xl border border-[rgb(var(--ec-page-border))] py-1.5 z-[60]"
217
- sideOffset={8}
218
- alignOffset={-8}
219
- >
220
- <DropdownMenu.Item
221
- onClick={handleSaveLayout}
222
- disabled={layoutStatus !== 'idle'}
223
- className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
224
- >
225
- {layoutStatus === 'saving' ? (
226
- <Loader2 className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 animate-spin" />
227
- ) : (
228
- <Save className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
229
- )}
230
- <span className="flex-1 font-normal">{layoutStatus === 'saving' ? 'Saving...' : 'Save Layout'}</span>
231
- </DropdownMenu.Item>
232
- <DropdownMenu.Item
233
- onClick={handleResetLayout}
234
- disabled={layoutStatus !== 'idle'}
235
- className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
236
- >
237
- {layoutStatus === 'resetting' ? (
238
- <Loader2 className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 animate-spin" />
239
- ) : (
240
- <RotateCcw className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
241
- )}
242
- <span className="flex-1 font-normal">{layoutStatus === 'resetting' ? 'Resetting...' : 'Reset Layout'}</span>
243
- </DropdownMenu.Item>
244
- </DropdownMenu.SubContent>
245
- </DropdownMenu.Portal>
246
- </DropdownMenu.Sub>
247
- )}
248
-
249
- {/* Ask AI */}
250
- {isChatEnabled && (
251
- <>
252
- <DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
253
- <DropdownMenu.Item
254
- onClick={openChat}
255
- className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
256
- >
257
- <Sparkles className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
258
- <span className="flex-1 font-normal">Ask a question</span>
259
- </DropdownMenu.Item>
260
- </>
261
- )}
262
-
263
- {/* Export Items */}
264
- <DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
265
- <DropdownMenu.Item
266
- onClick={handleCopyArchitectureCode}
267
- className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
268
- >
269
- <Code className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
270
- <span className="flex-1 font-normal">Copy as mermaid</span>
271
- </DropdownMenu.Item>
272
-
273
- <DropdownMenu.Item
274
- onClick={handleExportVisual}
275
- className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
276
- >
277
- <DocumentArrowDownIcon className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
278
- <span className="flex-1 font-normal">Export image</span>
279
- </DropdownMenu.Item>
280
-
281
- {/* Share Link */}
282
- <DropdownMenu.Item
283
- onClick={() => setIsShareModalOpen(true)}
284
- className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
285
- >
286
- <Share2 className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
287
- <span className="flex-1 font-normal">Share Link</span>
288
- </DropdownMenu.Item>
289
-
290
- {/* Start Presentation */}
291
- <DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
292
- <DropdownMenu.Item
293
- onClick={toggleFullScreen}
294
- className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
295
- >
296
- <PresentationChartLineIcon className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
297
- <span className="flex-1 font-normal">Start Presentation</span>
298
- </DropdownMenu.Item>
299
-
300
- {/* Open in EventCatalog Studio */}
301
- <DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
302
- <DropdownMenu.Item
303
- onClick={openStudioModal}
304
- className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
305
- >
306
- <ExternalLink className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
307
- <span className="flex-1 font-normal">Open in EventCatalog Studio</span>
308
- </DropdownMenu.Item>
309
- </>
310
- );
311
- };
312
-
313
- export default VisualizerDropdownContent;