dsh-explorer 0.0.0-stage → 0.1.0

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,240 @@
1
+ import React, { useState, useRef, useEffect } from 'react'
2
+ import css from './explorer.module.css'
3
+ import type { FsEntry } from './api.ts'
4
+ import type { SingleClickAction } from './options.ts'
5
+ import { renderFileIcon } from './vscode-theme.tsx'
6
+ import { setupDragStart, setupDragEnd, type DragExplorerItem } from './drag-drop.ts'
7
+
8
+ interface ExplorerRowProps {
9
+ entry: FsEntry
10
+ depth: number
11
+ isExpanded: boolean
12
+ isSelected: boolean
13
+ isFocused: boolean
14
+ gitStatus?: string
15
+ relativePath: string
16
+ compactChain?: FsEntry[]
17
+ indentStep?: number
18
+ iconSize?: number
19
+ enableMotion?: boolean
20
+ showGitBadges?: boolean
21
+ compactFolders?: boolean
22
+ singleClickAction?: SingleClickAction
23
+ onToggle: () => void
24
+ onSelect: (e: React.MouseEvent) => void
25
+ onDoubleClick?: () => void
26
+ onContextMenu: (e: React.MouseEvent) => void
27
+ onInsertReference: () => void
28
+ onCopyRelativePath: () => void
29
+ onRenameSubmit: (newName: string) => void
30
+ onDelete: () => void
31
+ }
32
+
33
+ export const ExplorerRow: React.FC<ExplorerRowProps> = ({
34
+ entry,
35
+ depth,
36
+ isExpanded,
37
+ isSelected,
38
+ isFocused,
39
+ gitStatus,
40
+ relativePath,
41
+ compactChain,
42
+ indentStep = 16,
43
+ iconSize = 16,
44
+ enableMotion = true,
45
+ showGitBadges = true,
46
+ compactFolders = true,
47
+ singleClickAction = 'select',
48
+ onToggle,
49
+ onSelect,
50
+ onDoubleClick,
51
+ onContextMenu,
52
+ onInsertReference,
53
+ onCopyRelativePath,
54
+ onRenameSubmit,
55
+ onDelete,
56
+ }) => {
57
+ const [isRenaming, setIsRenaming] = useState(false)
58
+ const [renameVal, setRenameVal] = useState(entry.name)
59
+ const inputRef = useRef<HTMLInputElement>(null)
60
+
61
+ useEffect(() => {
62
+ if (isRenaming) {
63
+ inputRef.current?.focus()
64
+ inputRef.current?.select()
65
+ }
66
+ }, [isRenaming])
67
+
68
+ const handleDragStart = (e: React.DragEvent) => {
69
+ const item: DragExplorerItem = {
70
+ path: entry.path,
71
+ relativePath,
72
+ isDir: entry.isDir,
73
+ name: entry.name,
74
+ }
75
+ setupDragStart(e, [item])
76
+ }
77
+
78
+ const handleRenameKeyDown = (e: React.KeyboardEvent) => {
79
+ if (e.key === 'Enter') {
80
+ const trimmed = renameVal.trim()
81
+ if (trimmed && trimmed !== entry.name) {
82
+ onRenameSubmit(trimmed)
83
+ }
84
+ setIsRenaming(false)
85
+ } else if (e.key === 'Escape') {
86
+ setRenameVal(entry.name)
87
+ setIsRenaming(false)
88
+ }
89
+ }
90
+
91
+ // Display label: if compactFolders is on and compactChain has elements, join their names (e.g. "vscode/antigravity-quota-context")
92
+ const displayName = compactFolders && compactChain && compactChain.length > 1
93
+ ? compactChain.map((c) => c.name).join('/')
94
+ : (compactFolders && entry.compactName
95
+ ? entry.compactName
96
+ : entry.name)
97
+
98
+ // Git status styling
99
+ let gitClass = ''
100
+ let gitLetter = ''
101
+ if (gitStatus) {
102
+ if (gitStatus.includes('M')) {
103
+ gitClass = css.gitModified
104
+ gitLetter = 'M'
105
+ } else if (gitStatus.includes('?') || gitStatus.includes('A')) {
106
+ gitClass = css.gitUntracked
107
+ gitLetter = 'U'
108
+ } else if (gitStatus.includes('D')) {
109
+ gitClass = css.gitDeleted
110
+ gitLetter = 'D'
111
+ }
112
+ }
113
+
114
+ const isIgnored = gitStatus?.includes('!')
115
+
116
+ return (
117
+ <div
118
+ className={`${css.row} ${isSelected ? css.rowSelected : ''} ${isFocused ? css.rowFocused : ''}`}
119
+ style={{ paddingLeft: `${depth * indentStep + 8}px` }}
120
+ onClick={(e) => {
121
+ onSelect(e)
122
+ if (entry.isDir) {
123
+ onToggle()
124
+ } else if (singleClickAction === 'mention') {
125
+ onInsertReference()
126
+ }
127
+ }}
128
+ onDoubleClick={(e) => {
129
+ if (!entry.isDir) {
130
+ e.stopPropagation()
131
+ onDoubleClick?.()
132
+ }
133
+ }}
134
+ onContextMenu={onContextMenu}
135
+ draggable={!isRenaming}
136
+ onDragStart={handleDragStart}
137
+ onDragEnd={setupDragEnd}
138
+ >
139
+ {/* Indentation guide lines */}
140
+ {Array.from({ length: depth }).map((_, i) => (
141
+ <span
142
+ key={i}
143
+ className={css.indentGuide}
144
+ style={{ left: `${i * indentStep + 15}px` }}
145
+ />
146
+ ))}
147
+
148
+ {/* Expand/Collapse Chevron */}
149
+ <span
150
+ className={`${css.chevron} ${isExpanded ? css.chevronExpanded : ''} ${!entry.isDir ? css.chevronHidden : ''}`}
151
+ style={!enableMotion ? { transition: 'none' } : undefined}
152
+ onClick={(e) => {
153
+ e.stopPropagation()
154
+ onToggle()
155
+ }}
156
+ >
157
+ <svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor">
158
+ <path d="M6 3.5L10.5 8 6 12.5V3.5Z" />
159
+ </svg>
160
+ </span>
161
+
162
+ {/* Language / Folder Icon */}
163
+ <span className={css.iconWrap}>
164
+ {renderFileIcon(entry.name, entry.isDir, isExpanded, iconSize)}
165
+ </span>
166
+
167
+ {/* Label or Inline Rename */}
168
+ {isRenaming ? (
169
+ <input
170
+ ref={inputRef}
171
+ type="text"
172
+ className={css.inlineInput}
173
+ value={renameVal}
174
+ onChange={(e) => setRenameVal(e.target.value)}
175
+ onKeyDown={handleRenameKeyDown}
176
+ onBlur={() => {
177
+ const trimmed = renameVal.trim()
178
+ if (trimmed && trimmed !== entry.name) {
179
+ onRenameSubmit(trimmed)
180
+ }
181
+ setIsRenaming(false)
182
+ }}
183
+ onClick={(e) => e.stopPropagation()}
184
+ />
185
+ ) : (
186
+ <span
187
+ className={`${css.label} ${isIgnored ? css.labelDimmed : ''} ${showGitBadges ? gitClass : ''}`}
188
+ title={relativePath}
189
+ >
190
+ {displayName}
191
+ </span>
192
+ )}
193
+
194
+ {/* Git status badge */}
195
+ {showGitBadges && gitLetter && <span className={`${css.gitBadge} ${gitClass}`}>{gitLetter}</span>}
196
+
197
+ {/* Hover actions */}
198
+ {!isRenaming && (
199
+ <div className={css.hoverActions} onClick={(e) => e.stopPropagation()}>
200
+ <button
201
+ className={css.actionBtn}
202
+ title="Mention in Chat (@)"
203
+ onClick={(e) => {
204
+ e.stopPropagation()
205
+ onInsertReference()
206
+ }}
207
+ >
208
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
209
+ <path d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2zM7 5h2v2h2v2H9v2H7V9H5V7h2V5z"/>
210
+ </svg>
211
+ </button>
212
+ <button
213
+ className={css.actionBtn}
214
+ title="Copy Relative Path"
215
+ onClick={(e) => {
216
+ e.stopPropagation()
217
+ onCopyRelativePath()
218
+ }}
219
+ >
220
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
221
+ <path d="M4 4h6v1H4v9h7v-4h1v5H3V4h1zm3-2h7v7h-1V3.7L7.7 9 7 8.3 12.3 3H7V2z"/>
222
+ </svg>
223
+ </button>
224
+ <button
225
+ className={css.actionBtn}
226
+ title="Rename (F2)"
227
+ onClick={(e) => {
228
+ e.stopPropagation()
229
+ setIsRenaming(true)
230
+ }}
231
+ >
232
+ <svg width="11" height="11" viewBox="0 0 16 16" fill="currentColor">
233
+ <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"/>
234
+ </svg>
235
+ </button>
236
+ </div>
237
+ )}
238
+ </div>
239
+ )
240
+ }
@@ -0,0 +1,42 @@
1
+ import React, { useRef, useEffect } from 'react'
2
+ import css from './explorer.module.css'
3
+
4
+ interface ExplorerSearchProps {
5
+ value: string
6
+ onChange: (val: string) => void
7
+ onClose: () => void
8
+ }
9
+
10
+ export const ExplorerSearch: React.FC<ExplorerSearchProps> = ({ value, onChange, onClose }) => {
11
+ const inputRef = useRef<HTMLInputElement>(null)
12
+
13
+ useEffect(() => {
14
+ inputRef.current?.focus()
15
+ }, [])
16
+
17
+ const handleKeyDown = (e: React.KeyboardEvent) => {
18
+ if (e.key === 'Escape') {
19
+ onClose()
20
+ }
21
+ }
22
+
23
+ return (
24
+ <div className={css.searchBar}>
25
+ <input
26
+ ref={inputRef}
27
+ type="text"
28
+ className={css.searchInput}
29
+ placeholder="Filter by name (fuzzy)..."
30
+ value={value}
31
+ onChange={(e) => onChange(e.target.value)}
32
+ onKeyDown={handleKeyDown}
33
+ />
34
+ {value && (
35
+ <button className={css.clearSearchBtn} title="Clear filter" onClick={() => onChange('')}>
36
+ ✕
37
+ </button>
38
+ )}
39
+ </div>
40
+ )
41
+ }
42
+