create-stylus-ide 1.0.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.
- package/Readme.MD +1515 -0
- package/cli.js +28 -0
- package/frontend/.vscode/settings.json +9 -0
- package/frontend/app/api/chat/route.ts +101 -0
- package/frontend/app/api/check-setup/route.ts +93 -0
- package/frontend/app/api/cleanup/route.ts +14 -0
- package/frontend/app/api/compile/route.ts +95 -0
- package/frontend/app/api/compile-stream/route.ts +98 -0
- package/frontend/app/api/complete/route.ts +86 -0
- package/frontend/app/api/deploy/route.ts +118 -0
- package/frontend/app/api/export-abi/route.ts +58 -0
- package/frontend/app/favicon.ico +0 -0
- package/frontend/app/globals.css +177 -0
- package/frontend/app/layout.tsx +29 -0
- package/frontend/app/ml/page.tsx +694 -0
- package/frontend/app/page.tsx +1132 -0
- package/frontend/app/providers.tsx +18 -0
- package/frontend/app/qlearning/page.tsx +188 -0
- package/frontend/app/raytracing/page.tsx +268 -0
- package/frontend/components/abi/ABIDialog.tsx +132 -0
- package/frontend/components/ai/AICompletionPopup.tsx +76 -0
- package/frontend/components/ai/ChatPanel.tsx +292 -0
- package/frontend/components/ai/QuickActions.tsx +128 -0
- package/frontend/components/blockchain/BlockchainContractBanner.tsx +64 -0
- package/frontend/components/blockchain/BlockchainLoadingDialog.tsx +188 -0
- package/frontend/components/deploy/DeployDialog.tsx +334 -0
- package/frontend/components/editor/FileTabs.tsx +181 -0
- package/frontend/components/editor/MonacoEditor.tsx +306 -0
- package/frontend/components/file-tree/ContextMenu.tsx +110 -0
- package/frontend/components/file-tree/DeleteConfirmDialog.tsx +61 -0
- package/frontend/components/file-tree/FileInputDialog.tsx +97 -0
- package/frontend/components/file-tree/FileNode.tsx +60 -0
- package/frontend/components/file-tree/FileTree.tsx +259 -0
- package/frontend/components/file-tree/FileTreeSkeleton.tsx +26 -0
- package/frontend/components/file-tree/FolderNode.tsx +105 -0
- package/frontend/components/github/GitHubLoadingDialog.tsx +201 -0
- package/frontend/components/github/GitHubMetadataBanner.tsx +61 -0
- package/frontend/components/github/LoadFromGitHubDialog.tsx +125 -0
- package/frontend/components/github/URLCopyButton.tsx +60 -0
- package/frontend/components/interact/ContractInteraction.tsx +323 -0
- package/frontend/components/interact/ContractPlaceholder.tsx +41 -0
- package/frontend/components/orbit/BenchmarkDialog.tsx +342 -0
- package/frontend/components/orbit/OrbitExplorer.tsx +273 -0
- package/frontend/components/project/ProjectActions.tsx +176 -0
- package/frontend/components/q-learning/ContractConfig.tsx +172 -0
- package/frontend/components/q-learning/MazeGrid.tsx +346 -0
- package/frontend/components/q-learning/PathAnimation.tsx +384 -0
- package/frontend/components/q-learning/QTableHeatmap.tsx +300 -0
- package/frontend/components/q-learning/TrainingForm.tsx +349 -0
- package/frontend/components/ray-tracing/ContractConfig.tsx +245 -0
- package/frontend/components/ray-tracing/MintingForm.tsx +280 -0
- package/frontend/components/ray-tracing/RenderCanvas.tsx +228 -0
- package/frontend/components/ray-tracing/RenderingPanel.tsx +259 -0
- package/frontend/components/ray-tracing/StyleControls.tsx +217 -0
- package/frontend/components/setup/SetupGuide.tsx +290 -0
- package/frontend/components/ui/KeyboardShortcutHint.tsx +74 -0
- package/frontend/components/ui/alert-dialog.tsx +157 -0
- package/frontend/components/ui/alert.tsx +66 -0
- package/frontend/components/ui/badge.tsx +46 -0
- package/frontend/components/ui/button.tsx +62 -0
- package/frontend/components/ui/card.tsx +92 -0
- package/frontend/components/ui/context-menu.tsx +252 -0
- package/frontend/components/ui/dialog.tsx +143 -0
- package/frontend/components/ui/dropdown-menu.tsx +257 -0
- package/frontend/components/ui/input.tsx +21 -0
- package/frontend/components/ui/label.tsx +24 -0
- package/frontend/components/ui/progress.tsx +31 -0
- package/frontend/components/ui/scroll-area.tsx +58 -0
- package/frontend/components/ui/select.tsx +190 -0
- package/frontend/components/ui/separator.tsx +28 -0
- package/frontend/components/ui/sheet.tsx +139 -0
- package/frontend/components/ui/skeleton.tsx +13 -0
- package/frontend/components/ui/slider.tsx +63 -0
- package/frontend/components/ui/sonner.tsx +40 -0
- package/frontend/components/ui/tabs.tsx +66 -0
- package/frontend/components/ui/textarea.tsx +18 -0
- package/frontend/components/wallet/ConnectButton.tsx +167 -0
- package/frontend/components/wallet/FaucetButton.tsx +256 -0
- package/frontend/components.json +22 -0
- package/frontend/eslint.config.mjs +18 -0
- package/frontend/hooks/useAICompletion.ts +75 -0
- package/frontend/hooks/useBlockchainLoader.ts +58 -0
- package/frontend/hooks/useChats.ts +137 -0
- package/frontend/hooks/useCompilation.ts +173 -0
- package/frontend/hooks/useFileTabs.ts +178 -0
- package/frontend/hooks/useGitHubLoader.ts +50 -0
- package/frontend/hooks/useKeyboardShortcuts.ts +47 -0
- package/frontend/hooks/usePanelState.ts +115 -0
- package/frontend/hooks/useProjectState.ts +276 -0
- package/frontend/hooks/useResponsive.ts +29 -0
- package/frontend/lib/abi-parser.ts +58 -0
- package/frontend/lib/blockchain-api.ts +374 -0
- package/frontend/lib/blockchain-explorers.ts +75 -0
- package/frontend/lib/blockchain-loader.ts +112 -0
- package/frontend/lib/cargo-template.ts +64 -0
- package/frontend/lib/compilation.ts +529 -0
- package/frontend/lib/constants.ts +31 -0
- package/frontend/lib/deployment.ts +176 -0
- package/frontend/lib/file-utils.ts +83 -0
- package/frontend/lib/github-api.ts +246 -0
- package/frontend/lib/github-loader.ts +369 -0
- package/frontend/lib/ml-contract-template.txt +900 -0
- package/frontend/lib/orbit-chains.ts +181 -0
- package/frontend/lib/output-formatter.ts +68 -0
- package/frontend/lib/project-manager.ts +632 -0
- package/frontend/lib/ray-tracing-abi.ts +206 -0
- package/frontend/lib/storage.ts +189 -0
- package/frontend/lib/templates.ts +1662 -0
- package/frontend/lib/url-parser.ts +188 -0
- package/frontend/lib/utils.ts +6 -0
- package/frontend/lib/wagmi-config.ts +24 -0
- package/frontend/next.config.ts +7 -0
- package/frontend/package-lock.json +16259 -0
- package/frontend/package.json +60 -0
- package/frontend/postcss.config.mjs +7 -0
- package/frontend/public/file.svg +1 -0
- package/frontend/public/globe.svg +1 -0
- package/frontend/public/ml-weights/.gitkeep +0 -0
- package/frontend/public/ml-weights/model.pkl +0 -0
- package/frontend/public/ml-weights/model_weights.json +27102 -0
- package/frontend/public/ml-weights/test_samples.json +7888 -0
- package/frontend/public/next.svg +1 -0
- package/frontend/public/vercel.svg +1 -0
- package/frontend/public/window.svg +1 -0
- package/frontend/scripts/check-env.js +52 -0
- package/frontend/scripts/setup.js +285 -0
- package/frontend/tailwind.config.ts +64 -0
- package/frontend/tsconfig.json +34 -0
- package/frontend/types/blockchain.ts +63 -0
- package/frontend/types/github.ts +54 -0
- package/frontend/types/project.ts +106 -0
- package/ml-training/README.md +56 -0
- package/ml-training/train_tiny_model.py +325 -0
- package/ml-training/update_template.py +59 -0
- package/package.json +30 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Editor, { Monaco } from '@monaco-editor/react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import type { editor, languages, IDisposable } from 'monaco-editor';
|
|
6
|
+
import { AICompletionPopup } from '../ai/AICompletionPopup';
|
|
7
|
+
import { useAICompletion } from '@/hooks/useAICompletion';
|
|
8
|
+
|
|
9
|
+
interface MonacoEditorProps {
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
onSave?: () => void;
|
|
13
|
+
readOnly?: boolean;
|
|
14
|
+
errors?: Array<{ line: number; column: number; message: string }>;
|
|
15
|
+
language?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function MonacoEditor({
|
|
19
|
+
value,
|
|
20
|
+
onChange,
|
|
21
|
+
onSave,
|
|
22
|
+
readOnly = false,
|
|
23
|
+
errors = [],
|
|
24
|
+
language = 'rust',
|
|
25
|
+
}: MonacoEditorProps) {
|
|
26
|
+
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
|
27
|
+
const monacoRef = useRef<Monaco | null>(null);
|
|
28
|
+
|
|
29
|
+
const [showCompletion, setShowCompletion] = useState(false);
|
|
30
|
+
const [completionPosition, setCompletionPosition] = useState({ top: 0, left: 0 });
|
|
31
|
+
|
|
32
|
+
const { isLoading, completion, generateCompletion, clearCompletion } = useAICompletion();
|
|
33
|
+
|
|
34
|
+
// ✅ SSR-safe minimap enablement
|
|
35
|
+
const [minimapEnabled, setMinimapEnabled] = useState(false);
|
|
36
|
+
|
|
37
|
+
// (optional but nice) avoid provider leaks / duplicates across unmounts
|
|
38
|
+
const completionProviderDisposableRef = useRef<IDisposable | null>(null);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const update = () => setMinimapEnabled(window.innerWidth > 768);
|
|
42
|
+
update();
|
|
43
|
+
window.addEventListener('resize', update);
|
|
44
|
+
return () => window.removeEventListener('resize', update);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
// keep minimap synced after mount + on resize changes
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
editorRef.current?.updateOptions({ minimap: { enabled: minimapEnabled } });
|
|
50
|
+
}, [minimapEnabled]);
|
|
51
|
+
|
|
52
|
+
// cleanup any registered provider on unmount (prevents StrictMode duplicates)
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
return () => {
|
|
55
|
+
completionProviderDisposableRef.current?.dispose?.();
|
|
56
|
+
completionProviderDisposableRef.current = null;
|
|
57
|
+
};
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
// Apply error markers when errors change
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const monaco = monacoRef.current;
|
|
63
|
+
const ed = editorRef.current;
|
|
64
|
+
if (!monaco || !ed) return;
|
|
65
|
+
|
|
66
|
+
const model = ed.getModel();
|
|
67
|
+
if (!model) return;
|
|
68
|
+
|
|
69
|
+
const owner = 'stylus-errors';
|
|
70
|
+
monaco.editor.setModelMarkers(model, owner, []);
|
|
71
|
+
|
|
72
|
+
if (errors.length > 0) {
|
|
73
|
+
const markers: editor.IMarkerData[] = errors.map((error) => ({
|
|
74
|
+
severity: monaco.MarkerSeverity.Error,
|
|
75
|
+
startLineNumber: error.line,
|
|
76
|
+
startColumn: error.column,
|
|
77
|
+
endLineNumber: error.line,
|
|
78
|
+
endColumn: Math.max(error.column + 1, error.column + 10),
|
|
79
|
+
message: error.message,
|
|
80
|
+
source: language,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
monaco.editor.setModelMarkers(model, owner, markers);
|
|
84
|
+
}
|
|
85
|
+
}, [errors, language]);
|
|
86
|
+
|
|
87
|
+
// Handle keyboard shortcuts for completion
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
90
|
+
// Escape to close
|
|
91
|
+
if (e.key === 'Escape' && showCompletion) {
|
|
92
|
+
setShowCompletion(false);
|
|
93
|
+
clearCompletion();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Tab to accept
|
|
97
|
+
if (e.key === 'Tab' && showCompletion && completion) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
handleAcceptCompletion(completion);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
104
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
105
|
+
}, [showCompletion, completion, clearCompletion]);
|
|
106
|
+
|
|
107
|
+
function handleEditorDidMount(ed: editor.IStandaloneCodeEditor, monaco: Monaco) {
|
|
108
|
+
editorRef.current = ed;
|
|
109
|
+
monacoRef.current = monaco;
|
|
110
|
+
|
|
111
|
+
// ✅ ensure minimap is correct immediately after mount
|
|
112
|
+
ed.updateOptions({ minimap: { enabled: minimapEnabled } });
|
|
113
|
+
|
|
114
|
+
// Register Stylus-specific snippets (and avoid duplicates)
|
|
115
|
+
if (!completionProviderDisposableRef.current) {
|
|
116
|
+
const provider: languages.CompletionItemProvider = {
|
|
117
|
+
provideCompletionItems: (model, position) => {
|
|
118
|
+
const word = model.getWordUntilPosition(position);
|
|
119
|
+
const range = {
|
|
120
|
+
startLineNumber: position.lineNumber,
|
|
121
|
+
endLineNumber: position.lineNumber,
|
|
122
|
+
startColumn: word.startColumn,
|
|
123
|
+
endColumn: word.endColumn,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const suggestions: languages.CompletionItem[] = [
|
|
127
|
+
{
|
|
128
|
+
label: 'sol_storage',
|
|
129
|
+
kind: monaco.languages.CompletionItemKind.Snippet,
|
|
130
|
+
insertText: [
|
|
131
|
+
'sol_storage! {',
|
|
132
|
+
' #[entrypoint]',
|
|
133
|
+
' pub struct ${1:ContractName} {',
|
|
134
|
+
' ${2:// Add storage variables}',
|
|
135
|
+
' }',
|
|
136
|
+
'}',
|
|
137
|
+
].join('\n'),
|
|
138
|
+
insertTextRules:
|
|
139
|
+
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
140
|
+
documentation: 'Stylus storage macro',
|
|
141
|
+
range,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
label: '#[public]',
|
|
145
|
+
kind: monaco.languages.CompletionItemKind.Snippet,
|
|
146
|
+
insertText: [
|
|
147
|
+
'#[public]',
|
|
148
|
+
'impl ${1:ContractName} {',
|
|
149
|
+
' pub fn ${2:function_name}(&self) -> ${3:ReturnType} {',
|
|
150
|
+
' ${4:// Implementation}',
|
|
151
|
+
' }',
|
|
152
|
+
'}',
|
|
153
|
+
].join('\n'),
|
|
154
|
+
insertTextRules:
|
|
155
|
+
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
156
|
+
documentation: 'Stylus public implementation',
|
|
157
|
+
range,
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
return { suggestions };
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
completionProviderDisposableRef.current =
|
|
166
|
+
monaco.languages.registerCompletionItemProvider('rust', provider);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// AI Completion - Ctrl/Cmd + K
|
|
170
|
+
ed.addAction({
|
|
171
|
+
id: 'ai-complete',
|
|
172
|
+
label: 'AI Complete',
|
|
173
|
+
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK],
|
|
174
|
+
run: () => {
|
|
175
|
+
handleAIComplete();
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Compile - Ctrl/Cmd + S
|
|
180
|
+
ed.addAction({
|
|
181
|
+
id: 'compile-contract',
|
|
182
|
+
label: 'Compile Contract',
|
|
183
|
+
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
|
|
184
|
+
run: () => onSave?.(),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
ed.focus();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handleEditorChange(newValue: string | undefined) {
|
|
191
|
+
onChange(newValue || '');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function handleAIComplete() {
|
|
195
|
+
if (!editorRef.current) return;
|
|
196
|
+
|
|
197
|
+
const position = editorRef.current.getPosition();
|
|
198
|
+
if (!position) return;
|
|
199
|
+
|
|
200
|
+
const model = editorRef.current.getModel();
|
|
201
|
+
if (!model) return;
|
|
202
|
+
|
|
203
|
+
// Get current line text
|
|
204
|
+
const lineContent = model.getLineContent(position.lineNumber);
|
|
205
|
+
const currentLineText = lineContent.substring(0, position.column - 1);
|
|
206
|
+
|
|
207
|
+
// Get context (previous 20 lines)
|
|
208
|
+
const startLine = Math.max(1, position.lineNumber - 20);
|
|
209
|
+
const contextRange = {
|
|
210
|
+
startLineNumber: startLine,
|
|
211
|
+
startColumn: 1,
|
|
212
|
+
endLineNumber: position.lineNumber,
|
|
213
|
+
endColumn: position.column,
|
|
214
|
+
};
|
|
215
|
+
const context = model.getValueInRange(contextRange);
|
|
216
|
+
|
|
217
|
+
// Calculate popup position
|
|
218
|
+
const coords = editorRef.current.getScrolledVisiblePosition(position);
|
|
219
|
+
if (coords) {
|
|
220
|
+
const editorDom = editorRef.current.getDomNode();
|
|
221
|
+
if (editorDom) {
|
|
222
|
+
const rect = editorDom.getBoundingClientRect();
|
|
223
|
+
setCompletionPosition({
|
|
224
|
+
top: rect.top + coords.top + coords.height + 5,
|
|
225
|
+
left: rect.left + coords.left,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
setShowCompletion(true);
|
|
231
|
+
await generateCompletion(currentLineText, context);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function handleAcceptCompletion(completionText: string) {
|
|
235
|
+
if (!editorRef.current) return;
|
|
236
|
+
|
|
237
|
+
const position = editorRef.current.getPosition();
|
|
238
|
+
if (!position) return;
|
|
239
|
+
|
|
240
|
+
// Insert completion at current position
|
|
241
|
+
const range = {
|
|
242
|
+
startLineNumber: position.lineNumber,
|
|
243
|
+
startColumn: position.column,
|
|
244
|
+
endLineNumber: position.lineNumber,
|
|
245
|
+
endColumn: position.column,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
editorRef.current.executeEdits('ai-completion', [
|
|
249
|
+
{
|
|
250
|
+
range,
|
|
251
|
+
text: completionText,
|
|
252
|
+
forceMoveMarkers: true,
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
setShowCompletion(false);
|
|
257
|
+
clearCompletion();
|
|
258
|
+
editorRef.current.focus();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function handleRejectCompletion() {
|
|
262
|
+
setShowCompletion(false);
|
|
263
|
+
clearCompletion();
|
|
264
|
+
editorRef.current?.focus();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<>
|
|
269
|
+
<Editor
|
|
270
|
+
height="100%"
|
|
271
|
+
defaultLanguage={language}
|
|
272
|
+
language={language}
|
|
273
|
+
value={value}
|
|
274
|
+
onChange={handleEditorChange}
|
|
275
|
+
onMount={handleEditorDidMount}
|
|
276
|
+
theme="vs-dark"
|
|
277
|
+
options={{
|
|
278
|
+
readOnly,
|
|
279
|
+
fontSize: 14,
|
|
280
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
|
281
|
+
minimap: { enabled: minimapEnabled }, // ✅ no window here
|
|
282
|
+
scrollBeyondLastLine: false,
|
|
283
|
+
automaticLayout: true,
|
|
284
|
+
tabSize: 4,
|
|
285
|
+
formatOnPaste: true,
|
|
286
|
+
formatOnType: true,
|
|
287
|
+
rulers: [80, 100],
|
|
288
|
+
wordWrap: 'on',
|
|
289
|
+
lineNumbers: 'on',
|
|
290
|
+
glyphMargin: true,
|
|
291
|
+
folding: true,
|
|
292
|
+
bracketPairColorization: { enabled: true },
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
|
|
296
|
+
<AICompletionPopup
|
|
297
|
+
visible={showCompletion}
|
|
298
|
+
position={completionPosition}
|
|
299
|
+
onAccept={handleAcceptCompletion}
|
|
300
|
+
onReject={handleRejectCompletion}
|
|
301
|
+
isLoading={isLoading}
|
|
302
|
+
completion={completion}
|
|
303
|
+
/>
|
|
304
|
+
</>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useCallback } from 'react';
|
|
4
|
+
import { FileNode } from '@/types/project';
|
|
5
|
+
import {
|
|
6
|
+
ContextMenu as ContextMenuPrimitive,
|
|
7
|
+
ContextMenuContent,
|
|
8
|
+
ContextMenuItem,
|
|
9
|
+
ContextMenuSeparator,
|
|
10
|
+
ContextMenuTrigger,
|
|
11
|
+
} from '@/components/ui/context-menu';
|
|
12
|
+
import {
|
|
13
|
+
FilePlus,
|
|
14
|
+
FolderPlus,
|
|
15
|
+
Pencil,
|
|
16
|
+
Copy,
|
|
17
|
+
Trash2,
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
|
|
20
|
+
interface ContextMenuProps {
|
|
21
|
+
node: FileNode | null;
|
|
22
|
+
onNewFile: (parentPath?: string) => void;
|
|
23
|
+
onNewFolder: (parentPath?: string) => void;
|
|
24
|
+
onRename: (path: string) => void;
|
|
25
|
+
onDuplicate: (path: string) => void;
|
|
26
|
+
onDelete: (path: string, isFolder: boolean) => void;
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function FileTreeContextMenu({
|
|
31
|
+
node,
|
|
32
|
+
onNewFile,
|
|
33
|
+
onNewFolder,
|
|
34
|
+
onRename,
|
|
35
|
+
onDuplicate,
|
|
36
|
+
onDelete,
|
|
37
|
+
children,
|
|
38
|
+
}: ContextMenuProps) {
|
|
39
|
+
const isFolder = node?.type === 'folder';
|
|
40
|
+
const isFile = node?.type === 'file';
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<ContextMenuPrimitive>
|
|
44
|
+
<ContextMenuTrigger asChild>
|
|
45
|
+
{children}
|
|
46
|
+
</ContextMenuTrigger>
|
|
47
|
+
|
|
48
|
+
<ContextMenuContent className="w-56">
|
|
49
|
+
{/* New File/Folder - Available for folders and empty space */}
|
|
50
|
+
{(isFolder || !node) && (
|
|
51
|
+
<>
|
|
52
|
+
<ContextMenuItem
|
|
53
|
+
onClick={() => onNewFile(node?.path)}
|
|
54
|
+
className="cursor-pointer"
|
|
55
|
+
>
|
|
56
|
+
<FilePlus className="h-4 w-4 mr-2" />
|
|
57
|
+
New File
|
|
58
|
+
</ContextMenuItem>
|
|
59
|
+
|
|
60
|
+
<ContextMenuItem
|
|
61
|
+
onClick={() => onNewFolder(node?.path)}
|
|
62
|
+
className="cursor-pointer"
|
|
63
|
+
>
|
|
64
|
+
<FolderPlus className="h-4 w-4 mr-2" />
|
|
65
|
+
New Folder
|
|
66
|
+
</ContextMenuItem>
|
|
67
|
+
|
|
68
|
+
{node && <ContextMenuSeparator />}
|
|
69
|
+
</>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{/* File-specific actions */}
|
|
73
|
+
{isFile && (
|
|
74
|
+
<>
|
|
75
|
+
<ContextMenuItem
|
|
76
|
+
onClick={() => onDuplicate(node.path)}
|
|
77
|
+
className="cursor-pointer"
|
|
78
|
+
>
|
|
79
|
+
<Copy className="h-4 w-4 mr-2" />
|
|
80
|
+
Duplicate
|
|
81
|
+
</ContextMenuItem>
|
|
82
|
+
|
|
83
|
+
<ContextMenuSeparator />
|
|
84
|
+
</>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{/* Rename & Delete - Available for both files and folders */}
|
|
88
|
+
{node && (
|
|
89
|
+
<>
|
|
90
|
+
<ContextMenuItem
|
|
91
|
+
onClick={() => onRename(node.path)}
|
|
92
|
+
className="cursor-pointer"
|
|
93
|
+
>
|
|
94
|
+
<Pencil className="h-4 w-4 mr-2" />
|
|
95
|
+
Rename
|
|
96
|
+
</ContextMenuItem>
|
|
97
|
+
|
|
98
|
+
<ContextMenuItem
|
|
99
|
+
onClick={() => onDelete(node.path, isFolder)}
|
|
100
|
+
className="cursor-pointer text-red-600 focus:text-red-600"
|
|
101
|
+
>
|
|
102
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
103
|
+
Delete
|
|
104
|
+
</ContextMenuItem>
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
</ContextMenuContent>
|
|
108
|
+
</ContextMenuPrimitive>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AlertDialog,
|
|
5
|
+
AlertDialogAction,
|
|
6
|
+
AlertDialogCancel,
|
|
7
|
+
AlertDialogContent,
|
|
8
|
+
AlertDialogDescription,
|
|
9
|
+
AlertDialogFooter,
|
|
10
|
+
AlertDialogHeader,
|
|
11
|
+
AlertDialogTitle,
|
|
12
|
+
} from '@/components/ui/alert-dialog';
|
|
13
|
+
|
|
14
|
+
interface DeleteConfirmDialogProps {
|
|
15
|
+
open: boolean;
|
|
16
|
+
onOpenChange: (open: boolean) => void;
|
|
17
|
+
itemName: string;
|
|
18
|
+
isFolder: boolean;
|
|
19
|
+
onConfirm: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function DeleteConfirmDialog({
|
|
23
|
+
open,
|
|
24
|
+
onOpenChange,
|
|
25
|
+
itemName,
|
|
26
|
+
isFolder,
|
|
27
|
+
onConfirm,
|
|
28
|
+
}: DeleteConfirmDialogProps) {
|
|
29
|
+
return (
|
|
30
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
31
|
+
<AlertDialogContent>
|
|
32
|
+
<AlertDialogHeader>
|
|
33
|
+
<AlertDialogTitle>
|
|
34
|
+
Delete {isFolder ? 'Folder' : 'File'}?
|
|
35
|
+
</AlertDialogTitle>
|
|
36
|
+
<AlertDialogDescription>
|
|
37
|
+
Are you sure you want to delete <strong>{itemName}</strong>?
|
|
38
|
+
{isFolder && (
|
|
39
|
+
<span className="block mt-2 text-red-600">
|
|
40
|
+
This will delete all files inside this folder.
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
<span className="block mt-2">
|
|
44
|
+
This action cannot be undone.
|
|
45
|
+
</span>
|
|
46
|
+
</AlertDialogDescription>
|
|
47
|
+
</AlertDialogHeader>
|
|
48
|
+
|
|
49
|
+
<AlertDialogFooter>
|
|
50
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
51
|
+
<AlertDialogAction
|
|
52
|
+
onClick={onConfirm}
|
|
53
|
+
className="bg-red-600 hover:bg-red-700 focus:ring-red-600"
|
|
54
|
+
>
|
|
55
|
+
Delete
|
|
56
|
+
</AlertDialogAction>
|
|
57
|
+
</AlertDialogFooter>
|
|
58
|
+
</AlertDialogContent>
|
|
59
|
+
</AlertDialog>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from '@/components/ui/dialog';
|
|
12
|
+
import { Button } from '@/components/ui/button';
|
|
13
|
+
import { Input } from '@/components/ui/input';
|
|
14
|
+
import { Label } from '@/components/ui/label';
|
|
15
|
+
|
|
16
|
+
interface FileInputDialogProps {
|
|
17
|
+
open: boolean;
|
|
18
|
+
onOpenChange: (open: boolean) => void;
|
|
19
|
+
title: string;
|
|
20
|
+
description: string;
|
|
21
|
+
defaultValue?: string;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
onConfirm: (value: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function FileInputDialog({
|
|
27
|
+
open,
|
|
28
|
+
onOpenChange,
|
|
29
|
+
title,
|
|
30
|
+
description,
|
|
31
|
+
defaultValue = '',
|
|
32
|
+
placeholder = '',
|
|
33
|
+
onConfirm,
|
|
34
|
+
}: FileInputDialogProps) {
|
|
35
|
+
const [value, setValue] = useState(defaultValue);
|
|
36
|
+
|
|
37
|
+
// Reset value when dialog opens
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (open) {
|
|
40
|
+
setValue(defaultValue);
|
|
41
|
+
}
|
|
42
|
+
}, [open, defaultValue]);
|
|
43
|
+
|
|
44
|
+
const handleConfirm = () => {
|
|
45
|
+
if (value.trim()) {
|
|
46
|
+
onConfirm(value.trim());
|
|
47
|
+
onOpenChange(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
52
|
+
if (e.key === 'Enter') {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
handleConfirm();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
60
|
+
<DialogContent className="sm:max-w-106.25">
|
|
61
|
+
<DialogHeader>
|
|
62
|
+
<DialogTitle>{title}</DialogTitle>
|
|
63
|
+
<DialogDescription>{description}</DialogDescription>
|
|
64
|
+
</DialogHeader>
|
|
65
|
+
|
|
66
|
+
<div className="grid gap-4 py-4">
|
|
67
|
+
<div className="grid gap-2">
|
|
68
|
+
<Label htmlFor="name">Name</Label>
|
|
69
|
+
<Input
|
|
70
|
+
id="name"
|
|
71
|
+
value={value}
|
|
72
|
+
onChange={(e) => setValue(e.target.value)}
|
|
73
|
+
onKeyDown={handleKeyDown}
|
|
74
|
+
placeholder={placeholder}
|
|
75
|
+
autoFocus
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<DialogFooter>
|
|
81
|
+
<Button
|
|
82
|
+
variant="outline"
|
|
83
|
+
onClick={() => onOpenChange(false)}
|
|
84
|
+
>
|
|
85
|
+
Cancel
|
|
86
|
+
</Button>
|
|
87
|
+
<Button
|
|
88
|
+
onClick={handleConfirm}
|
|
89
|
+
disabled={!value.trim()}
|
|
90
|
+
>
|
|
91
|
+
Confirm
|
|
92
|
+
</Button>
|
|
93
|
+
</DialogFooter>
|
|
94
|
+
</DialogContent>
|
|
95
|
+
</Dialog>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { FileNode as FileNodeType } from '@/types/project';
|
|
4
|
+
import { FileCode, FileText, File } from 'lucide-react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
interface FileNodeProps {
|
|
8
|
+
node: FileNodeType;
|
|
9
|
+
isActive: boolean;
|
|
10
|
+
onClick: () => void;
|
|
11
|
+
onContextMenu: () => void; // Keep this prop
|
|
12
|
+
depth: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function FileNode({
|
|
16
|
+
node,
|
|
17
|
+
isActive,
|
|
18
|
+
onClick,
|
|
19
|
+
onContextMenu,
|
|
20
|
+
depth,
|
|
21
|
+
}: FileNodeProps) {
|
|
22
|
+
const paddingLeft = depth * 12 + 8;
|
|
23
|
+
|
|
24
|
+
const getFileIcon = () => {
|
|
25
|
+
const name = node.name.toLowerCase();
|
|
26
|
+
|
|
27
|
+
if (name.endsWith('.rs')) {
|
|
28
|
+
return <FileCode className="h-4 w-4 text-orange-500" />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (name.endsWith('.toml')) {
|
|
32
|
+
return <FileText className="h-4 w-4 text-blue-500" />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (name.endsWith('.md')) {
|
|
36
|
+
return <FileText className="h-4 w-4 text-gray-500" />;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return <File className="h-4 w-4 text-gray-400" />;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={cn(
|
|
45
|
+
'flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer text-sm',
|
|
46
|
+
'hover:bg-accent transition-colors select-none',
|
|
47
|
+
isActive && 'bg-accent text-accent-foreground font-medium'
|
|
48
|
+
)}
|
|
49
|
+
style={{ paddingLeft: `${paddingLeft}px` }}
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
onContextMenu={(e) => {
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
onContextMenu();
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{getFileIcon()}
|
|
57
|
+
<span className="truncate">{node.name}</span>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|