dsh-explorer 0.0.0-stage → 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/LICENSE +22 -0
- package/README.md +106 -2
- package/cordis.patch.yml +5 -0
- package/icon.svg +7 -0
- package/lib/client.js +859 -0
- package/lib/index.js +854 -0
- package/package.json +91 -4
- package/src/client/ExplorerView.tsx +981 -0
- package/src/client/components/ExplorerContextMenu.tsx +157 -0
- package/src/client/components/ExplorerDeleteModal.tsx +124 -0
- package/src/client/components/ExplorerEmptyContextMenu.tsx +125 -0
- package/src/client/components/ExplorerHeader.tsx +81 -0
- package/src/client/components/ExplorerOptionsModal.tsx +230 -0
- package/src/client/components/ExplorerRow.tsx +258 -0
- package/src/client/components/ExplorerSearch.tsx +55 -0
- package/src/client/components/TreeNode.tsx +384 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/explorer.module.css +833 -0
- package/src/client/hooks/useKeyboardNav.ts +89 -0
- package/src/client/hooks/useLiveSync.ts +81 -0
- package/src/client/index.tsx +238 -0
- package/src/client/utils/api.ts +251 -0
- package/src/client/utils/conversation-draft.ts +644 -0
- package/src/client/utils/drag-drop.ts +209 -0
- package/src/client/utils/open-tab.ts +69 -0
- package/src/client/utils/options.ts +59 -0
- package/src/client/utils/vscode-theme.tsx +250 -0
- package/src/config.ts +11 -0
- package/src/fs-operations.ts +116 -0
- package/src/fs-search.ts +116 -0
- package/src/fs-tree.ts +155 -0
- package/src/fs-watch.ts +179 -0
- package/src/git-status.ts +167 -0
- package/src/index.ts +177 -0
- package/src/path-security.ts +22 -0
- package/src/session-path.ts +35 -0
- package/src/wire.ts +63 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
import css from '../explorer.module.css'
|
|
3
|
+
import type { ExplorerOptions, ExplorerDensity, SingleClickAction } from '../utils/options.ts'
|
|
4
|
+
|
|
5
|
+
interface ExplorerOptionsModalProps {
|
|
6
|
+
options: ExplorerOptions
|
|
7
|
+
onChange: (updated: Partial<ExplorerOptions>) => void
|
|
8
|
+
onReset: () => void
|
|
9
|
+
onClose: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ExplorerOptionsModal: React.FC<ExplorerOptionsModalProps> = ({
|
|
13
|
+
options,
|
|
14
|
+
onChange,
|
|
15
|
+
onReset,
|
|
16
|
+
onClose,
|
|
17
|
+
}) => {
|
|
18
|
+
const popoverRef = useRef<HTMLDivElement>(null)
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
22
|
+
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
|
|
23
|
+
onClose()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
27
|
+
if (e.key === 'Escape') {
|
|
28
|
+
onClose()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
document.addEventListener('mousedown', handleMouseDown, true)
|
|
33
|
+
document.addEventListener('keydown', handleKeyDown, true)
|
|
34
|
+
return () => {
|
|
35
|
+
document.removeEventListener('mousedown', handleMouseDown, true)
|
|
36
|
+
document.removeEventListener('keydown', handleKeyDown, true)
|
|
37
|
+
}
|
|
38
|
+
}, [onClose])
|
|
39
|
+
|
|
40
|
+
const handleDensityChange = (density: ExplorerDensity) => {
|
|
41
|
+
onChange({ density })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleScaleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
45
|
+
const scale = parseFloat(e.target.value)
|
|
46
|
+
if (!Number.isNaN(scale)) {
|
|
47
|
+
onChange({ scale: Math.round(scale * 100) / 100 })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleActionChange = (action: SingleClickAction) => {
|
|
52
|
+
onChange({ singleClickAction: action })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div ref={popoverRef} className={css.optionsPopover}>
|
|
57
|
+
<div className={css.optionsHeader}>
|
|
58
|
+
<div className={css.optionsHeaderTitle}>
|
|
59
|
+
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor" style={{ marginRight: '6px' }}>
|
|
60
|
+
<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" />
|
|
61
|
+
</svg>
|
|
62
|
+
EXPLORER OPTIONS
|
|
63
|
+
</div>
|
|
64
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
65
|
+
<button className={css.optionsIconBtn} title="Reset to defaults" onClick={onReset}>
|
|
66
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
67
|
+
<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" />
|
|
68
|
+
</svg>
|
|
69
|
+
</button>
|
|
70
|
+
<button className={css.optionsIconBtn} title="Close" onClick={onClose}>
|
|
71
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
72
|
+
<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" />
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className={css.optionsBody}>
|
|
79
|
+
<div className={css.optionsSection}>
|
|
80
|
+
<div className={css.optionsSectionTitle}>Scale & Density</div>
|
|
81
|
+
<div className={css.segmentGroup}>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
className={`${css.segmentBtn} ${options.density === 'compact' ? css.segmentBtnActive : ''}`}
|
|
85
|
+
onClick={() => handleDensityChange('compact')}
|
|
86
|
+
>
|
|
87
|
+
Compact
|
|
88
|
+
</button>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
className={`${css.segmentBtn} ${options.density === 'normal' ? css.segmentBtnActive : ''}`}
|
|
92
|
+
onClick={() => handleDensityChange('normal')}
|
|
93
|
+
>
|
|
94
|
+
Normal
|
|
95
|
+
</button>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
className={`${css.segmentBtn} ${options.density === 'relaxed' ? css.segmentBtnActive : ''}`}
|
|
99
|
+
onClick={() => handleDensityChange('relaxed')}
|
|
100
|
+
>
|
|
101
|
+
Relaxed
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div className={css.optionsRow} style={{ marginTop: '6px' }}>
|
|
106
|
+
<div className={css.optionsLabelWrap}>
|
|
107
|
+
<span className={css.optionsLabel}>Zoom Scale</span>
|
|
108
|
+
<span className={css.optionsSubLabel}>Resize tree text and icons</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
111
|
+
<span className={css.optionsBadge}>{Math.round(options.scale * 100)}%</span>
|
|
112
|
+
{options.scale !== 1.0 && (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
className={css.optionsMiniBtn}
|
|
116
|
+
title="Reset scale to 100%"
|
|
117
|
+
onClick={() => onChange({ scale: 1.0 })}
|
|
118
|
+
>
|
|
119
|
+
1x
|
|
120
|
+
</button>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<input
|
|
125
|
+
type="range"
|
|
126
|
+
min="0.75"
|
|
127
|
+
max="1.35"
|
|
128
|
+
step="0.05"
|
|
129
|
+
value={options.scale}
|
|
130
|
+
onChange={handleScaleChange}
|
|
131
|
+
className={css.optionsSlider}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className={css.optionsSection}>
|
|
136
|
+
<div className={css.optionsSectionTitle}>Motion & Transitions</div>
|
|
137
|
+
<div className={css.optionsRow}>
|
|
138
|
+
<div className={css.optionsLabelWrap}>
|
|
139
|
+
<span className={css.optionsLabel}>Smooth Animations</span>
|
|
140
|
+
<span className={css.optionsSubLabel}>Fluid folder collapse & icon turns</span>
|
|
141
|
+
</div>
|
|
142
|
+
<div
|
|
143
|
+
className={`${css.switch} ${options.enableMotion ? css.switchActive : ''}`}
|
|
144
|
+
onClick={() => onChange({ enableMotion: !options.enableMotion })}
|
|
145
|
+
role="switch"
|
|
146
|
+
aria-checked={options.enableMotion}
|
|
147
|
+
>
|
|
148
|
+
<div className={`${css.switchKnob} ${options.enableMotion ? css.switchKnobActive : ''}`} />
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div className={css.optionsSection}>
|
|
154
|
+
<div className={css.optionsSectionTitle}>Workspace View</div>
|
|
155
|
+
|
|
156
|
+
<div className={css.optionsRow}>
|
|
157
|
+
<div className={css.optionsLabelWrap}>
|
|
158
|
+
<span className={css.optionsLabel}>Show Hidden Files</span>
|
|
159
|
+
<span className={css.optionsSubLabel}>Include dotfiles and config directories</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div
|
|
162
|
+
className={`${css.switch} ${options.showHiddenFiles ? css.switchActive : ''}`}
|
|
163
|
+
onClick={() => onChange({ showHiddenFiles: !options.showHiddenFiles })}
|
|
164
|
+
role="switch"
|
|
165
|
+
aria-checked={options.showHiddenFiles}
|
|
166
|
+
>
|
|
167
|
+
<div className={`${css.switchKnob} ${options.showHiddenFiles ? css.switchKnobActive : ''}`} />
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className={css.optionsRow}>
|
|
172
|
+
<div className={css.optionsLabelWrap}>
|
|
173
|
+
<span className={css.optionsLabel}>Git Status Indicators</span>
|
|
174
|
+
<span className={css.optionsSubLabel}>Highlight modified & untracked files</span>
|
|
175
|
+
</div>
|
|
176
|
+
<div
|
|
177
|
+
className={`${css.switch} ${options.showGitBadges ? css.switchActive : ''}`}
|
|
178
|
+
onClick={() => onChange({ showGitBadges: !options.showGitBadges })}
|
|
179
|
+
role="switch"
|
|
180
|
+
aria-checked={options.showGitBadges}
|
|
181
|
+
>
|
|
182
|
+
<div className={`${css.switchKnob} ${options.showGitBadges ? css.switchKnobActive : ''}`} />
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div className={css.optionsRow}>
|
|
187
|
+
<div className={css.optionsLabelWrap}>
|
|
188
|
+
<span className={css.optionsLabel}>Compact Folders</span>
|
|
189
|
+
<span className={css.optionsSubLabel}>Chain single-child directory paths</span>
|
|
190
|
+
</div>
|
|
191
|
+
<div
|
|
192
|
+
className={`${css.switch} ${options.compactFolders ? css.switchActive : ''}`}
|
|
193
|
+
onClick={() => onChange({ compactFolders: !options.compactFolders })}
|
|
194
|
+
role="switch"
|
|
195
|
+
aria-checked={options.compactFolders}
|
|
196
|
+
>
|
|
197
|
+
<div className={`${css.switchKnob} ${options.compactFolders ? css.switchKnobActive : ''}`} />
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div className={css.optionsSection}>
|
|
203
|
+
<div className={css.optionsSectionTitle}>Interaction</div>
|
|
204
|
+
<div className={css.optionsRow}>
|
|
205
|
+
<div className={css.optionsLabelWrap}>
|
|
206
|
+
<span className={css.optionsLabel}>File Click Behavior</span>
|
|
207
|
+
<span className={css.optionsSubLabel}>Direct action when clicking a file</span>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
<div className={css.segmentGroup} style={{ marginTop: '4px' }}>
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
className={`${css.segmentBtn} ${options.singleClickAction === 'select' ? css.segmentBtnActive : ''}`}
|
|
214
|
+
onClick={() => handleActionChange('select')}
|
|
215
|
+
>
|
|
216
|
+
Select / Focus
|
|
217
|
+
</button>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
className={`${css.segmentBtn} ${options.singleClickAction === 'mention' ? css.segmentBtnActive : ''}`}
|
|
221
|
+
onClick={() => handleActionChange('mention')}
|
|
222
|
+
>
|
|
223
|
+
Insert @Mention
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react'
|
|
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
|
+
|
|
8
|
+
interface ExplorerRowProps {
|
|
9
|
+
entry: FsEntry
|
|
10
|
+
depth: number
|
|
11
|
+
isExpanded: boolean
|
|
12
|
+
isSelected: boolean
|
|
13
|
+
isFocused: boolean
|
|
14
|
+
isRenaming?: boolean
|
|
15
|
+
gitStatus?: string
|
|
16
|
+
relativePath: string
|
|
17
|
+
compactChain?: FsEntry[]
|
|
18
|
+
indentStep?: number
|
|
19
|
+
iconSize?: number
|
|
20
|
+
enableMotion?: boolean
|
|
21
|
+
showGitBadges?: boolean
|
|
22
|
+
compactFolders?: boolean
|
|
23
|
+
singleClickAction?: SingleClickAction
|
|
24
|
+
onToggle: () => void
|
|
25
|
+
onSelect: (e: React.MouseEvent) => void
|
|
26
|
+
onDoubleClick?: () => void
|
|
27
|
+
onContextMenu: (e: React.MouseEvent) => void
|
|
28
|
+
onInsertReference: () => void
|
|
29
|
+
onCopyRelativePath: () => void
|
|
30
|
+
onStartRename?: () => void
|
|
31
|
+
onCancelRename?: () => void
|
|
32
|
+
onRenameSubmit: (newName: string) => void
|
|
33
|
+
onDelete: () => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const ExplorerRow: React.FC<ExplorerRowProps> = ({
|
|
37
|
+
entry,
|
|
38
|
+
depth,
|
|
39
|
+
isExpanded,
|
|
40
|
+
isSelected,
|
|
41
|
+
isFocused,
|
|
42
|
+
isRenaming = false,
|
|
43
|
+
gitStatus,
|
|
44
|
+
relativePath,
|
|
45
|
+
compactChain,
|
|
46
|
+
indentStep = 16,
|
|
47
|
+
iconSize = 16,
|
|
48
|
+
enableMotion = true,
|
|
49
|
+
showGitBadges = true,
|
|
50
|
+
compactFolders = true,
|
|
51
|
+
singleClickAction = 'select',
|
|
52
|
+
onToggle,
|
|
53
|
+
onSelect,
|
|
54
|
+
onDoubleClick,
|
|
55
|
+
onContextMenu,
|
|
56
|
+
onInsertReference,
|
|
57
|
+
onCopyRelativePath,
|
|
58
|
+
onStartRename,
|
|
59
|
+
onCancelRename,
|
|
60
|
+
onRenameSubmit,
|
|
61
|
+
onDelete,
|
|
62
|
+
}) => {
|
|
63
|
+
const [renameVal, setRenameVal] = useState(entry.name)
|
|
64
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (isRenaming) {
|
|
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
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}, [isRenaming, entry.name, entry.isDir])
|
|
82
|
+
|
|
83
|
+
const handleDragStart = (e: React.DragEvent) => {
|
|
84
|
+
const item: DragExplorerItem = {
|
|
85
|
+
path: entry.path,
|
|
86
|
+
relativePath,
|
|
87
|
+
isDir: entry.isDir,
|
|
88
|
+
name: entry.name,
|
|
89
|
+
}
|
|
90
|
+
setupDragStart(e, [item])
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const handleRenameKeyDown = (e: React.KeyboardEvent) => {
|
|
94
|
+
if (e.key === 'Enter') {
|
|
95
|
+
e.stopPropagation()
|
|
96
|
+
const trimmed = renameVal.trim()
|
|
97
|
+
if (trimmed && trimmed !== entry.name) {
|
|
98
|
+
onRenameSubmit(trimmed)
|
|
99
|
+
} else {
|
|
100
|
+
onCancelRename?.()
|
|
101
|
+
}
|
|
102
|
+
} else if (e.key === 'Escape') {
|
|
103
|
+
e.stopPropagation()
|
|
104
|
+
setRenameVal(entry.name)
|
|
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?.()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const displayName = compactFolders && compactChain && compactChain.length > 1
|
|
119
|
+
? compactChain.map((c) => c.name).join('/')
|
|
120
|
+
: (compactFolders && entry.compactName
|
|
121
|
+
? entry.compactName
|
|
122
|
+
: entry.name)
|
|
123
|
+
|
|
124
|
+
let gitClass = ''
|
|
125
|
+
let gitLetter = ''
|
|
126
|
+
if (gitStatus) {
|
|
127
|
+
if (gitStatus.includes('M')) {
|
|
128
|
+
gitClass = css.gitModified
|
|
129
|
+
gitLetter = 'M'
|
|
130
|
+
} else if (gitStatus.includes('?') || gitStatus.includes('A')) {
|
|
131
|
+
gitClass = css.gitUntracked
|
|
132
|
+
gitLetter = 'U'
|
|
133
|
+
} else if (gitStatus.includes('D')) {
|
|
134
|
+
gitClass = css.gitDeleted
|
|
135
|
+
gitLetter = 'D'
|
|
136
|
+
} else if (gitStatus.includes('R')) {
|
|
137
|
+
gitClass = css.gitModified
|
|
138
|
+
gitLetter = 'R'
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const isIgnored = gitStatus?.includes('!')
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
className={`${css.row} ${isSelected ? css.rowSelected : ''} ${isFocused ? css.rowFocused : ''}`}
|
|
147
|
+
style={{ paddingLeft: `${depth * indentStep + 8}px` }}
|
|
148
|
+
onClick={(e) => {
|
|
149
|
+
if (isRenaming) return
|
|
150
|
+
onSelect(e)
|
|
151
|
+
if (entry.isDir) {
|
|
152
|
+
onToggle()
|
|
153
|
+
} else if (singleClickAction === 'mention') {
|
|
154
|
+
onInsertReference()
|
|
155
|
+
}
|
|
156
|
+
}}
|
|
157
|
+
onDoubleClick={(e) => {
|
|
158
|
+
if (isRenaming) return
|
|
159
|
+
if (!entry.isDir) {
|
|
160
|
+
e.stopPropagation()
|
|
161
|
+
onDoubleClick?.()
|
|
162
|
+
}
|
|
163
|
+
}}
|
|
164
|
+
onContextMenu={onContextMenu}
|
|
165
|
+
draggable={!isRenaming}
|
|
166
|
+
onDragStart={handleDragStart}
|
|
167
|
+
onDragEnd={setupDragEnd}
|
|
168
|
+
>
|
|
169
|
+
{Array.from({ length: depth }).map((_, i) => (
|
|
170
|
+
<span
|
|
171
|
+
key={i}
|
|
172
|
+
className={css.indentGuide}
|
|
173
|
+
style={{ left: `${i * indentStep + 15}px` }}
|
|
174
|
+
/>
|
|
175
|
+
))}
|
|
176
|
+
|
|
177
|
+
<span
|
|
178
|
+
className={`${css.chevron} ${isExpanded ? css.chevronExpanded : ''} ${!entry.isDir ? css.chevronHidden : ''}`}
|
|
179
|
+
style={!enableMotion ? { transition: 'none' } : undefined}
|
|
180
|
+
onClick={(e) => {
|
|
181
|
+
e.stopPropagation()
|
|
182
|
+
onToggle()
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor">
|
|
186
|
+
<path d="M6 3.5L10.5 8 6 12.5V3.5Z" />
|
|
187
|
+
</svg>
|
|
188
|
+
</span>
|
|
189
|
+
|
|
190
|
+
<span className={css.iconWrap}>
|
|
191
|
+
{renderFileIcon(entry.name, entry.isDir, isExpanded, iconSize)}
|
|
192
|
+
</span>
|
|
193
|
+
|
|
194
|
+
{isRenaming ? (
|
|
195
|
+
<input
|
|
196
|
+
ref={inputRef}
|
|
197
|
+
type="text"
|
|
198
|
+
className={css.inlineInput}
|
|
199
|
+
value={renameVal}
|
|
200
|
+
onChange={(e) => setRenameVal(e.target.value)}
|
|
201
|
+
onKeyDown={handleRenameKeyDown}
|
|
202
|
+
onBlur={handleRenameBlur}
|
|
203
|
+
onClick={(e) => e.stopPropagation()}
|
|
204
|
+
/>
|
|
205
|
+
) : (
|
|
206
|
+
<span
|
|
207
|
+
className={`${css.label} ${isIgnored ? css.labelDimmed : ''} ${showGitBadges ? gitClass : ''}`}
|
|
208
|
+
title={relativePath}
|
|
209
|
+
>
|
|
210
|
+
{displayName}
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{showGitBadges && gitLetter && <span className={`${css.gitBadge} ${gitClass}`}>{gitLetter}</span>}
|
|
215
|
+
|
|
216
|
+
{!isRenaming && (
|
|
217
|
+
<div className={css.hoverActions} onClick={(e) => e.stopPropagation()}>
|
|
218
|
+
<button
|
|
219
|
+
className={css.actionBtn}
|
|
220
|
+
title="Mention in Chat (@)"
|
|
221
|
+
onClick={(e) => {
|
|
222
|
+
e.stopPropagation()
|
|
223
|
+
onInsertReference()
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
227
|
+
<path d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2zM7 5h2v2h2v2H9v2H7V9H5V7h2V5z"/>
|
|
228
|
+
</svg>
|
|
229
|
+
</button>
|
|
230
|
+
<button
|
|
231
|
+
className={css.actionBtn}
|
|
232
|
+
title="Copy Relative Path"
|
|
233
|
+
onClick={(e) => {
|
|
234
|
+
e.stopPropagation()
|
|
235
|
+
onCopyRelativePath()
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
239
|
+
<path d="M4 4h6v1H4v9h7v-4h1v5H3V4h1zm3-2h7v7h-1V3.7L7.7 9 7 8.3 12.3 3H7V2z"/>
|
|
240
|
+
</svg>
|
|
241
|
+
</button>
|
|
242
|
+
<button
|
|
243
|
+
className={css.actionBtn}
|
|
244
|
+
title="Rename (F2)"
|
|
245
|
+
onClick={(e) => {
|
|
246
|
+
e.stopPropagation()
|
|
247
|
+
onStartRename?.()
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
<svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor">
|
|
251
|
+
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
|
252
|
+
</svg>
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react'
|
|
2
|
+
import css from '../explorer.module.css'
|
|
3
|
+
|
|
4
|
+
interface ExplorerSearchProps {
|
|
5
|
+
value: string
|
|
6
|
+
loading?: boolean
|
|
7
|
+
matchCount?: number | null
|
|
8
|
+
onChange: (val: string) => void
|
|
9
|
+
onClose: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ExplorerSearch: React.FC<ExplorerSearchProps> = ({
|
|
13
|
+
value,
|
|
14
|
+
loading = false,
|
|
15
|
+
matchCount = null,
|
|
16
|
+
onChange,
|
|
17
|
+
onClose,
|
|
18
|
+
}) => {
|
|
19
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
inputRef.current?.focus()
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
26
|
+
if (e.key === 'Escape') {
|
|
27
|
+
onClose()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={css.searchBar}>
|
|
33
|
+
<input
|
|
34
|
+
ref={inputRef}
|
|
35
|
+
type="text"
|
|
36
|
+
className={css.searchInput}
|
|
37
|
+
placeholder="Search files and folders by name..."
|
|
38
|
+
value={value}
|
|
39
|
+
onChange={(e) => onChange(e.target.value)}
|
|
40
|
+
onKeyDown={handleKeyDown}
|
|
41
|
+
/>
|
|
42
|
+
{loading && <div className={css.searchSpinner} title="Searching workspace..." />}
|
|
43
|
+
{!loading && matchCount !== null && value.trim() !== '' && (
|
|
44
|
+
<span className={css.matchCountBadge} title={`${matchCount} matches found`}>
|
|
45
|
+
{matchCount}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
{value && (
|
|
49
|
+
<button className={css.clearSearchBtn} title="Clear search (Esc)" onClick={() => onChange('')}>
|
|
50
|
+
✕
|
|
51
|
+
</button>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|