dsh-explorer 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +817 -2772
- package/lib/index.js +124 -17
- package/package.json +1 -2
- package/src/client/ExplorerView.tsx +494 -652
- package/src/client/{ExplorerContextMenu.tsx → components/ExplorerContextMenu.tsx} +2 -4
- package/src/client/components/ExplorerDeleteModal.tsx +124 -0
- package/src/client/components/ExplorerEmptyContextMenu.tsx +125 -0
- package/src/client/{ExplorerHeader.tsx → components/ExplorerHeader.tsx} +18 -12
- package/src/client/{ExplorerOptionsModal.tsx → components/ExplorerOptionsModal.tsx} +2 -15
- package/src/client/{ExplorerRow.tsx → components/ExplorerRow.tsx} +45 -27
- package/src/client/{ExplorerSearch.tsx → components/ExplorerSearch.tsx} +18 -5
- package/src/client/components/TreeNode.tsx +384 -0
- package/src/client/explorer.module.css +215 -10
- package/src/client/hooks/useKeyboardNav.ts +89 -0
- package/src/client/hooks/useLiveSync.ts +81 -0
- package/src/client/index.tsx +82 -4
- package/src/client/{api.ts → utils/api.ts} +133 -3
- package/src/client/{conversation-draft.ts → utils/conversation-draft.ts} +38 -29
- package/src/client/utils/open-tab.ts +69 -0
- package/src/fs-search.ts +116 -0
- package/src/git-status.ts +19 -13
- package/src/index.ts +12 -0
- /package/src/client/{drag-drop.ts → utils/drag-drop.ts} +0 -0
- /package/src/client/{options.ts → utils/options.ts} +0 -0
- /package/src/client/{vscode-theme.tsx → utils/vscode-theme.tsx} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react'
|
|
2
|
-
import css from '
|
|
3
|
-
import type { FsEntry } from '
|
|
2
|
+
import css from '../explorer.module.css'
|
|
3
|
+
import type { FsEntry } from '../utils/api.ts'
|
|
4
4
|
|
|
5
5
|
interface ContextMenuProps {
|
|
6
6
|
x: number
|
|
@@ -47,7 +47,6 @@ export const ExplorerContextMenu: React.FC<ContextMenuProps> = ({
|
|
|
47
47
|
return () => window.removeEventListener('mousedown', handleOutsideClick)
|
|
48
48
|
}, [onClose])
|
|
49
49
|
|
|
50
|
-
// Adjust menu bounds if overflowing screen
|
|
51
50
|
const top = Math.min(y, window.innerHeight - 260)
|
|
52
51
|
const left = Math.min(x, window.innerWidth - 200)
|
|
53
52
|
|
|
@@ -156,4 +155,3 @@ export const ExplorerContextMenu: React.FC<ContextMenuProps> = ({
|
|
|
156
155
|
</div>
|
|
157
156
|
)
|
|
158
157
|
}
|
|
159
|
-
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import css from '../explorer.module.css'
|
|
3
|
+
import type { FsEntry } from '../utils/api.ts'
|
|
4
|
+
import { renderFileIcon } from '../utils/vscode-theme.tsx'
|
|
5
|
+
|
|
6
|
+
interface ExplorerDeleteModalProps {
|
|
7
|
+
entry: FsEntry
|
|
8
|
+
relativePath: string
|
|
9
|
+
onConfirm: () => Promise<void>
|
|
10
|
+
onClose: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ExplorerDeleteModal: React.FC<ExplorerDeleteModalProps> = ({
|
|
14
|
+
entry,
|
|
15
|
+
relativePath,
|
|
16
|
+
onConfirm,
|
|
17
|
+
onClose,
|
|
18
|
+
}) => {
|
|
19
|
+
const [loading, setLoading] = useState(false)
|
|
20
|
+
const [error, setError] = useState<string | null>(null)
|
|
21
|
+
const cancelBtnRef = useRef<HTMLButtonElement>(null)
|
|
22
|
+
const modalRef = useRef<HTMLDivElement>(null)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
cancelBtnRef.current?.focus()
|
|
26
|
+
|
|
27
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
28
|
+
if (e.key === 'Escape') {
|
|
29
|
+
e.preventDefault()
|
|
30
|
+
onClose()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
35
|
+
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
|
36
|
+
onClose()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
41
|
+
document.addEventListener('mousedown', handleMouseDown)
|
|
42
|
+
return () => {
|
|
43
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
44
|
+
document.removeEventListener('mousedown', handleMouseDown)
|
|
45
|
+
}
|
|
46
|
+
}, [onClose])
|
|
47
|
+
|
|
48
|
+
const handleDelete = async () => {
|
|
49
|
+
if (loading) return
|
|
50
|
+
setLoading(true)
|
|
51
|
+
setError(null)
|
|
52
|
+
try {
|
|
53
|
+
await onConfirm()
|
|
54
|
+
onClose()
|
|
55
|
+
} catch (err: any) {
|
|
56
|
+
setError(err?.message || 'Failed to delete')
|
|
57
|
+
setLoading(false)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={css.modalOverlay}>
|
|
63
|
+
<div ref={modalRef} className={css.modalCard} role="dialog" aria-modal="true">
|
|
64
|
+
<div className={css.modalHeader}>
|
|
65
|
+
<div className={css.modalTitleWrap}>
|
|
66
|
+
<svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor">
|
|
67
|
+
<path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H14a.5.5 0 0 1 0 1h-.6l-.8 11.2A2 2 0 0 1 10.6 16H5.4a2 2 0 0 1-2-1.8L2.6 3.5H2a.5.5 0 0 1 0-1h4v-1a.5.5 0 0 1 .5-.5zM7 2v.5h2V2H7zm-2.4 12.4a1 1 0 0 0 1 .6h5.2a1 1 0 0 0 1-.9L12.4 3.5H3.6l.8 10.9h.2zm2.9-9.9a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5z" />
|
|
68
|
+
</svg>
|
|
69
|
+
<span>{entry.isDir ? 'Delete Folder' : 'Delete File'}</span>
|
|
70
|
+
</div>
|
|
71
|
+
<button className={css.optionsIconBtn} title="Close" onClick={onClose}>
|
|
72
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
73
|
+
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
|
74
|
+
</svg>
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className={css.modalBody}>
|
|
79
|
+
<div>
|
|
80
|
+
Are you sure you want to permanently delete this {entry.isDir ? 'directory' : 'file'}?
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div className={css.modalItemPreview}>
|
|
84
|
+
<span className={css.iconWrap}>
|
|
85
|
+
{renderFileIcon(entry.name, entry.isDir, false, 16)}
|
|
86
|
+
</span>
|
|
87
|
+
<div className={css.modalItemInfo}>
|
|
88
|
+
<div className={css.modalItemName}>{entry.name}</div>
|
|
89
|
+
<div className={css.modalItemPath}>{relativePath}</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{entry.isDir && (
|
|
94
|
+
<div className={css.modalWarning}>
|
|
95
|
+
Warning: All nested files and subdirectories inside this folder will be permanently removed.
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{error && <div className={css.modalErrorBanner}>{error}</div>}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className={css.modalFooter}>
|
|
103
|
+
<button
|
|
104
|
+
ref={cancelBtnRef}
|
|
105
|
+
type="button"
|
|
106
|
+
className={css.modalBtnCancel}
|
|
107
|
+
onClick={onClose}
|
|
108
|
+
disabled={loading}
|
|
109
|
+
>
|
|
110
|
+
Cancel
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
className={css.modalBtnDanger}
|
|
115
|
+
onClick={handleDelete}
|
|
116
|
+
disabled={loading}
|
|
117
|
+
>
|
|
118
|
+
{loading ? 'Deleting...' : 'Delete'}
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
import css from '../explorer.module.css'
|
|
3
|
+
|
|
4
|
+
interface ExplorerEmptyContextMenuProps {
|
|
5
|
+
x: number
|
|
6
|
+
y: number
|
|
7
|
+
onClose: () => void
|
|
8
|
+
onNewFile: () => void
|
|
9
|
+
onNewFolder: () => void
|
|
10
|
+
onToggleSearch: () => void
|
|
11
|
+
onExpandAll: () => void
|
|
12
|
+
onCollapseAll: () => void
|
|
13
|
+
onRefresh: () => void
|
|
14
|
+
onOpenOptions: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ExplorerEmptyContextMenu: React.FC<ExplorerEmptyContextMenuProps> = ({
|
|
18
|
+
x,
|
|
19
|
+
y,
|
|
20
|
+
onClose,
|
|
21
|
+
onNewFile,
|
|
22
|
+
onNewFolder,
|
|
23
|
+
onToggleSearch,
|
|
24
|
+
onExpandAll,
|
|
25
|
+
onCollapseAll,
|
|
26
|
+
onRefresh,
|
|
27
|
+
onOpenOptions,
|
|
28
|
+
}) => {
|
|
29
|
+
const menuRef = useRef<HTMLDivElement>(null)
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const handleOutsideClick = (e: MouseEvent) => {
|
|
33
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
34
|
+
onClose()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
38
|
+
if (e.key === 'Escape') {
|
|
39
|
+
onClose()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
window.addEventListener('mousedown', handleOutsideClick)
|
|
43
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
44
|
+
return () => {
|
|
45
|
+
window.removeEventListener('mousedown', handleOutsideClick)
|
|
46
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
47
|
+
}
|
|
48
|
+
}, [onClose])
|
|
49
|
+
|
|
50
|
+
const top = Math.min(y, window.innerHeight - 250)
|
|
51
|
+
const left = Math.min(x, window.innerWidth - 200)
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div ref={menuRef} className={css.contextMenu} style={{ top, left }}>
|
|
55
|
+
<div
|
|
56
|
+
className={css.menuItem}
|
|
57
|
+
onClick={() => {
|
|
58
|
+
onNewFile()
|
|
59
|
+
onClose()
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<span>New File...</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div
|
|
65
|
+
className={css.menuItem}
|
|
66
|
+
onClick={() => {
|
|
67
|
+
onNewFolder()
|
|
68
|
+
onClose()
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<span>New Folder...</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div className={css.menuDivider} />
|
|
74
|
+
<div
|
|
75
|
+
className={css.menuItem}
|
|
76
|
+
onClick={() => {
|
|
77
|
+
onToggleSearch()
|
|
78
|
+
onClose()
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<span>Search Files...</span>
|
|
82
|
+
<span className={css.menuShortcut}>Ctrl+F</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div className={css.menuDivider} />
|
|
85
|
+
<div
|
|
86
|
+
className={css.menuItem}
|
|
87
|
+
onClick={() => {
|
|
88
|
+
onExpandAll()
|
|
89
|
+
onClose()
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<span>Expand All</span>
|
|
93
|
+
</div>
|
|
94
|
+
<div
|
|
95
|
+
className={css.menuItem}
|
|
96
|
+
onClick={() => {
|
|
97
|
+
onCollapseAll()
|
|
98
|
+
onClose()
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<span>Collapse All</span>
|
|
102
|
+
</div>
|
|
103
|
+
<div className={css.menuDivider} />
|
|
104
|
+
<div
|
|
105
|
+
className={css.menuItem}
|
|
106
|
+
onClick={() => {
|
|
107
|
+
onRefresh()
|
|
108
|
+
onClose()
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<span>Refresh</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div className={css.menuDivider} />
|
|
114
|
+
<div
|
|
115
|
+
className={css.menuItem}
|
|
116
|
+
onClick={() => {
|
|
117
|
+
onOpenOptions()
|
|
118
|
+
onClose()
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<span>Display Preferences...</span>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import css from '
|
|
2
|
+
import css from '../explorer.module.css'
|
|
3
3
|
|
|
4
4
|
interface ExplorerHeaderProps {
|
|
5
5
|
title: string
|
|
@@ -10,6 +10,7 @@ interface ExplorerHeaderProps {
|
|
|
10
10
|
onNewFile: () => void
|
|
11
11
|
onNewFolder: () => void
|
|
12
12
|
onRefresh: () => void
|
|
13
|
+
onExpandAll: () => void
|
|
13
14
|
onCollapseAll: () => void
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -22,6 +23,7 @@ export const ExplorerHeader: React.FC<ExplorerHeaderProps> = ({
|
|
|
22
23
|
onNewFile,
|
|
23
24
|
onNewFolder,
|
|
24
25
|
onRefresh,
|
|
26
|
+
onExpandAll,
|
|
25
27
|
onCollapseAll,
|
|
26
28
|
}) => {
|
|
27
29
|
return (
|
|
@@ -32,39 +34,44 @@ export const ExplorerHeader: React.FC<ExplorerHeaderProps> = ({
|
|
|
32
34
|
<div className={css.headerActions}>
|
|
33
35
|
<button
|
|
34
36
|
className={`${css.iconBtn} ${searchOpen ? css.iconBtnActive : ''}`}
|
|
35
|
-
title="
|
|
37
|
+
title="Search files and folders"
|
|
36
38
|
onClick={onToggleSearch}
|
|
37
39
|
>
|
|
38
|
-
<svg width="
|
|
40
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
39
41
|
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
|
|
40
42
|
</svg>
|
|
41
43
|
</button>
|
|
42
44
|
<button className={css.iconBtn} title="New File" onClick={onNewFile}>
|
|
43
|
-
<svg width="
|
|
45
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
44
46
|
<path d="M9 1H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6L9 1zm0 1.5L12.5 6H9V2.5zM8 8.5V11h1V8.5h2.5v-1H9V5H8v2.5H5.5v1H8z"/>
|
|
45
47
|
</svg>
|
|
46
48
|
</button>
|
|
47
49
|
<button className={css.iconBtn} title="New Folder" onClick={onNewFolder}>
|
|
48
|
-
<svg width="
|
|
50
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
49
51
|
<path d="M7 2l2 2h5a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h5zm2.5 6.5H8V6H7v2.5H4.5v1H7V12h1V9.5h2.5v-1z"/>
|
|
50
52
|
</svg>
|
|
51
53
|
</button>
|
|
52
54
|
<button className={css.iconBtn} title="Refresh" onClick={onRefresh}>
|
|
53
|
-
<svg width="
|
|
55
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
54
56
|
<path d="M13.65 2.35A7.958 7.958 0 0 0 8 0a8 8 0 1 0 8 8h-2a6 6 0 1 1-1.76-4.24l-2.24 2.24H16V0l-2.35 2.35z"/>
|
|
55
57
|
</svg>
|
|
56
58
|
</button>
|
|
57
|
-
<button className={css.iconBtn} title="
|
|
58
|
-
<svg width="
|
|
59
|
-
<path d="
|
|
59
|
+
<button className={css.iconBtn} title="Expand All" onClick={onExpandAll}>
|
|
60
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
61
|
+
<path d="M4.646 5.854a.5.5 0 0 1 0-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8 3.207 5.354 5.854a.5.5 0 0 1-.708 0zm0 4.292a.5.5 0 0 0 0 .708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8 12.793l-2.646-2.647a.5.5 0 0 0-.708 0z"/>
|
|
62
|
+
</svg>
|
|
63
|
+
</button>
|
|
64
|
+
<button className={css.iconBtn} title="Collapse All" onClick={onCollapseAll}>
|
|
65
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
66
|
+
<path d="M4.646 2.854a.5.5 0 0 0 0 .708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8 5.293 5.354 2.854a.5.5 0 0 0-.708 0zm0 10.292a.5.5 0 0 1 0-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8 10.707l-2.646 2.647a.5.5 0 0 1-.708 0z"/>
|
|
60
67
|
</svg>
|
|
61
68
|
</button>
|
|
62
69
|
<button
|
|
63
70
|
className={`${css.iconBtn} ${optionsOpen ? css.iconBtnActive : ''}`}
|
|
64
|
-
title="Explorer
|
|
71
|
+
title="Explorer display preferences"
|
|
65
72
|
onClick={onToggleOptions}
|
|
66
73
|
>
|
|
67
|
-
<svg width="
|
|
74
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
68
75
|
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
|
69
76
|
</svg>
|
|
70
77
|
</button>
|
|
@@ -72,4 +79,3 @@ export const ExplorerHeader: React.FC<ExplorerHeaderProps> = ({
|
|
|
72
79
|
</div>
|
|
73
80
|
)
|
|
74
81
|
}
|
|
75
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react'
|
|
2
|
-
import css from '
|
|
3
|
-
import type { ExplorerOptions, ExplorerDensity, SingleClickAction } from '
|
|
2
|
+
import css from '../explorer.module.css'
|
|
3
|
+
import type { ExplorerOptions, ExplorerDensity, SingleClickAction } from '../utils/options.ts'
|
|
4
4
|
|
|
5
5
|
interface ExplorerOptionsModalProps {
|
|
6
6
|
options: ExplorerOptions
|
|
@@ -17,7 +17,6 @@ export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
|
17
17
|
}) => {
|
|
18
18
|
const popoverRef = useRef<HTMLDivElement>(null)
|
|
19
19
|
|
|
20
|
-
// Close on Click Outside & Escape key
|
|
21
20
|
useEffect(() => {
|
|
22
21
|
const handleMouseDown = (e: MouseEvent) => {
|
|
23
22
|
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
|
|
@@ -55,7 +54,6 @@ export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
|
55
54
|
|
|
56
55
|
return (
|
|
57
56
|
<div ref={popoverRef} className={css.optionsPopover}>
|
|
58
|
-
{/* Header */}
|
|
59
57
|
<div className={css.optionsHeader}>
|
|
60
58
|
<div className={css.optionsHeaderTitle}>
|
|
61
59
|
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor" style={{ marginRight: '6px' }}>
|
|
@@ -77,13 +75,9 @@ export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
|
77
75
|
</div>
|
|
78
76
|
</div>
|
|
79
77
|
|
|
80
|
-
{/* Body */}
|
|
81
78
|
<div className={css.optionsBody}>
|
|
82
|
-
{/* Section 1: Density & Scale */}
|
|
83
79
|
<div className={css.optionsSection}>
|
|
84
80
|
<div className={css.optionsSectionTitle}>Scale & Density</div>
|
|
85
|
-
|
|
86
|
-
{/* Density preset buttons */}
|
|
87
81
|
<div className={css.segmentGroup}>
|
|
88
82
|
<button
|
|
89
83
|
type="button"
|
|
@@ -108,7 +102,6 @@ export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
|
108
102
|
</button>
|
|
109
103
|
</div>
|
|
110
104
|
|
|
111
|
-
{/* Scale zoom slider */}
|
|
112
105
|
<div className={css.optionsRow} style={{ marginTop: '6px' }}>
|
|
113
106
|
<div className={css.optionsLabelWrap}>
|
|
114
107
|
<span className={css.optionsLabel}>Zoom Scale</span>
|
|
@@ -139,7 +132,6 @@ export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
|
139
132
|
/>
|
|
140
133
|
</div>
|
|
141
134
|
|
|
142
|
-
{/* Section 2: Motion & Animation */}
|
|
143
135
|
<div className={css.optionsSection}>
|
|
144
136
|
<div className={css.optionsSectionTitle}>Motion & Transitions</div>
|
|
145
137
|
<div className={css.optionsRow}>
|
|
@@ -158,11 +150,9 @@ export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
|
158
150
|
</div>
|
|
159
151
|
</div>
|
|
160
152
|
|
|
161
|
-
{/* Section 3: File Tree Display */}
|
|
162
153
|
<div className={css.optionsSection}>
|
|
163
154
|
<div className={css.optionsSectionTitle}>Workspace View</div>
|
|
164
155
|
|
|
165
|
-
{/* Show hidden files */}
|
|
166
156
|
<div className={css.optionsRow}>
|
|
167
157
|
<div className={css.optionsLabelWrap}>
|
|
168
158
|
<span className={css.optionsLabel}>Show Hidden Files</span>
|
|
@@ -178,7 +168,6 @@ export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
|
178
168
|
</div>
|
|
179
169
|
</div>
|
|
180
170
|
|
|
181
|
-
{/* Git badges */}
|
|
182
171
|
<div className={css.optionsRow}>
|
|
183
172
|
<div className={css.optionsLabelWrap}>
|
|
184
173
|
<span className={css.optionsLabel}>Git Status Indicators</span>
|
|
@@ -194,7 +183,6 @@ export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
|
194
183
|
</div>
|
|
195
184
|
</div>
|
|
196
185
|
|
|
197
|
-
{/* Compact folders */}
|
|
198
186
|
<div className={css.optionsRow}>
|
|
199
187
|
<div className={css.optionsLabelWrap}>
|
|
200
188
|
<span className={css.optionsLabel}>Compact Folders</span>
|
|
@@ -211,7 +199,6 @@ export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
|
211
199
|
</div>
|
|
212
200
|
</div>
|
|
213
201
|
|
|
214
|
-
{/* Section 4: Quick Workflow */}
|
|
215
202
|
<div className={css.optionsSection}>
|
|
216
203
|
<div className={css.optionsSectionTitle}>Interaction</div>
|
|
217
204
|
<div className={css.optionsRow}>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect } from 'react'
|
|
2
|
-
import css from '
|
|
3
|
-
import type { FsEntry } from '
|
|
4
|
-
import type { SingleClickAction } from '
|
|
5
|
-
import { renderFileIcon } from '
|
|
6
|
-
import { setupDragStart, setupDragEnd, type DragExplorerItem } from '
|
|
2
|
+
import css from '../explorer.module.css'
|
|
3
|
+
import type { FsEntry } from '../utils/api.ts'
|
|
4
|
+
import type { SingleClickAction } from '../utils/options.ts'
|
|
5
|
+
import { renderFileIcon } from '../utils/vscode-theme.tsx'
|
|
6
|
+
import { setupDragStart, setupDragEnd, type DragExplorerItem } from '../utils/drag-drop.ts'
|
|
7
7
|
|
|
8
8
|
interface ExplorerRowProps {
|
|
9
9
|
entry: FsEntry
|
|
@@ -11,6 +11,7 @@ interface ExplorerRowProps {
|
|
|
11
11
|
isExpanded: boolean
|
|
12
12
|
isSelected: boolean
|
|
13
13
|
isFocused: boolean
|
|
14
|
+
isRenaming?: boolean
|
|
14
15
|
gitStatus?: string
|
|
15
16
|
relativePath: string
|
|
16
17
|
compactChain?: FsEntry[]
|
|
@@ -26,6 +27,8 @@ interface ExplorerRowProps {
|
|
|
26
27
|
onContextMenu: (e: React.MouseEvent) => void
|
|
27
28
|
onInsertReference: () => void
|
|
28
29
|
onCopyRelativePath: () => void
|
|
30
|
+
onStartRename?: () => void
|
|
31
|
+
onCancelRename?: () => void
|
|
29
32
|
onRenameSubmit: (newName: string) => void
|
|
30
33
|
onDelete: () => void
|
|
31
34
|
}
|
|
@@ -36,6 +39,7 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
36
39
|
isExpanded,
|
|
37
40
|
isSelected,
|
|
38
41
|
isFocused,
|
|
42
|
+
isRenaming = false,
|
|
39
43
|
gitStatus,
|
|
40
44
|
relativePath,
|
|
41
45
|
compactChain,
|
|
@@ -51,19 +55,30 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
51
55
|
onContextMenu,
|
|
52
56
|
onInsertReference,
|
|
53
57
|
onCopyRelativePath,
|
|
58
|
+
onStartRename,
|
|
59
|
+
onCancelRename,
|
|
54
60
|
onRenameSubmit,
|
|
55
61
|
onDelete,
|
|
56
62
|
}) => {
|
|
57
|
-
const [isRenaming, setIsRenaming] = useState(false)
|
|
58
63
|
const [renameVal, setRenameVal] = useState(entry.name)
|
|
59
64
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
60
65
|
|
|
61
66
|
useEffect(() => {
|
|
62
67
|
if (isRenaming) {
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
setRenameVal(entry.name)
|
|
69
|
+
requestAnimationFrame(() => {
|
|
70
|
+
if (inputRef.current) {
|
|
71
|
+
inputRef.current.focus()
|
|
72
|
+
const dotIndex = entry.isDir ? -1 : entry.name.lastIndexOf('.')
|
|
73
|
+
if (dotIndex > 0) {
|
|
74
|
+
inputRef.current.setSelectionRange(0, dotIndex)
|
|
75
|
+
} else {
|
|
76
|
+
inputRef.current.select()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
65
80
|
}
|
|
66
|
-
}, [isRenaming])
|
|
81
|
+
}, [isRenaming, entry.name, entry.isDir])
|
|
67
82
|
|
|
68
83
|
const handleDragStart = (e: React.DragEvent) => {
|
|
69
84
|
const item: DragExplorerItem = {
|
|
@@ -77,25 +92,35 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
77
92
|
|
|
78
93
|
const handleRenameKeyDown = (e: React.KeyboardEvent) => {
|
|
79
94
|
if (e.key === 'Enter') {
|
|
95
|
+
e.stopPropagation()
|
|
80
96
|
const trimmed = renameVal.trim()
|
|
81
97
|
if (trimmed && trimmed !== entry.name) {
|
|
82
98
|
onRenameSubmit(trimmed)
|
|
99
|
+
} else {
|
|
100
|
+
onCancelRename?.()
|
|
83
101
|
}
|
|
84
|
-
setIsRenaming(false)
|
|
85
102
|
} else if (e.key === 'Escape') {
|
|
103
|
+
e.stopPropagation()
|
|
86
104
|
setRenameVal(entry.name)
|
|
87
|
-
|
|
105
|
+
onCancelRename?.()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const handleRenameBlur = () => {
|
|
110
|
+
const trimmed = renameVal.trim()
|
|
111
|
+
if (trimmed && trimmed !== entry.name) {
|
|
112
|
+
onRenameSubmit(trimmed)
|
|
113
|
+
} else {
|
|
114
|
+
onCancelRename?.()
|
|
88
115
|
}
|
|
89
116
|
}
|
|
90
117
|
|
|
91
|
-
// Display label: if compactFolders is on and compactChain has elements, join their names (e.g. "vscode/antigravity-quota-context")
|
|
92
118
|
const displayName = compactFolders && compactChain && compactChain.length > 1
|
|
93
119
|
? compactChain.map((c) => c.name).join('/')
|
|
94
120
|
: (compactFolders && entry.compactName
|
|
95
121
|
? entry.compactName
|
|
96
122
|
: entry.name)
|
|
97
123
|
|
|
98
|
-
// Git status styling
|
|
99
124
|
let gitClass = ''
|
|
100
125
|
let gitLetter = ''
|
|
101
126
|
if (gitStatus) {
|
|
@@ -108,6 +133,9 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
108
133
|
} else if (gitStatus.includes('D')) {
|
|
109
134
|
gitClass = css.gitDeleted
|
|
110
135
|
gitLetter = 'D'
|
|
136
|
+
} else if (gitStatus.includes('R')) {
|
|
137
|
+
gitClass = css.gitModified
|
|
138
|
+
gitLetter = 'R'
|
|
111
139
|
}
|
|
112
140
|
}
|
|
113
141
|
|
|
@@ -118,6 +146,7 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
118
146
|
className={`${css.row} ${isSelected ? css.rowSelected : ''} ${isFocused ? css.rowFocused : ''}`}
|
|
119
147
|
style={{ paddingLeft: `${depth * indentStep + 8}px` }}
|
|
120
148
|
onClick={(e) => {
|
|
149
|
+
if (isRenaming) return
|
|
121
150
|
onSelect(e)
|
|
122
151
|
if (entry.isDir) {
|
|
123
152
|
onToggle()
|
|
@@ -126,6 +155,7 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
126
155
|
}
|
|
127
156
|
}}
|
|
128
157
|
onDoubleClick={(e) => {
|
|
158
|
+
if (isRenaming) return
|
|
129
159
|
if (!entry.isDir) {
|
|
130
160
|
e.stopPropagation()
|
|
131
161
|
onDoubleClick?.()
|
|
@@ -136,7 +166,6 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
136
166
|
onDragStart={handleDragStart}
|
|
137
167
|
onDragEnd={setupDragEnd}
|
|
138
168
|
>
|
|
139
|
-
{/* Indentation guide lines */}
|
|
140
169
|
{Array.from({ length: depth }).map((_, i) => (
|
|
141
170
|
<span
|
|
142
171
|
key={i}
|
|
@@ -145,7 +174,6 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
145
174
|
/>
|
|
146
175
|
))}
|
|
147
176
|
|
|
148
|
-
{/* Expand/Collapse Chevron */}
|
|
149
177
|
<span
|
|
150
178
|
className={`${css.chevron} ${isExpanded ? css.chevronExpanded : ''} ${!entry.isDir ? css.chevronHidden : ''}`}
|
|
151
179
|
style={!enableMotion ? { transition: 'none' } : undefined}
|
|
@@ -159,12 +187,10 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
159
187
|
</svg>
|
|
160
188
|
</span>
|
|
161
189
|
|
|
162
|
-
{/* Language / Folder Icon */}
|
|
163
190
|
<span className={css.iconWrap}>
|
|
164
191
|
{renderFileIcon(entry.name, entry.isDir, isExpanded, iconSize)}
|
|
165
192
|
</span>
|
|
166
193
|
|
|
167
|
-
{/* Label or Inline Rename */}
|
|
168
194
|
{isRenaming ? (
|
|
169
195
|
<input
|
|
170
196
|
ref={inputRef}
|
|
@@ -173,13 +199,7 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
173
199
|
value={renameVal}
|
|
174
200
|
onChange={(e) => setRenameVal(e.target.value)}
|
|
175
201
|
onKeyDown={handleRenameKeyDown}
|
|
176
|
-
onBlur={
|
|
177
|
-
const trimmed = renameVal.trim()
|
|
178
|
-
if (trimmed && trimmed !== entry.name) {
|
|
179
|
-
onRenameSubmit(trimmed)
|
|
180
|
-
}
|
|
181
|
-
setIsRenaming(false)
|
|
182
|
-
}}
|
|
202
|
+
onBlur={handleRenameBlur}
|
|
183
203
|
onClick={(e) => e.stopPropagation()}
|
|
184
204
|
/>
|
|
185
205
|
) : (
|
|
@@ -191,10 +211,8 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
191
211
|
</span>
|
|
192
212
|
)}
|
|
193
213
|
|
|
194
|
-
{/* Git status badge */}
|
|
195
214
|
{showGitBadges && gitLetter && <span className={`${css.gitBadge} ${gitClass}`}>{gitLetter}</span>}
|
|
196
215
|
|
|
197
|
-
{/* Hover actions */}
|
|
198
216
|
{!isRenaming && (
|
|
199
217
|
<div className={css.hoverActions} onClick={(e) => e.stopPropagation()}>
|
|
200
218
|
<button
|
|
@@ -226,7 +244,7 @@ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
|
226
244
|
title="Rename (F2)"
|
|
227
245
|
onClick={(e) => {
|
|
228
246
|
e.stopPropagation()
|
|
229
|
-
|
|
247
|
+
onStartRename?.()
|
|
230
248
|
}}
|
|
231
249
|
>
|
|
232
250
|
<svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor">
|