@marktoflow/gui 2.0.0-alpha.1

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 (165) hide show
  1. package/.turbo/turbo-build.log +26 -0
  2. package/.turbo/turbo-test.log +22 -0
  3. package/README.md +179 -0
  4. package/dist/client/assets/index-DwTI8opO.js +608 -0
  5. package/dist/client/assets/index-DwTI8opO.js.map +1 -0
  6. package/dist/client/assets/index-RoEdL6gO.css +1 -0
  7. package/dist/client/index.html +20 -0
  8. package/dist/client/vite.svg +9 -0
  9. package/dist/server/index.d.ts +3 -0
  10. package/dist/server/index.d.ts.map +1 -0
  11. package/dist/server/index.js +56 -0
  12. package/dist/server/index.js.map +1 -0
  13. package/dist/server/routes/ai.js +50 -0
  14. package/dist/server/routes/ai.js.map +1 -0
  15. package/dist/server/routes/execute.js +62 -0
  16. package/dist/server/routes/execute.js.map +1 -0
  17. package/dist/server/routes/workflows.js +99 -0
  18. package/dist/server/routes/workflows.js.map +1 -0
  19. package/dist/server/server/index.js +95 -0
  20. package/dist/server/server/index.js.map +1 -0
  21. package/dist/server/server/routes/ai.js +87 -0
  22. package/dist/server/server/routes/ai.js.map +1 -0
  23. package/dist/server/server/routes/execute.js +63 -0
  24. package/dist/server/server/routes/execute.js.map +1 -0
  25. package/dist/server/server/routes/tools.js +518 -0
  26. package/dist/server/server/routes/tools.js.map +1 -0
  27. package/dist/server/server/routes/workflows.js +99 -0
  28. package/dist/server/server/routes/workflows.js.map +1 -0
  29. package/dist/server/server/services/AIService.js +69 -0
  30. package/dist/server/server/services/AIService.js.map +1 -0
  31. package/dist/server/server/services/FileWatcher.js +60 -0
  32. package/dist/server/server/services/FileWatcher.js.map +1 -0
  33. package/dist/server/server/services/WorkflowService.js +363 -0
  34. package/dist/server/server/services/WorkflowService.js.map +1 -0
  35. package/dist/server/server/services/agents/claude-code-provider.js +250 -0
  36. package/dist/server/server/services/agents/claude-code-provider.js.map +1 -0
  37. package/dist/server/server/services/agents/claude-provider.js +204 -0
  38. package/dist/server/server/services/agents/claude-provider.js.map +1 -0
  39. package/dist/server/server/services/agents/copilot-provider.js +227 -0
  40. package/dist/server/server/services/agents/copilot-provider.js.map +1 -0
  41. package/dist/server/server/services/agents/demo-provider.js +167 -0
  42. package/dist/server/server/services/agents/demo-provider.js.map +1 -0
  43. package/dist/server/server/services/agents/index.js +31 -0
  44. package/dist/server/server/services/agents/index.js.map +1 -0
  45. package/dist/server/server/services/agents/ollama-provider.js +220 -0
  46. package/dist/server/server/services/agents/ollama-provider.js.map +1 -0
  47. package/dist/server/server/services/agents/prompts.js +436 -0
  48. package/dist/server/server/services/agents/prompts.js.map +1 -0
  49. package/dist/server/server/services/agents/registry.js +242 -0
  50. package/dist/server/server/services/agents/registry.js.map +1 -0
  51. package/dist/server/server/services/agents/types.js +6 -0
  52. package/dist/server/server/services/agents/types.js.map +1 -0
  53. package/dist/server/server/websocket/index.js +85 -0
  54. package/dist/server/server/websocket/index.js.map +1 -0
  55. package/dist/server/services/AIService.d.ts +30 -0
  56. package/dist/server/services/AIService.d.ts.map +1 -0
  57. package/dist/server/services/AIService.js +216 -0
  58. package/dist/server/services/AIService.js.map +1 -0
  59. package/dist/server/services/FileWatcher.d.ts +10 -0
  60. package/dist/server/services/FileWatcher.d.ts.map +1 -0
  61. package/dist/server/services/FileWatcher.js +62 -0
  62. package/dist/server/services/FileWatcher.js.map +1 -0
  63. package/dist/server/services/WorkflowService.d.ts +54 -0
  64. package/dist/server/services/WorkflowService.d.ts.map +1 -0
  65. package/dist/server/services/WorkflowService.js +323 -0
  66. package/dist/server/services/WorkflowService.js.map +1 -0
  67. package/dist/server/shared/constants.js +175 -0
  68. package/dist/server/shared/constants.js.map +1 -0
  69. package/dist/server/shared/types.js +3 -0
  70. package/dist/server/shared/types.js.map +1 -0
  71. package/dist/server/websocket/index.d.ts +10 -0
  72. package/dist/server/websocket/index.d.ts.map +1 -0
  73. package/dist/server/websocket/index.js +85 -0
  74. package/dist/server/websocket/index.js.map +1 -0
  75. package/index.html +19 -0
  76. package/package.json +96 -0
  77. package/playwright.config.ts +27 -0
  78. package/postcss.config.js +6 -0
  79. package/public/vite.svg +9 -0
  80. package/src/client/App.tsx +520 -0
  81. package/src/client/components/Canvas/Canvas.tsx +405 -0
  82. package/src/client/components/Canvas/ExecutionOverlay.tsx +847 -0
  83. package/src/client/components/Canvas/NodeContextMenu.tsx +188 -0
  84. package/src/client/components/Canvas/OutputNode.tsx +111 -0
  85. package/src/client/components/Canvas/StepNode.tsx +106 -0
  86. package/src/client/components/Canvas/SubWorkflowNode.tsx +141 -0
  87. package/src/client/components/Canvas/Toolbar.tsx +189 -0
  88. package/src/client/components/Canvas/TriggerNode.tsx +128 -0
  89. package/src/client/components/Editor/InputsEditor.tsx +458 -0
  90. package/src/client/components/Editor/NewStepWizard.tsx +344 -0
  91. package/src/client/components/Editor/StepEditor.tsx +532 -0
  92. package/src/client/components/Editor/YamlEditor.tsx +160 -0
  93. package/src/client/components/Panels/PropertiesPanel.tsx +589 -0
  94. package/src/client/components/Prompt/ChangePreview.tsx +281 -0
  95. package/src/client/components/Prompt/PromptHistoryPanel.tsx +209 -0
  96. package/src/client/components/Prompt/PromptInput.tsx +108 -0
  97. package/src/client/components/Sidebar/Sidebar.tsx +343 -0
  98. package/src/client/components/common/Breadcrumb.tsx +40 -0
  99. package/src/client/components/common/Button.tsx +68 -0
  100. package/src/client/components/common/ContextMenu.tsx +202 -0
  101. package/src/client/components/common/KeyboardShortcuts.tsx +143 -0
  102. package/src/client/components/common/Modal.tsx +93 -0
  103. package/src/client/components/common/Tabs.tsx +57 -0
  104. package/src/client/components/common/ThemeToggle.tsx +63 -0
  105. package/src/client/components/index.ts +32 -0
  106. package/src/client/hooks/index.ts +4 -0
  107. package/src/client/hooks/useAIPrompt.ts +108 -0
  108. package/src/client/hooks/useCanvas.ts +247 -0
  109. package/src/client/hooks/useWebSocket.ts +164 -0
  110. package/src/client/hooks/useWorkflow.ts +138 -0
  111. package/src/client/main.tsx +10 -0
  112. package/src/client/stores/canvasStore.ts +348 -0
  113. package/src/client/stores/editorStore.ts +133 -0
  114. package/src/client/stores/executionStore.ts +440 -0
  115. package/src/client/stores/index.ts +4 -0
  116. package/src/client/stores/layoutStore.ts +103 -0
  117. package/src/client/stores/navigationStore.ts +49 -0
  118. package/src/client/stores/promptStore.ts +113 -0
  119. package/src/client/stores/themeStore.ts +75 -0
  120. package/src/client/stores/workflowStore.ts +177 -0
  121. package/src/client/styles/globals.css +346 -0
  122. package/src/client/utils/cn.ts +9 -0
  123. package/src/client/utils/index.ts +4 -0
  124. package/src/client/utils/serviceIcons.tsx +64 -0
  125. package/src/client/utils/stepValidation.ts +155 -0
  126. package/src/client/utils/workflowToGraph.ts +299 -0
  127. package/src/server/index.ts +114 -0
  128. package/src/server/routes/ai.ts +91 -0
  129. package/src/server/routes/execute.ts +71 -0
  130. package/src/server/routes/tools.ts +564 -0
  131. package/src/server/routes/workflows.ts +106 -0
  132. package/src/server/services/AIService.ts +105 -0
  133. package/src/server/services/FileWatcher.ts +69 -0
  134. package/src/server/services/WorkflowService.ts +441 -0
  135. package/src/server/services/agents/claude-code-provider.ts +320 -0
  136. package/src/server/services/agents/claude-provider.ts +248 -0
  137. package/src/server/services/agents/copilot-provider.ts +311 -0
  138. package/src/server/services/agents/demo-provider.ts +184 -0
  139. package/src/server/services/agents/index.ts +31 -0
  140. package/src/server/services/agents/ollama-provider.ts +267 -0
  141. package/src/server/services/agents/prompts.ts +482 -0
  142. package/src/server/services/agents/registry.ts +289 -0
  143. package/src/server/services/agents/types.ts +146 -0
  144. package/src/server/websocket/index.ts +104 -0
  145. package/src/shared/constants.ts +180 -0
  146. package/src/shared/types.ts +179 -0
  147. package/tailwind.config.ts +73 -0
  148. package/tests/e2e/app.spec.ts +90 -0
  149. package/tests/e2e/canvas.spec.ts +128 -0
  150. package/tests/e2e/workflow.spec.ts +185 -0
  151. package/tests/integration/api.test.ts +250 -0
  152. package/tests/integration/testApp.ts +31 -0
  153. package/tests/setup.ts +37 -0
  154. package/tests/unit/canvasStore.test.ts +502 -0
  155. package/tests/unit/components.test.tsx +151 -0
  156. package/tests/unit/executionStore.test.ts +527 -0
  157. package/tests/unit/layoutStore.test.ts +194 -0
  158. package/tests/unit/navigationStore.test.ts +152 -0
  159. package/tests/unit/stepValidation.test.ts +226 -0
  160. package/tests/unit/themeStore.test.ts +141 -0
  161. package/tests/unit/workflowToGraph.test.ts +289 -0
  162. package/tsconfig.json +29 -0
  163. package/tsconfig.server.json +28 -0
  164. package/vite.config.ts +31 -0
  165. package/vitest.config.ts +26 -0
@@ -0,0 +1,343 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import {
3
+ FileText,
4
+ FolderTree,
5
+ ChevronRight,
6
+ ChevronLeft,
7
+ Plus,
8
+ Search,
9
+ Loader2,
10
+ X,
11
+ } from 'lucide-react';
12
+ import { useWorkflowStore } from '../../stores/workflowStore';
13
+ import { useNavigationStore } from '../../stores/navigationStore';
14
+ import { useLayoutStore } from '../../stores/layoutStore';
15
+
16
+ export function Sidebar() {
17
+ const [activeTab, setActiveTab] = useState<'workflows' | 'tools'>(
18
+ 'workflows'
19
+ );
20
+ const { workflows, selectedWorkflow, selectWorkflow } = useWorkflowStore();
21
+ const { resetNavigation } = useNavigationStore();
22
+ const { sidebarOpen, setSidebarOpen, breakpoint } = useLayoutStore();
23
+
24
+ // Handle workflow selection - resets sub-workflow navigation
25
+ const handleSelectWorkflow = useCallback(
26
+ (path: string) => {
27
+ resetNavigation();
28
+ selectWorkflow(path);
29
+ // Close sidebar on mobile after selection
30
+ if (breakpoint === 'mobile') {
31
+ setSidebarOpen(false);
32
+ }
33
+ },
34
+ [resetNavigation, selectWorkflow, breakpoint, setSidebarOpen]
35
+ );
36
+
37
+ // Collapsed state for desktop
38
+ if (!sidebarOpen && breakpoint !== 'mobile') {
39
+ return (
40
+ <button
41
+ onClick={() => setSidebarOpen(true)}
42
+ className="w-12 bg-panel-bg border-r border-node-border flex flex-col items-center py-4 gap-4 hover:bg-white/5 transition-colors"
43
+ aria-label="Expand sidebar"
44
+ >
45
+ <ChevronRight className="w-4 h-4 text-gray-400" />
46
+ <FolderTree className="w-5 h-5 text-primary" />
47
+ </button>
48
+ );
49
+ }
50
+
51
+ // Mobile overlay
52
+ if (breakpoint === 'mobile') {
53
+ if (!sidebarOpen) return null;
54
+
55
+ return (
56
+ <>
57
+ {/* Backdrop */}
58
+ <div
59
+ className="fixed inset-0 bg-black/50 z-40 md:hidden"
60
+ onClick={() => setSidebarOpen(false)}
61
+ />
62
+ {/* Sidebar */}
63
+ <div className="fixed inset-y-0 left-0 w-72 bg-panel-bg border-r border-node-border flex flex-col z-50 md:hidden animate-slide-in-left">
64
+ <SidebarContent
65
+ activeTab={activeTab}
66
+ setActiveTab={setActiveTab}
67
+ workflows={workflows}
68
+ selectedWorkflow={selectedWorkflow}
69
+ onSelectWorkflow={handleSelectWorkflow}
70
+ onClose={() => setSidebarOpen(false)}
71
+ showClose
72
+ />
73
+ </div>
74
+ </>
75
+ );
76
+ }
77
+
78
+ // Desktop/Tablet sidebar
79
+ return (
80
+ <div className="w-64 bg-panel-bg border-r border-node-border flex flex-col">
81
+ <SidebarContent
82
+ activeTab={activeTab}
83
+ setActiveTab={setActiveTab}
84
+ workflows={workflows}
85
+ selectedWorkflow={selectedWorkflow}
86
+ onSelectWorkflow={handleSelectWorkflow}
87
+ onClose={() => setSidebarOpen(false)}
88
+ showClose={breakpoint === 'tablet'}
89
+ />
90
+ </div>
91
+ );
92
+ }
93
+
94
+ interface SidebarContentProps {
95
+ activeTab: 'workflows' | 'tools';
96
+ setActiveTab: (tab: 'workflows' | 'tools') => void;
97
+ workflows: Array<{ path: string; name: string }>;
98
+ selectedWorkflow: string | null;
99
+ onSelectWorkflow: (path: string) => void;
100
+ onClose: () => void;
101
+ showClose?: boolean;
102
+ }
103
+
104
+ function SidebarContent({
105
+ activeTab,
106
+ setActiveTab,
107
+ workflows,
108
+ selectedWorkflow,
109
+ onSelectWorkflow,
110
+ onClose,
111
+ showClose,
112
+ }: SidebarContentProps) {
113
+ return (
114
+ <>
115
+ {/* Logo/Title */}
116
+ <div className="p-4 border-b border-node-border flex items-center justify-between">
117
+ <h1 className="text-lg font-semibold text-white flex items-center gap-2">
118
+ <FolderTree className="w-5 h-5 text-primary" />
119
+ Marktoflow
120
+ </h1>
121
+ {showClose && (
122
+ <button
123
+ onClick={onClose}
124
+ className="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-white/10 transition-colors"
125
+ aria-label="Close sidebar"
126
+ >
127
+ <X className="w-4 h-4 text-gray-400" />
128
+ </button>
129
+ )}
130
+ </div>
131
+
132
+ {/* Tab buttons */}
133
+ <div className="flex border-b border-node-border">
134
+ <button
135
+ onClick={() => setActiveTab('workflows')}
136
+ className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
137
+ activeTab === 'workflows'
138
+ ? 'text-primary border-b-2 border-primary'
139
+ : 'text-gray-400 hover:text-white'
140
+ }`}
141
+ >
142
+ Workflows
143
+ </button>
144
+ <button
145
+ onClick={() => setActiveTab('tools')}
146
+ className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
147
+ activeTab === 'tools'
148
+ ? 'text-primary border-b-2 border-primary'
149
+ : 'text-gray-400 hover:text-white'
150
+ }`}
151
+ >
152
+ Tools
153
+ </button>
154
+ </div>
155
+
156
+ {/* Search */}
157
+ <div className="p-3 border-b border-node-border">
158
+ <div className="relative">
159
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
160
+ <input
161
+ type="text"
162
+ placeholder={`Search ${activeTab}...`}
163
+ className="w-full pl-9 pr-3 py-2 bg-node-bg border border-node-border rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary"
164
+ />
165
+ </div>
166
+ </div>
167
+
168
+ {/* Content */}
169
+ <div className="flex-1 overflow-y-auto p-2">
170
+ {activeTab === 'workflows' ? (
171
+ <WorkflowList
172
+ workflows={workflows}
173
+ selectedWorkflow={selectedWorkflow}
174
+ onSelect={onSelectWorkflow}
175
+ />
176
+ ) : (
177
+ <ToolsPalette />
178
+ )}
179
+ </div>
180
+
181
+ {/* New workflow button */}
182
+ {activeTab === 'workflows' && (
183
+ <div className="p-3 border-t border-node-border">
184
+ <button className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-primary hover:bg-primary-dark text-white rounded-lg text-sm font-medium transition-colors">
185
+ <Plus className="w-4 h-4" />
186
+ New Workflow
187
+ </button>
188
+ </div>
189
+ )}
190
+ </>
191
+ );
192
+ }
193
+
194
+ interface WorkflowListProps {
195
+ workflows: Array<{ path: string; name: string }>;
196
+ selectedWorkflow: string | null;
197
+ onSelect: (path: string) => void;
198
+ }
199
+
200
+ function WorkflowList({
201
+ workflows,
202
+ selectedWorkflow,
203
+ onSelect,
204
+ }: WorkflowListProps) {
205
+ if (workflows.length === 0) {
206
+ return (
207
+ <div className="text-center py-8 text-gray-500 text-sm">
208
+ No workflows found
209
+ </div>
210
+ );
211
+ }
212
+
213
+ return (
214
+ <div className="space-y-1">
215
+ {workflows.map((workflow) => (
216
+ <button
217
+ key={workflow.path}
218
+ onClick={() => onSelect(workflow.path)}
219
+ className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left transition-colors ${
220
+ selectedWorkflow === workflow.path
221
+ ? 'bg-primary/10 text-primary'
222
+ : 'text-gray-300 hover:bg-white/5'
223
+ }`}
224
+ >
225
+ <FileText className="w-4 h-4 flex-shrink-0" />
226
+ <span className="text-sm truncate">{workflow.name}</span>
227
+ <ChevronRight className="w-4 h-4 ml-auto flex-shrink-0 opacity-50" />
228
+ </button>
229
+ ))}
230
+ </div>
231
+ );
232
+ }
233
+
234
+ export interface ToolDefinition {
235
+ id: string;
236
+ name: string;
237
+ icon: string;
238
+ category: string;
239
+ description?: string;
240
+ sdk?: string;
241
+ authType?: string;
242
+ actionCount?: number;
243
+ actions?: string[];
244
+ }
245
+
246
+ // Fallback tools in case API is unavailable
247
+ const fallbackTools: ToolDefinition[] = [
248
+ { id: 'slack', name: 'Slack', icon: '💬', category: 'Communication', sdk: '@slack/web-api' },
249
+ { id: 'github', name: 'GitHub', icon: '🐙', category: 'Development', sdk: '@octokit/rest' },
250
+ { id: 'jira', name: 'Jira', icon: '📋', category: 'Project Management', sdk: 'jira.js' },
251
+ { id: 'gmail', name: 'Gmail', icon: '📧', category: 'Communication', sdk: 'googleapis' },
252
+ { id: 'http', name: 'HTTP', icon: '🌐', category: 'Network' },
253
+ { id: 'claude', name: 'Claude', icon: '🤖', category: 'AI' },
254
+ ];
255
+
256
+ function ToolsPalette() {
257
+ const [tools, setTools] = useState<ToolDefinition[]>(fallbackTools);
258
+ const [loading, setLoading] = useState(true);
259
+ const [searchQuery, setSearchQuery] = useState('');
260
+
261
+ // Fetch tools from API
262
+ useEffect(() => {
263
+ async function fetchTools() {
264
+ try {
265
+ const response = await fetch('/api/tools');
266
+ if (response.ok) {
267
+ const data = await response.json();
268
+ setTools(data.tools);
269
+ }
270
+ } catch (error) {
271
+ console.error('Failed to fetch tools:', error);
272
+ // Keep fallback tools
273
+ } finally {
274
+ setLoading(false);
275
+ }
276
+ }
277
+ fetchTools();
278
+ }, []);
279
+
280
+ // Filter tools by search query
281
+ const filteredTools = searchQuery
282
+ ? tools.filter(
283
+ (t) =>
284
+ t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
285
+ t.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
286
+ t.description?.toLowerCase().includes(searchQuery.toLowerCase())
287
+ )
288
+ : tools;
289
+
290
+ const categories = [...new Set(filteredTools.map((t) => t.category))];
291
+
292
+ const handleDragStart = (e: React.DragEvent, tool: ToolDefinition) => {
293
+ e.dataTransfer.setData('application/marktoflow-tool', JSON.stringify(tool));
294
+ e.dataTransfer.effectAllowed = 'copy';
295
+ };
296
+
297
+ if (loading) {
298
+ return (
299
+ <div className="flex items-center justify-center py-8">
300
+ <Loader2 className="w-5 h-5 text-gray-500 animate-spin" />
301
+ </div>
302
+ );
303
+ }
304
+
305
+ return (
306
+ <div className="space-y-4">
307
+ {categories.map((category) => (
308
+ <div key={category}>
309
+ <h3 className="text-xs font-medium text-gray-500 uppercase tracking-wider px-2 mb-2">
310
+ {category}
311
+ </h3>
312
+ <div className="space-y-1">
313
+ {filteredTools
314
+ .filter((t) => t.category === category)
315
+ .map((tool) => (
316
+ <div
317
+ key={tool.id}
318
+ draggable
319
+ onDragStart={(e) => handleDragStart(e, tool)}
320
+ className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300 hover:bg-white/5 cursor-grab active:cursor-grabbing transition-colors group"
321
+ title={tool.description || (tool.sdk ? 'SDK: ' + tool.sdk : undefined)}
322
+ >
323
+ <span className="text-lg">{tool.icon}</span>
324
+ <div className="flex-1 min-w-0">
325
+ <span className="text-sm block truncate">{tool.name}</span>
326
+ {tool.actionCount !== undefined && (
327
+ <span className="text-xs text-gray-500">{tool.actionCount} actions</span>
328
+ )}
329
+ </div>
330
+ </div>
331
+ ))}
332
+ </div>
333
+ </div>
334
+ ))}
335
+
336
+ {filteredTools.length === 0 && (
337
+ <div className="text-center py-8 text-gray-500 text-sm">
338
+ No tools found
339
+ </div>
340
+ )}
341
+ </div>
342
+ );
343
+ }
@@ -0,0 +1,40 @@
1
+ import { ChevronRight, Home } from 'lucide-react';
2
+
3
+ export interface BreadcrumbItem {
4
+ id: string;
5
+ name: string;
6
+ path?: string;
7
+ }
8
+
9
+ interface BreadcrumbProps {
10
+ items: BreadcrumbItem[];
11
+ onNavigate: (item: BreadcrumbItem, index: number) => void;
12
+ }
13
+
14
+ export function Breadcrumb({ items, onNavigate }: BreadcrumbProps) {
15
+ if (items.length <= 1) {
16
+ return null;
17
+ }
18
+
19
+ return (
20
+ <nav className="flex items-center gap-1 px-4 py-2 bg-panel-bg border-b border-node-border">
21
+ {items.map((item, index) => (
22
+ <div key={item.id} className="flex items-center">
23
+ {index > 0 && <ChevronRight className="w-4 h-4 text-gray-600 mx-1" />}
24
+ <button
25
+ onClick={() => onNavigate(item, index)}
26
+ className={`flex items-center gap-1.5 px-2 py-1 rounded text-sm transition-colors ${
27
+ index === items.length - 1
28
+ ? 'text-white font-medium cursor-default'
29
+ : 'text-gray-400 hover:text-white hover:bg-white/5'
30
+ }`}
31
+ disabled={index === items.length - 1}
32
+ >
33
+ {index === 0 && <Home className="w-3.5 h-3.5" />}
34
+ <span className="max-w-[150px] truncate">{item.name}</span>
35
+ </button>
36
+ </div>
37
+ ))}
38
+ </nav>
39
+ );
40
+ }
@@ -0,0 +1,68 @@
1
+ import { forwardRef } from 'react';
2
+ import { cn } from '../../utils/cn';
3
+ import { Loader2 } from 'lucide-react';
4
+
5
+ export interface ButtonProps
6
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
7
+ variant?: 'primary' | 'secondary' | 'ghost' | 'destructive';
8
+ size?: 'sm' | 'md' | 'lg';
9
+ loading?: boolean;
10
+ icon?: React.ReactNode;
11
+ }
12
+
13
+ const variantClasses = {
14
+ primary:
15
+ 'bg-primary hover:bg-primary-dark text-white border-transparent',
16
+ secondary:
17
+ 'bg-node-bg hover:bg-white/10 text-gray-300 border-node-border hover:border-primary',
18
+ ghost: 'bg-transparent hover:bg-white/10 text-gray-300 border-transparent',
19
+ destructive:
20
+ 'bg-error/10 hover:bg-error/20 text-error border-error/20 hover:border-error',
21
+ };
22
+
23
+ const sizeClasses = {
24
+ sm: 'px-2.5 py-1.5 text-xs',
25
+ md: 'px-4 py-2 text-sm',
26
+ lg: 'px-6 py-3 text-base',
27
+ };
28
+
29
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
30
+ (
31
+ {
32
+ className,
33
+ variant = 'primary',
34
+ size = 'md',
35
+ loading,
36
+ icon,
37
+ disabled,
38
+ children,
39
+ ...props
40
+ },
41
+ ref
42
+ ) => {
43
+ return (
44
+ <button
45
+ ref={ref}
46
+ disabled={disabled || loading}
47
+ className={cn(
48
+ 'inline-flex items-center justify-center gap-2 rounded-lg border font-medium transition-colors',
49
+ 'focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2 focus:ring-offset-panel-bg',
50
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
51
+ variantClasses[variant],
52
+ sizeClasses[size],
53
+ className
54
+ )}
55
+ {...props}
56
+ >
57
+ {loading ? (
58
+ <Loader2 className="w-4 h-4 animate-spin" />
59
+ ) : (
60
+ icon && <span className="flex-shrink-0">{icon}</span>
61
+ )}
62
+ {children}
63
+ </button>
64
+ );
65
+ }
66
+ );
67
+
68
+ Button.displayName = 'Button';
@@ -0,0 +1,202 @@
1
+ import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
2
+ import { forwardRef } from 'react';
3
+ import { cn } from '../../utils/cn';
4
+
5
+ const ContextMenu = ContextMenuPrimitive.Root;
6
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
7
+ const ContextMenuGroup = ContextMenuPrimitive.Group;
8
+ const ContextMenuPortal = ContextMenuPrimitive.Portal;
9
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
10
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
11
+
12
+ const ContextMenuSubTrigger = forwardRef<
13
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
14
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
15
+ inset?: boolean;
16
+ }
17
+ >(({ className, inset, children, ...props }, ref) => (
18
+ <ContextMenuPrimitive.SubTrigger
19
+ ref={ref}
20
+ className={cn(
21
+ 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
22
+ 'focus:bg-white/10 data-[state=open]:bg-white/10',
23
+ inset && 'pl-8',
24
+ className
25
+ )}
26
+ {...props}
27
+ >
28
+ {children}
29
+ <span className="ml-auto text-xs">▶</span>
30
+ </ContextMenuPrimitive.SubTrigger>
31
+ ));
32
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
33
+
34
+ const ContextMenuSubContent = forwardRef<
35
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
36
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
37
+ >(({ className, ...props }, ref) => (
38
+ <ContextMenuPrimitive.SubContent
39
+ ref={ref}
40
+ className={cn(
41
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border border-node-border bg-panel-bg p-1 shadow-lg',
42
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
43
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
44
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
45
+ className
46
+ )}
47
+ {...props}
48
+ />
49
+ ));
50
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
51
+
52
+ const ContextMenuContent = forwardRef<
53
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
54
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
55
+ >(({ className, ...props }, ref) => (
56
+ <ContextMenuPrimitive.Portal>
57
+ <ContextMenuPrimitive.Content
58
+ ref={ref}
59
+ className={cn(
60
+ 'z-50 min-w-[12rem] overflow-hidden rounded-md border border-node-border bg-panel-bg p-1 shadow-lg',
61
+ 'animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out',
62
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
63
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ </ContextMenuPrimitive.Portal>
69
+ ));
70
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
71
+
72
+ const ContextMenuItem = forwardRef<
73
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
74
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75
+ inset?: boolean;
76
+ destructive?: boolean;
77
+ }
78
+ >(({ className, inset, destructive, ...props }, ref) => (
79
+ <ContextMenuPrimitive.Item
80
+ ref={ref}
81
+ className={cn(
82
+ 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
83
+ 'focus:bg-white/10 focus:text-white',
84
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
85
+ destructive && 'text-error focus:bg-error/10 focus:text-error',
86
+ inset && 'pl-8',
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ ));
92
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
93
+
94
+ const ContextMenuCheckboxItem = forwardRef<
95
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
96
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
97
+ >(({ className, children, checked, ...props }, ref) => (
98
+ <ContextMenuPrimitive.CheckboxItem
99
+ ref={ref}
100
+ className={cn(
101
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none',
102
+ 'focus:bg-white/10 focus:text-white',
103
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
104
+ className
105
+ )}
106
+ checked={checked}
107
+ {...props}
108
+ >
109
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
110
+ <ContextMenuPrimitive.ItemIndicator>
111
+ <span className="text-primary">✓</span>
112
+ </ContextMenuPrimitive.ItemIndicator>
113
+ </span>
114
+ {children}
115
+ </ContextMenuPrimitive.CheckboxItem>
116
+ ));
117
+ ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
118
+
119
+ const ContextMenuRadioItem = forwardRef<
120
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
121
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
122
+ >(({ className, children, ...props }, ref) => (
123
+ <ContextMenuPrimitive.RadioItem
124
+ ref={ref}
125
+ className={cn(
126
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none',
127
+ 'focus:bg-white/10 focus:text-white',
128
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
129
+ className
130
+ )}
131
+ {...props}
132
+ >
133
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
134
+ <ContextMenuPrimitive.ItemIndicator>
135
+ <span className="h-2 w-2 rounded-full bg-primary" />
136
+ </ContextMenuPrimitive.ItemIndicator>
137
+ </span>
138
+ {children}
139
+ </ContextMenuPrimitive.RadioItem>
140
+ ));
141
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
142
+
143
+ const ContextMenuLabel = forwardRef<
144
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
145
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
146
+ inset?: boolean;
147
+ }
148
+ >(({ className, inset, ...props }, ref) => (
149
+ <ContextMenuPrimitive.Label
150
+ ref={ref}
151
+ className={cn(
152
+ 'px-2 py-1.5 text-xs font-semibold text-gray-400',
153
+ inset && 'pl-8',
154
+ className
155
+ )}
156
+ {...props}
157
+ />
158
+ ));
159
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
160
+
161
+ const ContextMenuSeparator = forwardRef<
162
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
163
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
164
+ >(({ className, ...props }, ref) => (
165
+ <ContextMenuPrimitive.Separator
166
+ ref={ref}
167
+ className={cn('-mx-1 my-1 h-px bg-node-border', className)}
168
+ {...props}
169
+ />
170
+ ));
171
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
172
+
173
+ const ContextMenuShortcut = ({
174
+ className,
175
+ ...props
176
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
177
+ return (
178
+ <span
179
+ className={cn('ml-auto text-xs tracking-widest text-gray-500', className)}
180
+ {...props}
181
+ />
182
+ );
183
+ };
184
+ ContextMenuShortcut.displayName = 'ContextMenuShortcut';
185
+
186
+ export {
187
+ ContextMenu,
188
+ ContextMenuTrigger,
189
+ ContextMenuContent,
190
+ ContextMenuItem,
191
+ ContextMenuCheckboxItem,
192
+ ContextMenuRadioItem,
193
+ ContextMenuLabel,
194
+ ContextMenuSeparator,
195
+ ContextMenuShortcut,
196
+ ContextMenuGroup,
197
+ ContextMenuPortal,
198
+ ContextMenuSub,
199
+ ContextMenuSubContent,
200
+ ContextMenuSubTrigger,
201
+ ContextMenuRadioGroup,
202
+ };