@catalystsoftware/ui 1.0.14 → 1.0.16
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/dist/components/catalyst-ui/data/chat-data.tsx +336 -0
- package/dist/components/catalyst-ui/data/code-data.tsx +1 -74
- package/dist/components/catalyst-ui/data/core-data.tsx +679 -7
- package/dist/components/catalyst-ui/data/crm-data.tsx +2 -2
- package/dist/components/catalyst-ui/data/forms-data.tsx +5 -5
- package/dist/components/catalyst-ui/data/media-data.tsx +66 -3
- package/dist/components/catalyst-ui/data/overlay-data.tsx +5 -3
- package/dist/components/catalyst-ui/data/primitive-data.tsx +18 -128
- package/dist/components/catalyst-ui/data/sidebars-data.tsx +38 -38
- package/dist/components/catalyst-ui/data/tools-data.tsx +1 -1
- package/dist/components/catalyst-ui/data/utils-data.tsx +27 -9
- package/dist/components/catalyst-ui/marketing/sections/header.tsx +1 -0
- package/dist/components/catalyst-ui/overlays/sidebar-props.tsx +1 -1
- package/dist/components/catalyst-ui/primitives/alert.tsx +1 -1
- package/dist/components/catalyst-ui/tools/md-badge-builder.tsx +1 -1
- package/dist/components/catalyst-ui/tools/monaco-sidebar.tsx +935 -331
- package/dist/components/catalyst-ui/tools/monaco.tsx +66 -66
- package/dist/components/catalyst-ui/tools/snippets.tsx +844 -0
- package/dist/data/tailwind.config.js +1 -1
- package/dist/data/tailwind.config.ngin.ts +1 -1
- package/dist/index.js +83 -86
- package/package.json +1 -1
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
import { ClientOnly, } from "~/utils/client-only";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Save, X, } from "lucide-react";
|
|
4
|
+
import { handleEditorDidMount as baseHandleEditorDidMount, renderEditor as baseRenderEditor, FieldContent, FieldGroup, FieldSeparator, FieldSet, MonacoToolbar, DEFAULT_PREF, loadEditorSettings, autoSaveSettings, } from "~/components/catalyst-ui";
|
|
5
|
+
import { Field, FieldLabel, FieldDescription, FieldError, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, Input, Switch, Button, cn, useToasted, useDS, LoadingPage, SpinnerV4, } from "~/components/catalyst-ui";
|
|
6
|
+
import { DEVSTACK_URL } from "~/components/shared";
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
|
|
9
|
+
export default function SnippetsEditor() {
|
|
10
|
+
return (
|
|
11
|
+
<ClientOnly>{() => <MonacoEditor />}</ClientOnly>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function MonacoEditor() {
|
|
16
|
+
const { toast } = useToasted();
|
|
17
|
+
const [code, setCode] = useState("Start coding...");
|
|
18
|
+
const { setOpen, open } = useDS();
|
|
19
|
+
const [es, setEs] = useState(() => loadEditorSettings());
|
|
20
|
+
const [theme, setTheme] = useState(DEFAULT_PREF.theme);
|
|
21
|
+
const [isFullscreen, setIsFullscreen] = useState(DEFAULT_PREF.isFullscreen);
|
|
22
|
+
const [editorRenameTab, setEditorRenameTab] = useState(false);
|
|
23
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
24
|
+
const [lastSaved, setLastSaved] = useState(null);
|
|
25
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
26
|
+
const customDarkTheme = {
|
|
27
|
+
base: "vs-dark",
|
|
28
|
+
inherit: true,
|
|
29
|
+
rules: [],
|
|
30
|
+
colors: {
|
|
31
|
+
"editor.background": "#020817",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const [activeTabId, setActiveTabId] = useState(1);
|
|
35
|
+
const [nextTabId, setNextTabId] = useState(2);
|
|
36
|
+
const [isClient, setIsClient] = useState(false);
|
|
37
|
+
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
|
|
38
|
+
const [folders, setFolders] = useState([]);
|
|
39
|
+
const [isEditing, setIsEditing] = useState(null);
|
|
40
|
+
const [hasEditedScope, setHasEditedScope] = useState(false);
|
|
41
|
+
const [replaceSpaces, setReplaceSpaces] = useState(true);
|
|
42
|
+
const [snippets, setSnippets] = useState([]);
|
|
43
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
44
|
+
const [isWordWrapEnabled, setIsWordWrapEnabled] = useState(false);
|
|
45
|
+
const [isStickyScrollEnabled, setIsStickyScrollEnabled] = useState(false);
|
|
46
|
+
const [isWhitespaceRendered, setIsWhitespaceRendered] = useState(false);
|
|
47
|
+
const [areIndentGuidesRendered, setAreIndentGuidesRendered] = useState(true);
|
|
48
|
+
const [error, setError] = useState({
|
|
49
|
+
label: null,
|
|
50
|
+
prefix: null,
|
|
51
|
+
description: null,
|
|
52
|
+
scope: null,
|
|
53
|
+
body: null,
|
|
54
|
+
folder: null,
|
|
55
|
+
});
|
|
56
|
+
const [tabs, setTabs] = useState([
|
|
57
|
+
{
|
|
58
|
+
id: 1,
|
|
59
|
+
name: "main.tsx",
|
|
60
|
+
content: code,
|
|
61
|
+
language: "typescript",
|
|
62
|
+
isDirty: false,
|
|
63
|
+
isActive: true,
|
|
64
|
+
|
|
65
|
+
label: '',
|
|
66
|
+
prefix: '',
|
|
67
|
+
description: '',
|
|
68
|
+
scope: 'typescript, typescriptreact',
|
|
69
|
+
folder: '',
|
|
70
|
+
global: true,
|
|
71
|
+
projectId: '',
|
|
72
|
+
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
const activeTab = tabs.find((tab) => tab.id === activeTabId);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
handleEditorChange(code);
|
|
79
|
+
}, [code]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!isClient) return;
|
|
83
|
+
async function initializeApp() {
|
|
84
|
+
const response = await fetch(DEVSTACK_URL + "/api/snippets-data");
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
|
|
87
|
+
// Extract snippets from the nested structure
|
|
88
|
+
const extractSnippets = (items) => {
|
|
89
|
+
let allSnippets = [];
|
|
90
|
+
|
|
91
|
+
for (const item of items) {
|
|
92
|
+
if (item.type === 'snippet') {
|
|
93
|
+
allSnippets.push({
|
|
94
|
+
id: item.label,
|
|
95
|
+
label: item.label,
|
|
96
|
+
prefix: item.prefix,
|
|
97
|
+
description: item.description || '',
|
|
98
|
+
body: Array.isArray(item.body) ? item.body.join('\n') : item.body || '',
|
|
99
|
+
scope: item.scope || '',
|
|
100
|
+
global: item.global || false,
|
|
101
|
+
projectId: item.projectId || '',
|
|
102
|
+
});
|
|
103
|
+
} else if (item.type === 'folder' && item.items) {
|
|
104
|
+
// Recursively extract snippets from folders
|
|
105
|
+
allSnippets = [...allSnippets, ...extractSnippets(item.items)];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return allSnippets;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// The data IS the object with "SNIPPETS" label and items array
|
|
113
|
+
// You need to access data.items directly
|
|
114
|
+
const snippetsArray = data.items ? extractSnippets(data.items) : [];
|
|
115
|
+
setSnippets(snippetsArray);
|
|
116
|
+
setIsEditing(false);
|
|
117
|
+
}
|
|
118
|
+
initializeApp();
|
|
119
|
+
const savedTabs = [];
|
|
120
|
+
let index = 1;
|
|
121
|
+
|
|
122
|
+
// Check localStorage for all saved tabs
|
|
123
|
+
while (true) {
|
|
124
|
+
const savedContent = localStorage.getItem(`catalyst-snippets-66-${index}`);
|
|
125
|
+
const savedName = localStorage.getItem(`catalyst-snippets-66-${index}-name`);
|
|
126
|
+
const savedLanguage = localStorage.getItem(`catalyst-snippets-66-${index}-language`);
|
|
127
|
+
|
|
128
|
+
if (!savedContent) break;
|
|
129
|
+
|
|
130
|
+
savedTabs.push({
|
|
131
|
+
id: index,
|
|
132
|
+
name: savedName || `new-snippet-${index}.tsx`,
|
|
133
|
+
content: savedContent,
|
|
134
|
+
language: savedLanguage || "typscript",
|
|
135
|
+
isDirty: false,
|
|
136
|
+
isActive: index === 1,
|
|
137
|
+
label: '',
|
|
138
|
+
prefix: '',
|
|
139
|
+
description: '',
|
|
140
|
+
scope: 'typescript, typescriptreact',
|
|
141
|
+
folder: '',
|
|
142
|
+
global: true,
|
|
143
|
+
projectId: '',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
index++;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (savedTabs.length > 0) {
|
|
150
|
+
setTabs(savedTabs);
|
|
151
|
+
setNextTabId(index);
|
|
152
|
+
setCode(savedTabs[0].content);
|
|
153
|
+
}
|
|
154
|
+
console.log(tabs,'tabs',snippets,'snippets',activeTab, 'activeTab')
|
|
155
|
+
}, [isClient]);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!activeTab) return;
|
|
159
|
+
setCode(activeTab.content);
|
|
160
|
+
}, [activeTabId]);
|
|
161
|
+
|
|
162
|
+
const autoSave = useCallback(
|
|
163
|
+
async (tabId, content) => {
|
|
164
|
+
const tab = tabs.find((t) => t.id === tabId);
|
|
165
|
+
if (!tab?.isDirty || !autoSaveEnabled) return;
|
|
166
|
+
|
|
167
|
+
setIsSaving(true);
|
|
168
|
+
try {
|
|
169
|
+
localStorage.setItem(`catalyst-snippets-66-${tabId}`, content);
|
|
170
|
+
localStorage.setItem(`catalyst-snippets-66-${tabId}-name`, tab.name);
|
|
171
|
+
localStorage.setItem(`catalyst-snippets-66-${tabId}-language`, tab.language);
|
|
172
|
+
setTabs((prev) => prev.map((t) => (t.id === tabId ? {
|
|
173
|
+
...t,
|
|
174
|
+
isDirty: false,
|
|
175
|
+
name: activeTab?.label.length > 0 ? `${activeTab?.label}.${getFileExtension(t.language)}` : t.name,
|
|
176
|
+
} : t)));
|
|
177
|
+
setLastSaved(new Date());
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error("Save failed:", error);
|
|
180
|
+
} finally {
|
|
181
|
+
setIsSaving(false);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
[tabs, autoSaveEnabled, activeTab?.label]
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
const setSetState = (setting, value) => {
|
|
189
|
+
setEs(prev => ({
|
|
190
|
+
...prev,
|
|
191
|
+
[setting]: typeof value === 'function' ? value(prev[setting]) : value
|
|
192
|
+
}))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const setNestedSetting = (path, value) => {
|
|
196
|
+
const keys = path.split('.');
|
|
197
|
+
setEs(prev => {
|
|
198
|
+
const newState = { ...prev };
|
|
199
|
+
let current = newState;
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
202
|
+
current[keys[i]] = { ...current[keys[i]] };
|
|
203
|
+
current = current[keys[i]];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
current[keys[keys.length - 1]] = value;
|
|
207
|
+
return newState;
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
212
|
+
const editorRef = useRef(null);
|
|
213
|
+
const saveTimeoutRef = useRef(null);
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
setIsClient(true);
|
|
217
|
+
}, []);
|
|
218
|
+
|
|
219
|
+
// Debounced auto-save
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (activeTab?.isDirty && autoSaveEnabled) {
|
|
222
|
+
clearTimeout(saveTimeoutRef.current);
|
|
223
|
+
saveTimeoutRef.current = setTimeout(() => {
|
|
224
|
+
autoSave(activeTabId, activeTab.content);
|
|
225
|
+
}, 2000);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return () => clearTimeout(saveTimeoutRef.current);
|
|
229
|
+
}, [activeTab?.content, activeTab?.isDirty, activeTabId, autoSave, autoSaveEnabled]);
|
|
230
|
+
|
|
231
|
+
const renameTab = (tabId, newName) => {
|
|
232
|
+
setTabs((prev) => prev.map((tab) => (tab.id === tabId ? { ...tab, name: newName, isDirty: true } : tab)));
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const getFileExtension = (language) => {
|
|
236
|
+
const extensions = {
|
|
237
|
+
javascript: "js",
|
|
238
|
+
typescript: "ts",
|
|
239
|
+
html: "html",
|
|
240
|
+
css: "css",
|
|
241
|
+
scss: "scss",
|
|
242
|
+
json: "json",
|
|
243
|
+
python: "py",
|
|
244
|
+
sql: "sql",
|
|
245
|
+
yaml: "yml",
|
|
246
|
+
xml: "xml",
|
|
247
|
+
java: "java",
|
|
248
|
+
csharp: "cs",
|
|
249
|
+
cpp: "cpp",
|
|
250
|
+
php: "php",
|
|
251
|
+
rust: "rs",
|
|
252
|
+
go: "go",
|
|
253
|
+
shell: "sh",
|
|
254
|
+
markdown: "md",
|
|
255
|
+
};
|
|
256
|
+
return extensions[language] || "txt";
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const createNewTab = useCallback(
|
|
260
|
+
async (language = "typescript") => {
|
|
261
|
+
setIsEditing(true)
|
|
262
|
+
const clipboardText = await navigator.clipboard.readText();
|
|
263
|
+
|
|
264
|
+
const newTab = {
|
|
265
|
+
id: nextTabId,
|
|
266
|
+
name: `untitled-${nextTabId}.${getFileExtension(language)}`,
|
|
267
|
+
content: clipboardText ? clipboardText : '',
|
|
268
|
+
language,
|
|
269
|
+
isDirty: false,
|
|
270
|
+
isActive: true,
|
|
271
|
+
label: '',
|
|
272
|
+
prefix: '',
|
|
273
|
+
description: '',
|
|
274
|
+
scope: 'jsx / tsx',
|
|
275
|
+
folder: '',
|
|
276
|
+
global: false
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
setTabs((prev) => prev.map((tab) => ({ ...tab, isActive: false })));
|
|
280
|
+
setTabs((prev) => [...prev, newTab]);
|
|
281
|
+
setActiveTabId(nextTabId);
|
|
282
|
+
setNextTabId((prev) => prev + 1);
|
|
283
|
+
|
|
284
|
+
const extractFolders = (items) => {
|
|
285
|
+
let folders = [];
|
|
286
|
+
|
|
287
|
+
for (const item of items) {
|
|
288
|
+
if (item.type === 'folder') {
|
|
289
|
+
folders.push({
|
|
290
|
+
label: item.label,
|
|
291
|
+
expanded: item.expanded || false,
|
|
292
|
+
global: item.global || false,
|
|
293
|
+
hidden: item.hidden || false,
|
|
294
|
+
icon: item.icon || null,
|
|
295
|
+
projectId: item.projectId || '',
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (item.items) {
|
|
299
|
+
folders = [...folders, ...extractFolders(item.items)];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return folders;
|
|
305
|
+
};
|
|
306
|
+
const response = await fetch(DEVSTACK_URL + "/api/cmd/config/get");
|
|
307
|
+
const data = await response.json();
|
|
308
|
+
const allFolders = extractFolders(data.categories);
|
|
309
|
+
setFolders(allFolders);
|
|
310
|
+
},
|
|
311
|
+
[nextTabId]
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const handleSnippetSelect = useCallback(
|
|
315
|
+
(language = "typescript", snippet) => {
|
|
316
|
+
setIsEditing(true);
|
|
317
|
+
if (editorRef.current) { editorRef.current.setValue(snippet.body || ''); }
|
|
318
|
+
setCode(snippet.body || '');
|
|
319
|
+
|
|
320
|
+
const newTab = {
|
|
321
|
+
id: nextTabId,
|
|
322
|
+
name: `${snippet.label}.${getFileExtension(language)}`,
|
|
323
|
+
content: snippet.body || '',
|
|
324
|
+
language,
|
|
325
|
+
isDirty: false,
|
|
326
|
+
isActive: true,
|
|
327
|
+
label: snippet.label || '',
|
|
328
|
+
prefix: snippet.prefix || '',
|
|
329
|
+
description: snippet.description || '',
|
|
330
|
+
scope: snippet.scope || 'typescript,typescriptreact',
|
|
331
|
+
folder: '',
|
|
332
|
+
global: snippet.global || false,
|
|
333
|
+
projectId: snippet.projectId || '',
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
setTabs((prev) => prev.map((tab) => ({ ...tab, isActive: false })));
|
|
337
|
+
setTabs((prev) => [...prev, newTab]);
|
|
338
|
+
setActiveTabId(nextTabId);
|
|
339
|
+
setNextTabId((prev) => prev + 1);
|
|
340
|
+
},
|
|
341
|
+
[nextTabId]
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const closeTab = useCallback(
|
|
345
|
+
(tabId) => {
|
|
346
|
+
if (tabs.length === 1) return;
|
|
347
|
+
|
|
348
|
+
setTabs((prev) => {
|
|
349
|
+
const newTabs = prev.filter((tab) => tab.id !== tabId);
|
|
350
|
+
if (tabId === activeTabId) {
|
|
351
|
+
const nextTab = newTabs[0] || newTabs[newTabs.length - 1];
|
|
352
|
+
setActiveTabId(nextTab?.id || 1);
|
|
353
|
+
}
|
|
354
|
+
return newTabs;
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
[tabs.length, activeTabId]
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const handleDelete = useCallback(
|
|
361
|
+
async (tabId) => {
|
|
362
|
+
if (tabs.length === 1) return;
|
|
363
|
+
|
|
364
|
+
setTabs((prev) => {
|
|
365
|
+
const newTabs = prev.filter((tab) => tab.id !== tabId);
|
|
366
|
+
if (tabId === activeTabId) {
|
|
367
|
+
const nextTab = newTabs[0] || newTabs[newTabs.length - 1];
|
|
368
|
+
setActiveTabId(nextTab?.id || 1);
|
|
369
|
+
}
|
|
370
|
+
return newTabs;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Use label instead of id for deletion
|
|
374
|
+
await axios.post(DEVSTACK_URL + `/api/snippets/${activeTab?.label}/delete`);
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
closeTab(activeTabId);
|
|
378
|
+
setIsEditing(false);
|
|
379
|
+
},
|
|
380
|
+
[tabs.length, activeTabId, activeTab?.label]
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const duplicateTab = (tabId) => {
|
|
384
|
+
const tab = tabs.find((t) => t.id === tabId);
|
|
385
|
+
if (!tab) return;
|
|
386
|
+
|
|
387
|
+
const newTab = {
|
|
388
|
+
...tab,
|
|
389
|
+
id: nextTabId,
|
|
390
|
+
name: `copy-of-${tab.name}`,
|
|
391
|
+
isDirty: true,
|
|
392
|
+
};
|
|
393
|
+
setTabs((prev) => [...prev, newTab]);
|
|
394
|
+
setActiveTabId(nextTabId);
|
|
395
|
+
setNextTabId((prev) => prev + 1);
|
|
396
|
+
};
|
|
397
|
+
const handleManualSave = useCallback(async () => {
|
|
398
|
+
setIsSubmitting(true);
|
|
399
|
+
if (!validateForm()) { setIsSubmitting(false); return; }
|
|
400
|
+
if (activeTab?.isDirty) { clearTimeout(saveTimeoutRef.current); autoSave(activeTabId, activeTab.content); }
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const snippetBody = activeTab?.content?.trim() || '';
|
|
404
|
+
|
|
405
|
+
const noSpace = activeTab?.label && activeTab?.label.length > 3
|
|
406
|
+
? activeTab?.label.toLowerCase().replace(/[\s.,/]+/g, '_')
|
|
407
|
+
: '';
|
|
408
|
+
|
|
409
|
+
let snippetData;
|
|
410
|
+
if (replaceSpaces === true) {
|
|
411
|
+
snippetData = {
|
|
412
|
+
label: isEditing ? activeTab.label : noSpace,
|
|
413
|
+
prefix: noSpace,
|
|
414
|
+
description: activeTab.description.trim(),
|
|
415
|
+
scope: activeTab.scope.trim() || 'typescript,typescriptreact',
|
|
416
|
+
body: activeTab.content.trim().split('\n'),
|
|
417
|
+
target: activeTab.global ? 'global' : 'workspace',
|
|
418
|
+
editingSnippet: isEditing,
|
|
419
|
+
};
|
|
420
|
+
} else {
|
|
421
|
+
snippetData = {
|
|
422
|
+
label: isEditing ? activeTab.label : activeTab.label.trim(),
|
|
423
|
+
prefix: activeTab.prefix.trim(),
|
|
424
|
+
description: activeTab.description.trim(),
|
|
425
|
+
scope: activeTab.scope.trim() || 'typescript,typescriptreact',
|
|
426
|
+
body: activeTab.content.trim().split('\n'),
|
|
427
|
+
target: activeTab.global ? 'global' : 'workspace',
|
|
428
|
+
editingSnippet: isEditing,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await axios.post(DEVSTACK_URL + `/api/snippets/save`, snippetData);
|
|
433
|
+
|
|
434
|
+
// Update folder structure if needed
|
|
435
|
+
if (!activeTab?.folder || typeof activeTab?.folder !== 'string') {
|
|
436
|
+
setError(prev => ({
|
|
437
|
+
...prev,
|
|
438
|
+
folder: 'Folder is required',
|
|
439
|
+
}));
|
|
440
|
+
setIsSubmitting(false);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const response = await fetch(DEVSTACK_URL + "/api/cmd/config/get");
|
|
445
|
+
const currentConfig = await response.json();
|
|
446
|
+
|
|
447
|
+
const updatedConfig = addNewItem(currentConfig, activeTab?.folder, {
|
|
448
|
+
label: snippetData.label,
|
|
449
|
+
type: 'snippet',
|
|
450
|
+
prefix: snippetData.prefix,
|
|
451
|
+
description: snippetData.description,
|
|
452
|
+
scope: snippetData.scope,
|
|
453
|
+
body: snippetData.body,
|
|
454
|
+
global: activeTab?.global || false,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
await axios.post(DEVSTACK_URL + `/api/cmd/config/save`, updatedConfig);
|
|
458
|
+
|
|
459
|
+
toast.success("Success", "Snippet saved successfully!");
|
|
460
|
+
setIsSubmitting(false);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.error('Error saving snippet:', error);
|
|
463
|
+
toast.error("Error", "Failed to save snippet");
|
|
464
|
+
setIsSubmitting(false);
|
|
465
|
+
}
|
|
466
|
+
}, [activeTab, activeTabId, autoSave, replaceSpaces, isEditing]);
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
const handleEditorDidMount = (editor, monaco) => {
|
|
470
|
+
baseHandleEditorDidMount(editor, monaco, {
|
|
471
|
+
setCode,
|
|
472
|
+
editorRef,
|
|
473
|
+
customDarkTheme,
|
|
474
|
+
handleManualSave,
|
|
475
|
+
renameTab,
|
|
476
|
+
setSetState,
|
|
477
|
+
createNewTab,
|
|
478
|
+
closeTab,
|
|
479
|
+
activeTabId,
|
|
480
|
+
setOpen
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const renderEditor = () => {
|
|
485
|
+
return baseRenderEditor({
|
|
486
|
+
isClient,
|
|
487
|
+
activeTab,
|
|
488
|
+
theme,
|
|
489
|
+
handleEditorChange,
|
|
490
|
+
handleEditorDidMount,
|
|
491
|
+
es,
|
|
492
|
+
LoadingComponent: LoadingPage,
|
|
493
|
+
SpinnerComponent: <SpinnerV4 title='Loading...' subtitle='Please wait while your editor loads the content.' size='sm' />
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
const TabComponent = ({ tab, isActive, onClose, onSelect }) => (
|
|
497
|
+
<div
|
|
498
|
+
className={`flex items-center px-3 py-2 border-r border-border cursor-pointer transition-colors ${isActive ? "bg-background text-foreground border-b-2 border-primary" : "bg-muted text-muted-foreground hover:bg-background hover:text-foreground"
|
|
499
|
+
}`}
|
|
500
|
+
onClick={() => onSelect(tab.id)}
|
|
501
|
+
>
|
|
502
|
+
<span className="text-sm truncate max-w-32">{tab.name}</span>
|
|
503
|
+
{tab.isDirty && <span className="ml-1 text-destructive">●</span>}
|
|
504
|
+
<button
|
|
505
|
+
onClick={(e) => {
|
|
506
|
+
e.stopPropagation();
|
|
507
|
+
onClose(tab.id);
|
|
508
|
+
}}
|
|
509
|
+
className="ml-2 p-1 rounded hover:bg-destructive/20 hover:text-destructive"
|
|
510
|
+
>
|
|
511
|
+
<X className="h-3 w-3" />
|
|
512
|
+
</button>
|
|
513
|
+
</div>
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const closeAllTabs = () => {
|
|
517
|
+
// Note: You may want to add a prompt here to warn about unsaved changes.
|
|
518
|
+
setTabs([]);
|
|
519
|
+
createNewTab();
|
|
520
|
+
handleManualSave()
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
useEffect(() => {
|
|
524
|
+
// Reset visibility when lastSaved changes
|
|
525
|
+
setIsVisible(true);
|
|
526
|
+
|
|
527
|
+
// Set a timer to hide the status after 5 seconds
|
|
528
|
+
const timer = setTimeout(() => {
|
|
529
|
+
setIsVisible(false);
|
|
530
|
+
}, 5000);
|
|
531
|
+
|
|
532
|
+
// Cleanup function: Clear the timeout if the component unmounts or lastSaved changes
|
|
533
|
+
return () => clearTimeout(timer);
|
|
534
|
+
}, [lastSaved]);
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
const handleEditorChange = (value) => {
|
|
538
|
+
setTabs((prev) => prev.map((tab) => (tab.id === activeTabId ? { ...tab, content: value || "", isDirty: true } : tab)));
|
|
539
|
+
setCode(value);
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const handleChange = useCallback((field, value) => {
|
|
543
|
+
// Handle both event objects and direct values
|
|
544
|
+
const actualValue = value?.target ? value.target.value : value;
|
|
545
|
+
|
|
546
|
+
setTabs(prev => prev.map(tab =>
|
|
547
|
+
tab.id === activeTabId
|
|
548
|
+
? { ...tab, [field]: actualValue, isDirty: true }
|
|
549
|
+
: tab
|
|
550
|
+
));
|
|
551
|
+
|
|
552
|
+
// Clear error when user starts typing
|
|
553
|
+
if (error[field]) {
|
|
554
|
+
setError(prev => ({
|
|
555
|
+
...prev,
|
|
556
|
+
[field]: null
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
}, [error, activeTabId]);
|
|
560
|
+
|
|
561
|
+
const addNewItem = (config, targetFolder, activeTab) => {
|
|
562
|
+
const createItem = () => {
|
|
563
|
+
const { label, path, type, ...additionalProps } = activeTab;
|
|
564
|
+
|
|
565
|
+
const baseItem = {
|
|
566
|
+
label,
|
|
567
|
+
type
|
|
568
|
+
};
|
|
569
|
+
// For other items (commands, urls, etc.)
|
|
570
|
+
return {
|
|
571
|
+
...baseItem,
|
|
572
|
+
path: path || "",
|
|
573
|
+
hidden: additionalProps.hidden ?? false,
|
|
574
|
+
...additionalProps // spread any other props from activeTab
|
|
575
|
+
};
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const newItem = createItem();
|
|
579
|
+
|
|
580
|
+
const findAndAdd = (items) => {
|
|
581
|
+
return items.map(item => {
|
|
582
|
+
// If this is the target folder, add the new item to its items array
|
|
583
|
+
if (item.label === targetFolder && item.type === 'folder') {
|
|
584
|
+
return {
|
|
585
|
+
...item,
|
|
586
|
+
items: [...(item.items || []), newItem]
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// If this item has nested items, recursively search them
|
|
591
|
+
if (item.items && item.items.length > 0) {
|
|
592
|
+
return {
|
|
593
|
+
...item,
|
|
594
|
+
items: findAndAdd(item.items)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Return the item unchanged if it's not the target and has no nested items
|
|
599
|
+
return item;
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
...config,
|
|
605
|
+
categories: findAndAdd(config.categories)
|
|
606
|
+
};
|
|
607
|
+
};
|
|
608
|
+
const validateForm = () => {
|
|
609
|
+
const label = activeTab.label?.trim() || '';
|
|
610
|
+
const prefix = activeTab.prefix?.trim() || '';
|
|
611
|
+
const body = activeTab?.content?.trim() || '';
|
|
612
|
+
|
|
613
|
+
if (!label) {
|
|
614
|
+
setError(prev => ({ ...prev, label: 'Label is required' }));
|
|
615
|
+
return false;
|
|
616
|
+
} else if (label.length < 4) {
|
|
617
|
+
setError(prev => ({ ...prev, label: 'Label needs to be at least 4 chars in length' }));
|
|
618
|
+
return false;
|
|
619
|
+
} else if (!prefix && replaceSpaces === false) {
|
|
620
|
+
setError(prev => ({ ...prev, prefix: 'Prefix is required' }));
|
|
621
|
+
return false;
|
|
622
|
+
} else if (!body) {
|
|
623
|
+
setError(prev => ({ ...prev, body: 'Snippet body is required' }));
|
|
624
|
+
return false;
|
|
625
|
+
} else {
|
|
626
|
+
setError({ label: null, prefix: null, description: null, scope: null, body: null, folder: null });
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const onOpen = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
632
|
+
try {
|
|
633
|
+
const file = e.target.files?.[0];
|
|
634
|
+
if (!file) return;
|
|
635
|
+
const contents = await file.text();
|
|
636
|
+
|
|
637
|
+
setCode(contents);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
console.error("Error loading PDF:", error);
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<div className="h-[90vh] flex flex-col bg-background border-2 border-border rounded-[10px] shadow-lg !overflow-hidden z-75 ">
|
|
645
|
+
<MonacoToolbar
|
|
646
|
+
createNewTab={createNewTab}
|
|
647
|
+
closeTab={closeTab}
|
|
648
|
+
closeAllTabs={closeAllTabs}
|
|
649
|
+
activeTab={activeTab}
|
|
650
|
+
handleManualSave={handleManualSave}
|
|
651
|
+
code={code}
|
|
652
|
+
setTabs={setTabs}
|
|
653
|
+
editorRef={editorRef}
|
|
654
|
+
activeTabId={activeTabId}
|
|
655
|
+
isWhitespaceRendered={isWhitespaceRendered}
|
|
656
|
+
setIsWhitespaceRendered={setIsWhitespaceRendered}
|
|
657
|
+
areIndentGuidesRendered={areIndentGuidesRendered}
|
|
658
|
+
setAreIndentGuidesRendered={setAreIndentGuidesRendered}
|
|
659
|
+
isStickyScrollEnabled={isStickyScrollEnabled}
|
|
660
|
+
setIsStickyScrollEnabled={setIsStickyScrollEnabled}
|
|
661
|
+
isWordWrapEnabled={isWordWrapEnabled}
|
|
662
|
+
setIsWordWrapEnabled={setIsWordWrapEnabled}
|
|
663
|
+
toast={toast}
|
|
664
|
+
isFullscreen={isFullscreen}
|
|
665
|
+
setIsFullscreen={setIsFullscreen}
|
|
666
|
+
tabs={tabs}
|
|
667
|
+
lastSaved={lastSaved}
|
|
668
|
+
isVisible={isVisible}
|
|
669
|
+
fileInputRef={fileInputRef}
|
|
670
|
+
es={es}
|
|
671
|
+
setSetState={setSetState}
|
|
672
|
+
duplicateTab={duplicateTab}
|
|
673
|
+
onOpen={onOpen}
|
|
674
|
+
setEditorRenameTab={setEditorRenameTab}
|
|
675
|
+
setNestedSetting={setNestedSetting}
|
|
676
|
+
parent='snippet'
|
|
677
|
+
isEditing={isEditing}
|
|
678
|
+
/>
|
|
679
|
+
<div className="flex border-b border-border bg-background overflow-x-auto">
|
|
680
|
+
{tabs.map((tab) => (
|
|
681
|
+
<TabComponent key={tab.id} tab={tab} isActive={tab.id === activeTabId} onClose={closeTab} onSelect={setActiveTabId} />
|
|
682
|
+
))}
|
|
683
|
+
</div>
|
|
684
|
+
<div className='grid grid-cols-5' >
|
|
685
|
+
{isEditing === true ? (
|
|
686
|
+
<>
|
|
687
|
+
{/* form inputs */}
|
|
688
|
+
<div className='flex flex-col gap-3 items-center p-6'>
|
|
689
|
+
<FieldSet>
|
|
690
|
+
<FieldGroup>
|
|
691
|
+
<Field className="w-full max-w-sm">
|
|
692
|
+
<FieldLabel>Label</FieldLabel>
|
|
693
|
+
<Input value={activeTab?.label} onChange={(e) => handleChange('label', e)} placeholder="calendar with event slots" />
|
|
694
|
+
{error.label && <FieldError>{error.label}</FieldError>}
|
|
695
|
+
</Field>
|
|
696
|
+
|
|
697
|
+
<Field className="w-full max-w-sm">
|
|
698
|
+
<FieldLabel>Prefix</FieldLabel>
|
|
699
|
+
<Input value={activeTab?.prefix} onChange={(e) => handleChange('prefix', e)} placeholder="calendar_with_event_slots" />
|
|
700
|
+
<FieldDescription>To trigger the snippet</FieldDescription>
|
|
701
|
+
{error.prefix && <FieldError>{error.prefix}</FieldError>}
|
|
702
|
+
</Field>
|
|
703
|
+
|
|
704
|
+
<Field className="w-full max-w-sm">
|
|
705
|
+
<FieldLabel>Description</FieldLabel>
|
|
706
|
+
<Input value={activeTab?.description} onChange={(e) => handleChange('description', e)} placeholder="Full calendar component with slots on right side to select event" />
|
|
707
|
+
</Field>
|
|
708
|
+
|
|
709
|
+
<Field className="w-full max-w-sm">
|
|
710
|
+
<FieldLabel>Scope</FieldLabel>
|
|
711
|
+
<Input
|
|
712
|
+
value={hasEditedScope ? activeTab?.scope : (activeTab?.scope || "jsx / tsx")}
|
|
713
|
+
onChange={(e) => {
|
|
714
|
+
handleChange('scope', e);
|
|
715
|
+
setHasEditedScope(true);
|
|
716
|
+
}}
|
|
717
|
+
onFocus={(e) => {
|
|
718
|
+
if (hasEditedScope === false) {
|
|
719
|
+
handleChange('scope', '');
|
|
720
|
+
}
|
|
721
|
+
}}
|
|
722
|
+
onBlur={(e) => {
|
|
723
|
+
if (e.target.value === '' && !hasEditedScope) {
|
|
724
|
+
handleChange('scope', 'jsx / tsx');
|
|
725
|
+
}
|
|
726
|
+
}}
|
|
727
|
+
/>
|
|
728
|
+
</Field>
|
|
729
|
+
|
|
730
|
+
<Field className="w-full max-w-sm">
|
|
731
|
+
<FieldLabel>Location</FieldLabel>
|
|
732
|
+
<Select value={activeTab?.folder} onValueChange={(value) => handleChange('folder', value)}>
|
|
733
|
+
<SelectTrigger size="sm" className="justify-start capitalize shadow-none *:data-[slot=select-value]:w-12 *:data-[slot=select-value]:capitalize focus:outline-none relative z-10 prose prose-lg font-sans text-white/95 leading-relaxed w-full">
|
|
734
|
+
<SelectValue placeholder="Select a folder" />
|
|
735
|
+
</SelectTrigger>
|
|
736
|
+
<SelectContent align="end">
|
|
737
|
+
<SelectGroup>
|
|
738
|
+
<SelectItem value='root' className="capitalize data-[state=checked]:opacity-50">Root</SelectItem>
|
|
739
|
+
{folders.map((item, index) => {
|
|
740
|
+
return (
|
|
741
|
+
<SelectItem key={index} value={item.label}>
|
|
742
|
+
<p>{item.label}</p>
|
|
743
|
+
<p className='text-muted-foreground'>{item.description}</p>
|
|
744
|
+
|
|
745
|
+
</SelectItem>
|
|
746
|
+
);
|
|
747
|
+
})}
|
|
748
|
+
</SelectGroup>
|
|
749
|
+
</SelectContent>
|
|
750
|
+
</Select>
|
|
751
|
+
{error.folder && <FieldError>{error.folder}</FieldError>}
|
|
752
|
+
<Button
|
|
753
|
+
type="button"
|
|
754
|
+
onClick={handleManualSave}
|
|
755
|
+
disabled={isSubmitting}
|
|
756
|
+
className="w-full mt-2"
|
|
757
|
+
>
|
|
758
|
+
<Save className="h-4 w-4 mr-2" />
|
|
759
|
+
{isSubmitting ? 'Saving...' : 'Save'}
|
|
760
|
+
</Button>
|
|
761
|
+
|
|
762
|
+
</Field>
|
|
763
|
+
<FieldSeparator />
|
|
764
|
+
|
|
765
|
+
<Field orientation="horizontal">
|
|
766
|
+
<FieldContent>
|
|
767
|
+
<FieldLabel htmlFor="tinting">Auto Prefix</FieldLabel>
|
|
768
|
+
<FieldDescription>
|
|
769
|
+
Replace Spaces w/ Underscores - label & prefix
|
|
770
|
+
</FieldDescription>
|
|
771
|
+
</FieldContent>
|
|
772
|
+
<Switch checked={replaceSpaces} onCheckedChange={(checked) => { setReplaceSpaces(checked); }} />
|
|
773
|
+
</Field>
|
|
774
|
+
|
|
775
|
+
<Field orientation="horizontal">
|
|
776
|
+
<FieldContent>
|
|
777
|
+
<FieldLabel htmlFor="tinting">Global</FieldLabel>
|
|
778
|
+
<FieldDescription>
|
|
779
|
+
Whether setting the snippet to global or workspace
|
|
780
|
+
</FieldDescription>
|
|
781
|
+
</FieldContent>
|
|
782
|
+
<Switch checked={activeTab?.global} onCheckedChange={(e) => { handleChange('global', e) }} />
|
|
783
|
+
</Field>
|
|
784
|
+
|
|
785
|
+
</FieldGroup>
|
|
786
|
+
</FieldSet>
|
|
787
|
+
</div>
|
|
788
|
+
</>
|
|
789
|
+
) : (<>
|
|
790
|
+
{/* Search Section
|
|
791
|
+
<Command className='rounded-none h-full'>
|
|
792
|
+
<CommandInput />
|
|
793
|
+
<CommandList maxHeight='max-h-none'>
|
|
794
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
795
|
+
<CommandGroup heading="Snippets">
|
|
796
|
+
{snippets.map((snippet, index) => {
|
|
797
|
+
return (
|
|
798
|
+
<CommandItem key={index} onSelect={() => handleSnippetSelect('typescript', snippet)}>
|
|
799
|
+
{snippet.label}
|
|
800
|
+
</CommandItem>
|
|
801
|
+
);
|
|
802
|
+
})}
|
|
803
|
+
</CommandGroup>
|
|
804
|
+
</CommandList>
|
|
805
|
+
</Command>*/}
|
|
806
|
+
|
|
807
|
+
<Command className='rounded-none h-full'>
|
|
808
|
+
<CommandInput />
|
|
809
|
+
<div className="relative">
|
|
810
|
+
{/* Top fade gradient - always visible */}
|
|
811
|
+
<div className="absolute top-0 left-0 right-0 h-4 bg-gradient-to-b from-background to-transparent z-10 pointer-events-none" />
|
|
812
|
+
|
|
813
|
+
<CommandList
|
|
814
|
+
maxHeight=''
|
|
815
|
+
className="overflow-y-auto scrollbar-thin"
|
|
816
|
+
>
|
|
817
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
818
|
+
<CommandGroup heading="Snippets">
|
|
819
|
+
{snippets.map((snippet, index) => (
|
|
820
|
+
<CommandItem
|
|
821
|
+
key={index}
|
|
822
|
+
onSelect={() => handleSnippetSelect('typescript', snippet)}
|
|
823
|
+
className={cn(activeTab.label === snippet.label ? 'border-primary' : null)}
|
|
824
|
+
>
|
|
825
|
+
{snippet.label}
|
|
826
|
+
</CommandItem>
|
|
827
|
+
))}
|
|
828
|
+
</CommandGroup>
|
|
829
|
+
</CommandList>
|
|
830
|
+
|
|
831
|
+
{/* Bottom fade gradient - always visible */}
|
|
832
|
+
<div className="absolute bottom-0 left-0 right-0 h-4 bg-gradient-to-t from-background to-transparent z-10 pointer-events-none" />
|
|
833
|
+
</div>
|
|
834
|
+
</Command>
|
|
835
|
+
</>)
|
|
836
|
+
}
|
|
837
|
+
<div className="flex-1 flex min-h-0 col-span-4">
|
|
838
|
+
<div className="flex-1">{renderEditor()}</div>
|
|
839
|
+
</div>
|
|
840
|
+
</div >
|
|
841
|
+
</div >
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|