@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.
@@ -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
+