@myrialabs/clopen 0.1.6 → 0.1.8
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 +7 -1
- package/backend/lib/git/git-service.ts +1 -0
- package/bin/clopen.ts +89 -0
- package/bun.lock +38 -214
- package/frontend/lib/components/common/MonacoEditor.svelte +6 -6
- package/frontend/lib/components/common/xterm/XTerm.svelte +27 -108
- package/frontend/lib/components/common/xterm/terminal-config.ts +2 -2
- package/frontend/lib/components/common/xterm/types.ts +1 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +69 -20
- package/frontend/lib/components/files/FileTree.svelte +34 -25
- package/frontend/lib/components/files/FileViewer.svelte +45 -101
- package/frontend/lib/components/git/CommitForm.svelte +1 -1
- package/frontend/lib/components/git/GitLog.svelte +117 -91
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +3 -3
- package/frontend/lib/components/workspace/PanelHeader.svelte +639 -623
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +34 -92
- package/frontend/lib/stores/ui/workspace.svelte.ts +14 -14
- package/package.json +9 -15
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
indentGuide: '#21262d',
|
|
59
59
|
indentGuideActive: '#30363d',
|
|
60
60
|
ruler: '#21262d',
|
|
61
|
-
scrollbar: '#
|
|
62
|
-
scrollbarHover: '#
|
|
63
|
-
scrollbarActive: '#
|
|
61
|
+
scrollbar: '#6e768140',
|
|
62
|
+
scrollbarHover: '#6e768180',
|
|
63
|
+
scrollbarActive: '#8b949e'
|
|
64
64
|
},
|
|
65
65
|
tokens: {
|
|
66
66
|
comment: '6A9955',
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
indentGuide: '#e3e3e3',
|
|
88
88
|
indentGuideActive: '#d3d3d3',
|
|
89
89
|
ruler: '#e3e3e3',
|
|
90
|
-
scrollbar: '#
|
|
91
|
-
scrollbarHover: '#
|
|
92
|
-
scrollbarActive: '#
|
|
90
|
+
scrollbar: '#92929240',
|
|
91
|
+
scrollbarHover: '#92929280',
|
|
92
|
+
scrollbarActive: '#555555'
|
|
93
93
|
},
|
|
94
94
|
tokens: {
|
|
95
95
|
comment: '008000',
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { settings } from '$frontend/lib/stores/features/settings.svelte';
|
|
15
15
|
|
|
16
16
|
// Import CSS directly - Vite will handle it properly
|
|
17
|
-
import 'xterm/css/xterm.css';
|
|
17
|
+
import '@xterm/xterm/css/xterm.css';
|
|
18
18
|
|
|
19
19
|
// Props
|
|
20
20
|
const {
|
|
@@ -183,127 +183,39 @@
|
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
// Handle right-click copy/paste
|
|
187
|
-
function
|
|
186
|
+
// Handle right-click copy/paste via clipboard addon
|
|
187
|
+
function setupClipboardHandling() {
|
|
188
188
|
if (!terminalContainer || !xtermService.terminal) return;
|
|
189
189
|
|
|
190
|
-
const
|
|
191
|
-
event.preventDefault();
|
|
192
|
-
|
|
193
|
-
// Get selected text from xterm.js
|
|
190
|
+
const handleContextMenu = async (event: MouseEvent) => {
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
|
|
194
193
|
const selectedText = xtermService.getSelectedText();
|
|
195
|
-
|
|
196
|
-
if (selectedText
|
|
194
|
+
|
|
195
|
+
if (selectedText?.trim()) {
|
|
197
196
|
// Copy selected text to clipboard
|
|
198
197
|
try {
|
|
199
198
|
await navigator.clipboard.writeText(selectedText);
|
|
200
|
-
|
|
201
|
-
// Clear selection after copy (like most terminals do)
|
|
202
199
|
xtermService.clearSelection();
|
|
203
|
-
|
|
204
|
-
// Show brief visual feedback
|
|
205
|
-
showCopyFeedback();
|
|
206
|
-
} catch (err) {
|
|
207
|
-
}
|
|
200
|
+
} catch { /* clipboard not available */ }
|
|
208
201
|
} else {
|
|
209
|
-
// No text selected - paste from clipboard
|
|
202
|
+
// No text selected - paste from clipboard
|
|
210
203
|
try {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
// Use terminal's built-in paste functionality
|
|
215
|
-
// This simulates typing each character through the input handler
|
|
216
|
-
if ((xtermService as any).inputHandler) {
|
|
217
|
-
// Process each character through the input handler
|
|
218
|
-
for (const char of clipboardText) {
|
|
219
|
-
// Skip newlines - let user decide when to execute
|
|
220
|
-
if (char !== '\n' && char !== '\r') {
|
|
221
|
-
(xtermService as any).inputHandler(char);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Show brief visual feedback
|
|
227
|
-
showPasteFeedback();
|
|
204
|
+
const text = await navigator.clipboard.readText();
|
|
205
|
+
if (text?.trim()) {
|
|
206
|
+
xtermService.pasteText(text);
|
|
228
207
|
}
|
|
229
|
-
} catch {
|
|
230
|
-
// paste not supported
|
|
231
|
-
}
|
|
208
|
+
} catch { /* clipboard not available */ }
|
|
232
209
|
}
|
|
233
210
|
};
|
|
234
211
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
212
|
+
terminalContainer.addEventListener('contextmenu', handleContextMenu);
|
|
213
|
+
|
|
238
214
|
return () => {
|
|
239
|
-
terminalContainer?.removeEventListener('contextmenu',
|
|
215
|
+
terminalContainer?.removeEventListener('contextmenu', handleContextMenu);
|
|
240
216
|
};
|
|
241
217
|
}
|
|
242
218
|
|
|
243
|
-
// Show brief visual feedback for copy action
|
|
244
|
-
function showCopyFeedback() {
|
|
245
|
-
if (!terminalContainer) return;
|
|
246
|
-
|
|
247
|
-
// Create temporary feedback element
|
|
248
|
-
const feedback = document.createElement('div');
|
|
249
|
-
feedback.textContent = 'Copied!';
|
|
250
|
-
feedback.style.cssText = `
|
|
251
|
-
position: absolute;
|
|
252
|
-
top: 10px;
|
|
253
|
-
right: 10px;
|
|
254
|
-
background: rgb(34 197 94 / 0.9);
|
|
255
|
-
color: white;
|
|
256
|
-
padding: 4px 8px;
|
|
257
|
-
border-radius: 4px;
|
|
258
|
-
font-size: 12px;
|
|
259
|
-
font-family: system-ui, sans-serif;
|
|
260
|
-
z-index: 1000;
|
|
261
|
-
pointer-events: none;
|
|
262
|
-
`;
|
|
263
|
-
|
|
264
|
-
terminalContainer.style.position = 'relative';
|
|
265
|
-
terminalContainer.appendChild(feedback);
|
|
266
|
-
|
|
267
|
-
// Remove feedback after 1 second
|
|
268
|
-
setTimeout(() => {
|
|
269
|
-
if (feedback.parentNode) {
|
|
270
|
-
feedback.parentNode.removeChild(feedback);
|
|
271
|
-
}
|
|
272
|
-
}, 1000);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Show brief visual feedback for paste action
|
|
276
|
-
function showPasteFeedback() {
|
|
277
|
-
if (!terminalContainer) return;
|
|
278
|
-
|
|
279
|
-
// Create temporary feedback element
|
|
280
|
-
const feedback = document.createElement('div');
|
|
281
|
-
feedback.textContent = 'Pasted!';
|
|
282
|
-
feedback.style.cssText = `
|
|
283
|
-
position: absolute;
|
|
284
|
-
top: 10px;
|
|
285
|
-
right: 10px;
|
|
286
|
-
background: rgba(59, 130, 246, 0.9);
|
|
287
|
-
color: white;
|
|
288
|
-
padding: 4px 8px;
|
|
289
|
-
border-radius: 4px;
|
|
290
|
-
font-size: 12px;
|
|
291
|
-
font-family: system-ui, sans-serif;
|
|
292
|
-
z-index: 1000;
|
|
293
|
-
pointer-events: none;
|
|
294
|
-
`;
|
|
295
|
-
|
|
296
|
-
terminalContainer.style.position = 'relative';
|
|
297
|
-
terminalContainer.appendChild(feedback);
|
|
298
|
-
|
|
299
|
-
// Remove feedback after 1 second
|
|
300
|
-
setTimeout(() => {
|
|
301
|
-
if (feedback.parentNode) {
|
|
302
|
-
feedback.parentNode.removeChild(feedback);
|
|
303
|
-
}
|
|
304
|
-
}, 1000);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
219
|
// Handle theme changes
|
|
308
220
|
function setupThemeHandling() {
|
|
309
221
|
xtermService.updateTheme();
|
|
@@ -404,7 +316,9 @@
|
|
|
404
316
|
$effect(() => {
|
|
405
317
|
const size = settings.fontSize;
|
|
406
318
|
if (isInitialized) {
|
|
407
|
-
|
|
319
|
+
const fontSize = Math.round(size * 0.9);
|
|
320
|
+
const lineHeight = Math.round(size * 0.9);
|
|
321
|
+
xtermService.updateFontSize(fontSize, lineHeight, session?.id);
|
|
408
322
|
}
|
|
409
323
|
});
|
|
410
324
|
|
|
@@ -656,12 +570,12 @@
|
|
|
656
570
|
|
|
657
571
|
const cleanupResize = setupResizeHandling();
|
|
658
572
|
const cleanupTheme = setupThemeHandling();
|
|
659
|
-
const
|
|
573
|
+
const cleanupClipboard = setupClipboardHandling();
|
|
660
574
|
|
|
661
575
|
return () => {
|
|
662
576
|
cleanupResize();
|
|
663
577
|
cleanupTheme();
|
|
664
|
-
|
|
578
|
+
cleanupClipboard?.();
|
|
665
579
|
};
|
|
666
580
|
});
|
|
667
581
|
|
|
@@ -711,6 +625,11 @@
|
|
|
711
625
|
export function clearSelection() {
|
|
712
626
|
xtermService.clearSelection();
|
|
713
627
|
}
|
|
628
|
+
|
|
629
|
+
export function pasteText(text: string) {
|
|
630
|
+
xtermService.pasteText(text);
|
|
631
|
+
}
|
|
632
|
+
|
|
714
633
|
</script>
|
|
715
634
|
|
|
716
635
|
<!-- Pure xterm.js terminal container -->
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Centralized xterm.js configuration and utilities
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ITerminalOptions } from 'xterm';
|
|
7
|
+
import type { ITerminalOptions } from '@xterm/xterm';
|
|
8
8
|
|
|
9
9
|
// Terminal theme configuration
|
|
10
10
|
export const terminalConfig: ITerminalOptions = {
|
|
@@ -40,7 +40,7 @@ export const terminalConfig: ITerminalOptions = {
|
|
|
40
40
|
convertEol: true,
|
|
41
41
|
scrollback: 1000,
|
|
42
42
|
tabStopWidth: 4,
|
|
43
|
-
allowProposedApi:
|
|
43
|
+
allowProposedApi: true,
|
|
44
44
|
altClickMovesCursor: true,
|
|
45
45
|
disableStdin: false, // ✅ ENABLED for interactive PTY mode - stdin forwards to backend
|
|
46
46
|
allowTransparency: false
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { browser } from '$frontend/lib/app-environment';
|
|
9
|
-
import type { Terminal } from 'xterm';
|
|
9
|
+
import type { Terminal } from '@xterm/xterm';
|
|
10
10
|
import type { FitAddon } from '@xterm/addon-fit';
|
|
11
11
|
import type { WebLinksAddon } from '@xterm/addon-web-links';
|
|
12
|
+
import type { ClipboardAddon } from '@xterm/addon-clipboard';
|
|
13
|
+
import type { Unicode11Addon } from '@xterm/addon-unicode11';
|
|
14
|
+
import type { LigaturesAddon } from '@xterm/addon-ligatures';
|
|
12
15
|
import type { TerminalLine } from '$shared/types/terminal';
|
|
13
16
|
import { terminalConfig } from './terminal-config';
|
|
14
17
|
import { debug } from '$shared/utils/logger';
|
|
@@ -18,12 +21,16 @@ export class XTermService {
|
|
|
18
21
|
public terminal: Terminal | null = null;
|
|
19
22
|
public fitAddon: FitAddon | null = null;
|
|
20
23
|
public webLinksAddon: WebLinksAddon | null = null;
|
|
24
|
+
public clipboardAddon: ClipboardAddon | null = null;
|
|
25
|
+
private unicode11Addon: Unicode11Addon | null = null;
|
|
26
|
+
private ligaturesAddon: LigaturesAddon | null = null;
|
|
21
27
|
public isInitialized = false;
|
|
22
28
|
public isReady = false;
|
|
23
29
|
|
|
24
30
|
private sessionId: string | null = null;
|
|
25
31
|
private resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
26
32
|
private inputDisposable: any = null;
|
|
33
|
+
private lastSentDims: { cols: number; rows: number } | null = null;
|
|
27
34
|
|
|
28
35
|
constructor() {
|
|
29
36
|
// Service is stateless by design
|
|
@@ -49,27 +56,39 @@ export class XTermService {
|
|
|
49
56
|
this.terminal = null;
|
|
50
57
|
this.fitAddon = null;
|
|
51
58
|
this.webLinksAddon = null;
|
|
59
|
+
this.clipboardAddon = null;
|
|
60
|
+
this.unicode11Addon = null;
|
|
61
|
+
this.ligaturesAddon = null;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
try {
|
|
55
65
|
debug.log('terminal', '🚀 Initializing XTerm...');
|
|
56
66
|
|
|
57
67
|
// Dynamic import xterm classes
|
|
58
|
-
const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
|
|
59
|
-
import('xterm'),
|
|
68
|
+
const [{ Terminal }, { FitAddon }, { WebLinksAddon }, { ClipboardAddon }, { Unicode11Addon }] = await Promise.all([
|
|
69
|
+
import('@xterm/xterm'),
|
|
60
70
|
import('@xterm/addon-fit'),
|
|
61
|
-
import('@xterm/addon-web-links')
|
|
71
|
+
import('@xterm/addon-web-links'),
|
|
72
|
+
import('@xterm/addon-clipboard'),
|
|
73
|
+
import('@xterm/addon-unicode11')
|
|
62
74
|
]);
|
|
63
75
|
|
|
64
76
|
// Create terminal instance
|
|
65
77
|
this.terminal = new Terminal(terminalConfig);
|
|
66
78
|
|
|
67
|
-
// Create and load addons
|
|
79
|
+
// Create and load core addons
|
|
68
80
|
this.fitAddon = new FitAddon();
|
|
69
81
|
this.webLinksAddon = new WebLinksAddon();
|
|
82
|
+
this.clipboardAddon = new ClipboardAddon();
|
|
83
|
+
this.unicode11Addon = new Unicode11Addon();
|
|
70
84
|
|
|
71
85
|
this.terminal.loadAddon(this.fitAddon);
|
|
72
86
|
this.terminal.loadAddon(this.webLinksAddon);
|
|
87
|
+
this.terminal.loadAddon(this.clipboardAddon);
|
|
88
|
+
this.terminal.loadAddon(this.unicode11Addon);
|
|
89
|
+
|
|
90
|
+
// Enable Unicode 11 for better character width support
|
|
91
|
+
this.terminal.unicode.activeVersion = '11';
|
|
73
92
|
|
|
74
93
|
// Open terminal in container
|
|
75
94
|
this.terminal.open(container);
|
|
@@ -82,6 +101,16 @@ export class XTermService {
|
|
|
82
101
|
debug.log('terminal', '⚠️ Initial fit failed (container may have zero dimensions), will retry on resize');
|
|
83
102
|
}
|
|
84
103
|
|
|
104
|
+
// Try ligatures addon for font ligature rendering (non-critical)
|
|
105
|
+
try {
|
|
106
|
+
const { LigaturesAddon } = await import('@xterm/addon-ligatures');
|
|
107
|
+
this.ligaturesAddon = new LigaturesAddon();
|
|
108
|
+
this.terminal.loadAddon(this.ligaturesAddon);
|
|
109
|
+
debug.log('terminal', '🔤 Font ligatures enabled');
|
|
110
|
+
} catch {
|
|
111
|
+
debug.log('terminal', '⚠️ Ligatures addon not available');
|
|
112
|
+
}
|
|
113
|
+
|
|
85
114
|
this.isInitialized = true;
|
|
86
115
|
this.isReady = true;
|
|
87
116
|
|
|
@@ -94,6 +123,9 @@ export class XTermService {
|
|
|
94
123
|
}
|
|
95
124
|
this.fitAddon = null;
|
|
96
125
|
this.webLinksAddon = null;
|
|
126
|
+
this.clipboardAddon = null;
|
|
127
|
+
this.unicode11Addon = null;
|
|
128
|
+
this.ligaturesAddon = null;
|
|
97
129
|
debug.error('terminal', '❌ Failed to initialize XTerm:', error);
|
|
98
130
|
}
|
|
99
131
|
}
|
|
@@ -180,11 +212,12 @@ export class XTermService {
|
|
|
180
212
|
}
|
|
181
213
|
|
|
182
214
|
/**
|
|
183
|
-
* Update terminal font size and refit to container
|
|
215
|
+
* Update terminal font size, line height, and refit to container
|
|
184
216
|
*/
|
|
185
|
-
updateFontSize(size: number, sessionId?: string): void {
|
|
217
|
+
updateFontSize(size: number, lineHeight: number, sessionId?: string): void {
|
|
186
218
|
if (!this.terminal) return;
|
|
187
219
|
this.terminal.options.fontSize = size;
|
|
220
|
+
this.terminal.options.lineHeight = lineHeight / size;
|
|
188
221
|
this.fit(sessionId);
|
|
189
222
|
}
|
|
190
223
|
|
|
@@ -262,6 +295,12 @@ export class XTermService {
|
|
|
262
295
|
const dims = this.fitAddon.proposeDimensions();
|
|
263
296
|
|
|
264
297
|
if (dims && sessionId) {
|
|
298
|
+
// Skip if dimensions haven't changed
|
|
299
|
+
if (this.lastSentDims && this.lastSentDims.cols === dims.cols && this.lastSentDims.rows === dims.rows) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this.lastSentDims = { cols: dims.cols, rows: dims.rows };
|
|
303
|
+
|
|
265
304
|
// Notify backend of new terminal size via WebSocket HTTP
|
|
266
305
|
debug.log('terminal', `🔧 Syncing terminal size: ${dims.cols}x${dims.rows}`);
|
|
267
306
|
ws.http('terminal:resize', {
|
|
@@ -293,20 +332,11 @@ export class XTermService {
|
|
|
293
332
|
scrollToBottomIfNearEnd(): void {
|
|
294
333
|
if (!this.terminal) return;
|
|
295
334
|
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
this.scrollToBottom();
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const scrollTop = viewport._viewportElement.scrollTop;
|
|
303
|
-
const scrollHeight = viewport._viewportElement.scrollHeight;
|
|
304
|
-
const clientHeight = viewport._viewportElement.clientHeight;
|
|
335
|
+
const buffer = this.terminal.buffer.active;
|
|
336
|
+
const isNearBottom = buffer.viewportY >= buffer.baseY - 3;
|
|
305
337
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (scrollHeight - scrollTop - clientHeight < threshold) {
|
|
309
|
-
this.scrollToBottom();
|
|
338
|
+
if (isNearBottom) {
|
|
339
|
+
this.terminal.scrollToBottom();
|
|
310
340
|
}
|
|
311
341
|
}
|
|
312
342
|
|
|
@@ -361,6 +391,21 @@ export class XTermService {
|
|
|
361
391
|
};
|
|
362
392
|
}
|
|
363
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Paste text by sending to PTY via WebSocket
|
|
396
|
+
*/
|
|
397
|
+
pasteText(text: string): void {
|
|
398
|
+
if (!this.sessionId || !text) return;
|
|
399
|
+
try {
|
|
400
|
+
ws.emit('terminal:input', {
|
|
401
|
+
sessionId: this.sessionId,
|
|
402
|
+
data: text
|
|
403
|
+
});
|
|
404
|
+
} catch (error) {
|
|
405
|
+
debug.error('terminal', '❌ Error pasting text:', error);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
364
409
|
/**
|
|
365
410
|
* Cleanup terminal resources
|
|
366
411
|
*/
|
|
@@ -382,6 +427,10 @@ export class XTermService {
|
|
|
382
427
|
|
|
383
428
|
this.fitAddon = null;
|
|
384
429
|
this.webLinksAddon = null;
|
|
430
|
+
this.clipboardAddon = null;
|
|
431
|
+
this.unicode11Addon = null;
|
|
432
|
+
this.ligaturesAddon = null;
|
|
433
|
+
this.lastSentDims = null;
|
|
385
434
|
this.isInitialized = false;
|
|
386
435
|
this.isReady = false;
|
|
387
436
|
}
|
|
@@ -222,19 +222,14 @@
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
performSearch();
|
|
234
|
-
}
|
|
235
|
-
}, 100);
|
|
236
|
-
}
|
|
237
|
-
// Don't clear search state on close - preserve inputs
|
|
225
|
+
function switchToSearch() {
|
|
226
|
+
searchVisible = true;
|
|
227
|
+
setTimeout(() => {
|
|
228
|
+
searchInputRef?.focus();
|
|
229
|
+
if (searchQuery.trim()) {
|
|
230
|
+
performSearch();
|
|
231
|
+
}
|
|
232
|
+
}, 100);
|
|
238
233
|
}
|
|
239
234
|
|
|
240
235
|
// Search functions
|
|
@@ -425,9 +420,9 @@
|
|
|
425
420
|
|
|
426
421
|
<div class="relative flex flex-col h-full overflow-hidden">
|
|
427
422
|
<!-- Modern Header -->
|
|
428
|
-
<div class="px-5 py-
|
|
423
|
+
<div class="px-5 py-2.5 border-b border-slate-200 dark:border-slate-700">
|
|
429
424
|
<div class="flex items-start justify-between gap-2">
|
|
430
|
-
<div class="flex-1 min-w-0">
|
|
425
|
+
<div class="flex-1 min-w-0" title={projectState.currentProject?.path}>
|
|
431
426
|
<h3 class="text-sm font-bold text-slate-900 dark:text-slate-100">
|
|
432
427
|
{projectState.currentProject?.name}
|
|
433
428
|
</h3>
|
|
@@ -454,15 +449,7 @@
|
|
|
454
449
|
<Icon name="lucide:folder-plus" class="w-4 h-4" />
|
|
455
450
|
</button>
|
|
456
451
|
{/if}
|
|
457
|
-
|
|
458
|
-
<button
|
|
459
|
-
class="flex flex-shrink-0 p-1.5 rounded-md transition-colors {searchVisible ? 'text-violet-600 dark:text-violet-400 bg-violet-50 dark:bg-violet-900/30' : 'text-slate-600 dark:text-slate-400 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/30'}"
|
|
460
|
-
onclick={toggleSearch}
|
|
461
|
-
title="Search"
|
|
462
|
-
>
|
|
463
|
-
<Icon name="lucide:search" class="w-4 h-4" />
|
|
464
|
-
</button>
|
|
465
|
-
{#if hasClipboard && onPasteToRoot}
|
|
452
|
+
{#if hasClipboard && onPasteToRoot}
|
|
466
453
|
<button
|
|
467
454
|
class="flex flex-shrink-0 p-1.5 text-slate-600 dark:text-slate-400 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/30 rounded-md transition-colors"
|
|
468
455
|
onclick={onPasteToRoot}
|
|
@@ -475,7 +462,29 @@
|
|
|
475
462
|
</div>
|
|
476
463
|
</div>
|
|
477
464
|
|
|
478
|
-
<!--
|
|
465
|
+
<!-- Tab Navigation -->
|
|
466
|
+
<div class="relative flex border-b border-slate-200 dark:border-slate-700">
|
|
467
|
+
<button
|
|
468
|
+
onclick={() => { searchVisible = false; }}
|
|
469
|
+
class="relative flex-1 flex items-center justify-center px-3 py-2 text-xs font-medium transition-colors {!searchVisible ? 'text-violet-600 dark:text-violet-400' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'}"
|
|
470
|
+
>
|
|
471
|
+
Explorer
|
|
472
|
+
{#if !searchVisible}
|
|
473
|
+
<span class="absolute bottom-0 inset-x-0 h-px bg-violet-600 dark:bg-violet-400"></span>
|
|
474
|
+
{/if}
|
|
475
|
+
</button>
|
|
476
|
+
<button
|
|
477
|
+
onclick={switchToSearch}
|
|
478
|
+
class="relative flex-1 flex items-center justify-center px-3 py-2 text-xs font-medium transition-colors {searchVisible ? 'text-violet-600 dark:text-violet-400' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'}"
|
|
479
|
+
>
|
|
480
|
+
Search
|
|
481
|
+
{#if searchVisible}
|
|
482
|
+
<span class="absolute bottom-0 inset-x-0 h-px bg-violet-600 dark:bg-violet-400"></span>
|
|
483
|
+
{/if}
|
|
484
|
+
</button>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<!-- Search Bar -->
|
|
479
488
|
{#if searchVisible}
|
|
480
489
|
<div class="px-3 py-2 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
|
|
481
490
|
<!-- Search Input -->
|