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.
@@ -1,13 +1,21 @@
1
1
  import React, { useRef, useEffect } from 'react'
2
- import css from './explorer.module.css'
2
+ import css from '../explorer.module.css'
3
3
 
4
4
  interface ExplorerSearchProps {
5
5
  value: string
6
+ loading?: boolean
7
+ matchCount?: number | null
6
8
  onChange: (val: string) => void
7
9
  onClose: () => void
8
10
  }
9
11
 
10
- export const ExplorerSearch: React.FC<ExplorerSearchProps> = ({ value, onChange, onClose }) => {
12
+ export const ExplorerSearch: React.FC<ExplorerSearchProps> = ({
13
+ value,
14
+ loading = false,
15
+ matchCount = null,
16
+ onChange,
17
+ onClose,
18
+ }) => {
11
19
  const inputRef = useRef<HTMLInputElement>(null)
12
20
 
13
21
  useEffect(() => {
@@ -26,17 +34,22 @@ export const ExplorerSearch: React.FC<ExplorerSearchProps> = ({ value, onChange,
26
34
  ref={inputRef}
27
35
  type="text"
28
36
  className={css.searchInput}
29
- placeholder="Filter by name (fuzzy)..."
37
+ placeholder="Search files and folders by name..."
30
38
  value={value}
31
39
  onChange={(e) => onChange(e.target.value)}
32
40
  onKeyDown={handleKeyDown}
33
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
+ )}
34
48
  {value && (
35
- <button className={css.clearSearchBtn} title="Clear filter" onClick={() => onChange('')}>
49
+ <button className={css.clearSearchBtn} title="Clear search (Esc)" onClick={() => onChange('')}>
36
50
  ✕
37
51
  </button>
38
52
  )}
39
53
  </div>
40
54
  )
41
55
  }
42
-
@@ -0,0 +1,384 @@
1
+ import React, { useEffect, useMemo } from 'react'
2
+ import css from '../explorer.module.css'
3
+ import type { FsEntry, FsListing } from '../utils/api.ts'
4
+ import type { ExplorerOptions } from '../utils/options.ts'
5
+ import { ExplorerRow } from './ExplorerRow.tsx'
6
+
7
+ export interface TreeNodeProps {
8
+ entry: FsEntry
9
+ depth: number
10
+ parentDir: string
11
+ rootPath: string
12
+ levels: Record<string, FsListing>
13
+ expanded: Set<string>
14
+ selected: Set<string>
15
+ focusedPath: string | null
16
+ renamingPath: string | null
17
+ gitStatusMap: Map<string, string>
18
+ filterText: string
19
+ matchingPaths?: Set<string> | null
20
+ ancestorPaths?: Set<string> | null
21
+ creating: { parentDir: string; isDir: boolean } | null
22
+ creatingName: string
23
+ createInputRef: React.RefObject<HTMLInputElement>
24
+ options: ExplorerOptions
25
+ indentStep: number
26
+ iconSize: number
27
+ reloadDirectory: (dir: string) => Promise<void>
28
+ onToggle: (path: string) => void
29
+ onSelect: (path: string, e: React.MouseEvent) => void
30
+ onOpenFile: (filePath: string, relPath: string) => void
31
+ onContextMenu: (entry: FsEntry, relPath: string, e: React.MouseEvent) => void
32
+ onInsertReference: (relPath: string, isDir: boolean) => void
33
+ onCopyRelativePath: (relPath: string) => void
34
+ onStartRename: (path: string) => void
35
+ onCancelRename: () => void
36
+ onRenameSubmit: (path: string, newName: string) => void
37
+ onDelete: (entry: FsEntry, relPath: string) => void
38
+ onCreatingNameChange: (val: string) => void
39
+ onCreatingKeyDown: (e: React.KeyboardEvent) => void
40
+ onCreatingBlur: () => void
41
+ getRelativePath: (path: string) => string
42
+ }
43
+
44
+ export const TreeNode: React.FC<TreeNodeProps> = React.memo(({
45
+ entry,
46
+ depth,
47
+ rootPath,
48
+ levels,
49
+ expanded,
50
+ selected,
51
+ focusedPath,
52
+ renamingPath,
53
+ gitStatusMap,
54
+ filterText,
55
+ matchingPaths,
56
+ ancestorPaths,
57
+ creating,
58
+ creatingName,
59
+ createInputRef,
60
+ options,
61
+ indentStep,
62
+ iconSize,
63
+ reloadDirectory,
64
+ onToggle,
65
+ onSelect,
66
+ onOpenFile,
67
+ onContextMenu,
68
+ onInsertReference,
69
+ onCopyRelativePath,
70
+ onStartRename,
71
+ onCancelRename,
72
+ onRenameSubmit,
73
+ onDelete,
74
+ onCreatingNameChange,
75
+ onCreatingKeyDown,
76
+ onCreatingBlur,
77
+ getRelativePath,
78
+ }) => {
79
+ const relPath = getRelativePath(entry?.path ?? '')
80
+ const normPathLower = (entry?.path ?? '').replace(/\\/g, '/').toLowerCase()
81
+ const listing =
82
+ levels[entry?.path ?? ''] ??
83
+ levels[(entry?.path ?? '').replace(/\\/g, '/')] ??
84
+ levels[(entry?.path ?? '').replace(/\//g, '\\')] ??
85
+ (normPathLower ? Object.entries(levels).find(([k]) => k.replace(/\\/g, '/').toLowerCase() === normPathLower)?.[1] : undefined)
86
+ const hasLoaded = listing !== undefined
87
+
88
+ const compactChain = useMemo(() => {
89
+ if (!options.compactFolders || !entry?.isDir) return undefined
90
+ const chain: FsEntry[] = [entry]
91
+ let curr = entry
92
+ let currListing = levels[curr.path] ?? levels[curr.path.replace(/\\/g, '/')] ?? levels[curr.path.replace(/\//g, '\\')]
93
+
94
+ while (currListing && Array.isArray(currListing.entries)) {
95
+ const valid = options.showHiddenFiles
96
+ ? currListing.entries
97
+ : currListing.entries.filter((c) => c && !c.hidden && !c.name.startsWith('.'))
98
+ if (valid.length === 1 && valid[0]!.isDir) {
99
+ const next = valid[0]!
100
+ chain.push(next)
101
+ curr = next
102
+ currListing = levels[curr.path] ?? levels[curr.path.replace(/\\/g, '/')] ?? levels[curr.path.replace(/\//g, '\\')]
103
+ } else {
104
+ break
105
+ }
106
+ }
107
+
108
+ return chain.length > 1 ? chain : undefined
109
+ }, [options.compactFolders, options.showHiddenFiles, entry, levels])
110
+
111
+ const isCompacted = compactChain !== undefined && compactChain.length > 1
112
+ const effectiveEntry = isCompacted ? compactChain[compactChain.length - 1]! : entry
113
+ const targetPath = effectiveEntry?.path ?? ''
114
+ const targetRelPath = getRelativePath(targetPath) || ''
115
+ const normRel = targetRelPath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '').toLowerCase()
116
+ const normRelOrig = (relPath || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '').toLowerCase()
117
+ const normTarget = (targetPath || '').replace(/\\/g, '/').toLowerCase()
118
+ const normSelf = (entry?.path || '').replace(/\\/g, '/').toLowerCase()
119
+
120
+ const isSearchActive = Boolean(filterText && matchingPaths)
121
+
122
+ const isDirectMatch = Boolean(
123
+ isSearchActive &&
124
+ matchingPaths &&
125
+ (matchingPaths.has(normRel) ||
126
+ matchingPaths.has(normRelOrig) ||
127
+ matchingPaths.has(normTarget) ||
128
+ matchingPaths.has(normSelf) ||
129
+ (entry?.path && matchingPaths.has(entry.path)) ||
130
+ (entry?.path && matchingPaths.has(entry.path.toLowerCase())) ||
131
+ (targetPath && matchingPaths.has(targetPath)) ||
132
+ (targetPath && matchingPaths.has(targetPath.toLowerCase())))
133
+ )
134
+
135
+ const isAncestor = Boolean(
136
+ isSearchActive &&
137
+ ancestorPaths &&
138
+ (ancestorPaths.has(normRel) ||
139
+ ancestorPaths.has(normRelOrig) ||
140
+ ancestorPaths.has(normTarget) ||
141
+ ancestorPaths.has(normSelf) ||
142
+ (entry?.path && ancestorPaths.has(entry.path)) ||
143
+ (entry?.path && ancestorPaths.has(entry.path.toLowerCase())) ||
144
+ (targetPath && ancestorPaths.has(targetPath)) ||
145
+ (targetPath && ancestorPaths.has(targetPath.toLowerCase())))
146
+ )
147
+
148
+ const normTargetLower = (targetPath || '').replace(/\\/g, '/').toLowerCase()
149
+ const targetListing =
150
+ levels[targetPath] ??
151
+ levels[targetPath.replace(/\\/g, '/')] ??
152
+ levels[targetPath.replace(/\//g, '\\')] ??
153
+ (normTargetLower ? Object.entries(levels).find(([k]) => k.replace(/\\/g, '/').toLowerCase() === normTargetLower)?.[1] : undefined)
154
+ const hasTargetLoaded = targetListing !== undefined
155
+
156
+ const isExpanded = Boolean(
157
+ entry?.isDir && (
158
+ isAncestor ||
159
+ expanded.has(entry.path) ||
160
+ expanded.has(targetPath) ||
161
+ (normSelf && expanded.has(normSelf)) ||
162
+ (normTarget && expanded.has(normTarget)) ||
163
+ (entry?.path && expanded.has(entry.path.toLowerCase())) ||
164
+ (targetPath && expanded.has(targetPath.toLowerCase()))
165
+ )
166
+ )
167
+
168
+ let shouldRender = true
169
+ if (!entry || (!options.showHiddenFiles && (entry.hidden || entry.name.startsWith('.')))) {
170
+ shouldRender = false
171
+ } else if (isSearchActive) {
172
+ if (entry.isDir) {
173
+ if (!isDirectMatch && !isAncestor) {
174
+ shouldRender = false
175
+ }
176
+ } else {
177
+ if (!isDirectMatch) {
178
+ shouldRender = false
179
+ }
180
+ }
181
+ } else if (filterText) {
182
+ const selfMatches =
183
+ entry.name.toLowerCase().includes(filterText.toLowerCase()) ||
184
+ relPath.toLowerCase().includes(filterText.toLowerCase())
185
+ if (!selfMatches && !entry.isDir) {
186
+ shouldRender = false
187
+ }
188
+ }
189
+
190
+ const inFlightDirRef = React.useRef<string | null>(null)
191
+
192
+ useEffect(() => {
193
+ if (!shouldRender || !entry || !entry.isDir) return
194
+ if (isExpanded && !hasTargetLoaded && inFlightDirRef.current !== targetPath) {
195
+ inFlightDirRef.current = targetPath
196
+ reloadDirectory(targetPath).finally(() => {
197
+ if (inFlightDirRef.current === targetPath) {
198
+ inFlightDirRef.current = null
199
+ }
200
+ })
201
+ }
202
+ }, [shouldRender, isExpanded, targetPath, entry, hasTargetLoaded, reloadDirectory])
203
+
204
+ useEffect(() => {
205
+ if (!shouldRender || !entry) return
206
+ if (options.compactFolders && entry.isDir && entry.compact && !hasLoaded) {
207
+ reloadDirectory(entry.path)
208
+ }
209
+ }, [shouldRender, options.compactFolders, entry, hasLoaded, reloadDirectory])
210
+
211
+ if (!entry || !shouldRender) {
212
+ return null
213
+ }
214
+
215
+ const isSel = selected.has(entry.path) || selected.has(targetPath)
216
+ const isFoc = focusedPath === entry.path || focusedPath === targetPath
217
+ const status =
218
+ gitStatusMap?.get(normRel) ??
219
+ gitStatusMap?.get(normRelOrig) ??
220
+ gitStatusMap?.get(normTarget) ??
221
+ gitStatusMap?.get(normSelf)
222
+
223
+ const handleToggle = () => {
224
+ if (isCompacted && targetPath !== entry.path) {
225
+ const isCurrentlyExpanded = expanded.has(entry.path) || expanded.has(targetPath)
226
+ if (isCurrentlyExpanded) {
227
+ if (expanded.has(entry.path)) onToggle(entry.path)
228
+ if (expanded.has(targetPath)) onToggle(targetPath)
229
+ } else {
230
+ onToggle(entry.path)
231
+ onToggle(targetPath)
232
+ }
233
+ } else {
234
+ onToggle(entry.path)
235
+ }
236
+ }
237
+
238
+ if (!entry.isDir) {
239
+ return (
240
+ <ExplorerRow
241
+ entry={entry}
242
+ depth={depth}
243
+ isExpanded={false}
244
+ isSelected={isSel}
245
+ isFocused={isFoc}
246
+ isRenaming={renamingPath === entry.path}
247
+ gitStatus={status}
248
+ relativePath={relPath}
249
+ indentStep={indentStep}
250
+ iconSize={iconSize}
251
+ enableMotion={options.enableMotion}
252
+ showGitBadges={options.showGitBadges}
253
+ compactFolders={options.compactFolders}
254
+ singleClickAction={options.singleClickAction}
255
+ onToggle={() => {}}
256
+ onSelect={(e) => onSelect(entry.path, e)}
257
+ onDoubleClick={() => onOpenFile(entry.path, relPath)}
258
+ onContextMenu={(e) => onContextMenu(entry, relPath, e)}
259
+ onInsertReference={() => onInsertReference(relPath, false)}
260
+ onCopyRelativePath={() => onCopyRelativePath(relPath)}
261
+ onStartRename={() => onStartRename(entry.path)}
262
+ onCancelRename={onCancelRename}
263
+ onRenameSubmit={(newName) => onRenameSubmit(entry.path, newName)}
264
+ onDelete={() => onDelete(entry, relPath)}
265
+ />
266
+ )
267
+ }
268
+
269
+ const visibleChildren = hasTargetLoaded && Array.isArray(targetListing?.entries)
270
+ ? options.showHiddenFiles
271
+ ? targetListing.entries
272
+ : targetListing.entries.filter((c) => c && !c.hidden && !c.name.startsWith('.'))
273
+ : []
274
+
275
+ const isRenamingFolder = renamingPath === entry.path || (isCompacted && renamingPath === targetPath)
276
+
277
+ return (
278
+ <div className={css.folderGroup}>
279
+ <ExplorerRow
280
+ entry={entry}
281
+ depth={depth}
282
+ isExpanded={isExpanded}
283
+ isSelected={isSel}
284
+ isFocused={isFoc}
285
+ isRenaming={isRenamingFolder}
286
+ gitStatus={status}
287
+ relativePath={targetRelPath}
288
+ compactChain={compactChain}
289
+ indentStep={indentStep}
290
+ iconSize={iconSize}
291
+ enableMotion={options.enableMotion}
292
+ showGitBadges={options.showGitBadges}
293
+ compactFolders={options.compactFolders}
294
+ singleClickAction={options.singleClickAction}
295
+ onToggle={handleToggle}
296
+ onSelect={(e) => onSelect(targetPath, e)}
297
+ onContextMenu={(e) => onContextMenu(entry, targetRelPath, e)}
298
+ onInsertReference={() => onInsertReference(targetRelPath, true)}
299
+ onCopyRelativePath={() => onCopyRelativePath(targetRelPath)}
300
+ onStartRename={() => onStartRename(isCompacted ? targetPath : entry.path)}
301
+ onCancelRename={onCancelRename}
302
+ onRenameSubmit={(newName) => onRenameSubmit(isCompacted ? targetPath : entry.path, newName)}
303
+ onDelete={() => onDelete(entry, targetRelPath)}
304
+ />
305
+
306
+ <div
307
+ className={`${css.folderChildren} ${isExpanded ? css.folderChildrenOpen : css.folderChildrenClosed}`}
308
+ style={!options.enableMotion ? { transition: 'none' } : undefined}
309
+ >
310
+ <div className={css.folderChildrenInner}>
311
+ {creating && (creating.parentDir === entry.path || creating.parentDir === targetPath) && (
312
+ <div className={css.row} style={{ paddingLeft: `${(depth + 1) * indentStep + 8}px` }}>
313
+ <span className={css.iconWrap}>{creating.isDir ? '📁' : '📄'}</span>
314
+ <input
315
+ ref={createInputRef}
316
+ autoFocus
317
+ type="text"
318
+ className={css.inlineInput}
319
+ placeholder={creating.isDir ? 'Folder name...' : 'File name...'}
320
+ value={creatingName}
321
+ onChange={(e) => onCreatingNameChange(e.target.value)}
322
+ onKeyDown={onCreatingKeyDown}
323
+ onBlur={onCreatingBlur}
324
+ />
325
+ </div>
326
+ )}
327
+ {visibleChildren.length > 0 ? (
328
+ visibleChildren.map((child) => (
329
+ <TreeNode
330
+ key={child.path}
331
+ entry={child}
332
+ depth={depth + 1}
333
+ parentDir={targetPath}
334
+ rootPath={rootPath}
335
+ levels={levels}
336
+ expanded={expanded}
337
+ selected={selected}
338
+ focusedPath={focusedPath}
339
+ renamingPath={renamingPath}
340
+ gitStatusMap={gitStatusMap}
341
+ filterText={filterText}
342
+ matchingPaths={matchingPaths}
343
+ ancestorPaths={ancestorPaths}
344
+ creating={creating}
345
+ creatingName={creatingName}
346
+ createInputRef={createInputRef}
347
+ options={options}
348
+ indentStep={indentStep}
349
+ iconSize={iconSize}
350
+ reloadDirectory={reloadDirectory}
351
+ onToggle={onToggle}
352
+ onSelect={onSelect}
353
+ onOpenFile={onOpenFile}
354
+ onContextMenu={onContextMenu}
355
+ onInsertReference={onInsertReference}
356
+ onCopyRelativePath={onCopyRelativePath}
357
+ onStartRename={onStartRename}
358
+ onCancelRename={onCancelRename}
359
+ onRenameSubmit={onRenameSubmit}
360
+ onDelete={onDelete}
361
+ onCreatingNameChange={onCreatingNameChange}
362
+ onCreatingKeyDown={onCreatingKeyDown}
363
+ onCreatingBlur={onCreatingBlur}
364
+ getRelativePath={getRelativePath}
365
+ />
366
+ ))
367
+ ) : !isSearchActive && hasTargetLoaded && targetListing.entries && targetListing.entries.length === 0 ? (
368
+ <div
369
+ className={css.row}
370
+ style={{
371
+ paddingLeft: `${(depth + 1) * indentStep + 8}px`,
372
+ opacity: 0.45,
373
+ fontStyle: 'italic',
374
+ fontSize: '12px',
375
+ }}
376
+ >
377
+ Empty folder
378
+ </div>
379
+ ) : null}
380
+ </div>
381
+ </div>
382
+ </div>
383
+ )
384
+ })
@@ -17,40 +17,47 @@
17
17
  display: flex;
18
18
  align-items: center;
19
19
  justify-content: space-between;
20
- padding: 6px 12px;
21
- font-size: 11px;
22
- font-weight: 700;
23
- text-transform: uppercase;
24
- letter-spacing: 0.05em;
20
+ padding: 8px 10px;
21
+ min-height: 38px;
22
+ font-size: 11.5px;
23
+ font-weight: 600;
25
24
  color: var(--dsh-fg-muted, #858585);
26
25
  border-bottom: 1px solid var(--dsh-border, rgba(255, 255, 255, 0.08));
27
26
  flex-shrink: 0;
27
+ gap: 8px;
28
28
  }
29
29
 
30
30
  .headerTitle {
31
31
  overflow: hidden;
32
32
  text-overflow: ellipsis;
33
33
  white-space: nowrap;
34
+ min-width: 0;
35
+ flex: 1;
36
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
37
+ font-size: 11px;
38
+ color: var(--dsh-fg-base, #cccccc);
39
+ letter-spacing: normal;
34
40
  }
35
41
 
36
42
  .headerActions {
37
43
  display: flex;
38
44
  align-items: center;
39
- gap: 4px;
45
+ gap: 3px;
46
+ flex-shrink: 0;
40
47
  }
41
48
 
42
49
  .iconBtn {
43
50
  display: flex;
44
51
  align-items: center;
45
52
  justify-content: center;
46
- width: 22px;
47
- height: 22px;
48
- border-radius: 4px;
53
+ width: 26px;
54
+ height: 26px;
55
+ border-radius: 5px;
49
56
  border: none;
50
57
  background: transparent;
51
58
  color: inherit;
52
59
  cursor: pointer;
53
- opacity: 0.75;
60
+ opacity: 0.8;
54
61
  transition: opacity 0.15s cubic-bezier(0.2, 0, 0, 1), background-color 0.15s cubic-bezier(0.2, 0, 0, 1), transform 0.12s ease;
55
62
  }
56
63
 
@@ -120,6 +127,32 @@
120
127
  opacity: 1;
121
128
  }
122
129
 
130
+ .searchSpinner {
131
+ width: 13px;
132
+ height: 13px;
133
+ border: 2px solid rgba(255, 255, 255, 0.2);
134
+ border-top-color: #007fd4;
135
+ border-radius: 50%;
136
+ animation: searchSpin 0.6s linear infinite;
137
+ flex-shrink: 0;
138
+ }
139
+
140
+ @keyframes searchSpin {
141
+ to {
142
+ transform: rotate(360deg);
143
+ }
144
+ }
145
+
146
+ .matchCountBadge {
147
+ font-size: 11px;
148
+ opacity: 0.7;
149
+ background: var(--dsh-badge-bg, rgba(255, 255, 255, 0.08));
150
+ border-radius: 3px;
151
+ padding: 1px 6px;
152
+ font-variant-numeric: tabular-nums;
153
+ white-space: nowrap;
154
+ }
155
+
123
156
  .treeBody {
124
157
  flex: 1;
125
158
  overflow-y: auto;
@@ -626,3 +659,175 @@
626
659
  .switchKnobActive {
627
660
  transform: translateX(14px);
628
661
  }
662
+
663
+ .modalOverlay {
664
+ position: absolute;
665
+ inset: 0;
666
+ background-color: rgba(0, 0, 0, 0.65);
667
+ backdrop-filter: blur(2px);
668
+ display: flex;
669
+ align-items: center;
670
+ justify-content: center;
671
+ z-index: 1200;
672
+ padding: 16px;
673
+ animation: modalFadeIn 0.15s cubic-bezier(0, 0, 0.2, 1);
674
+ }
675
+
676
+ @keyframes modalFadeIn {
677
+ from {
678
+ opacity: 0;
679
+ }
680
+ to {
681
+ opacity: 1;
682
+ }
683
+ }
684
+
685
+ .modalCard {
686
+ background-color: var(--dsh-bg-elevated, #202020);
687
+ border: 1px solid var(--dsh-border, rgba(255, 255, 255, 0.14));
688
+ border-radius: 8px;
689
+ width: 100%;
690
+ max-width: 320px;
691
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.65), 0 2px 10px rgba(0, 0, 0, 0.45);
692
+ overflow: hidden;
693
+ display: flex;
694
+ flex-direction: column;
695
+ animation: modalCardScale 0.16s cubic-bezier(0.16, 1, 0.3, 1);
696
+ }
697
+
698
+ @keyframes modalCardScale {
699
+ from {
700
+ opacity: 0;
701
+ transform: scale(0.95) translateY(-4px);
702
+ }
703
+ to {
704
+ opacity: 1;
705
+ transform: scale(1) translateY(0);
706
+ }
707
+ }
708
+
709
+ .modalHeader {
710
+ display: flex;
711
+ align-items: center;
712
+ justify-content: space-between;
713
+ padding: 10px 14px;
714
+ background-color: rgba(0, 0, 0, 0.25);
715
+ border-bottom: 1px solid var(--dsh-border, rgba(255, 255, 255, 0.08));
716
+ }
717
+
718
+ .modalTitleWrap {
719
+ display: flex;
720
+ align-items: center;
721
+ gap: 7px;
722
+ font-size: 12px;
723
+ font-weight: 600;
724
+ color: #ff5252;
725
+ letter-spacing: 0.02em;
726
+ }
727
+
728
+ .modalBody {
729
+ padding: 14px;
730
+ display: flex;
731
+ flex-direction: column;
732
+ gap: 10px;
733
+ font-size: 12px;
734
+ color: var(--dsh-fg-base, #cccccc);
735
+ line-height: 1.45;
736
+ }
737
+
738
+ .modalItemPreview {
739
+ display: flex;
740
+ align-items: center;
741
+ gap: 8px;
742
+ padding: 8px 10px;
743
+ background-color: rgba(0, 0, 0, 0.25);
744
+ border: 1px solid rgba(255, 255, 255, 0.08);
745
+ border-radius: 6px;
746
+ }
747
+
748
+ .modalItemInfo {
749
+ display: flex;
750
+ flex-direction: column;
751
+ gap: 2px;
752
+ min-width: 0;
753
+ flex: 1;
754
+ }
755
+
756
+ .modalItemName {
757
+ font-weight: 600;
758
+ color: #ffffff;
759
+ overflow: hidden;
760
+ text-overflow: ellipsis;
761
+ white-space: nowrap;
762
+ }
763
+
764
+ .modalItemPath {
765
+ font-size: 10.5px;
766
+ color: var(--dsh-fg-muted, #858585);
767
+ overflow: hidden;
768
+ text-overflow: ellipsis;
769
+ white-space: nowrap;
770
+ }
771
+
772
+ .modalWarning {
773
+ font-size: 11px;
774
+ color: #e0a443;
775
+ line-height: 1.35;
776
+ }
777
+
778
+ .modalErrorBanner {
779
+ padding: 6px 10px;
780
+ border-radius: 4px;
781
+ background-color: rgba(244, 67, 54, 0.15);
782
+ border: 1px solid rgba(244, 67, 54, 0.3);
783
+ color: #ff8a80;
784
+ font-size: 11.5px;
785
+ }
786
+
787
+ .modalFooter {
788
+ display: flex;
789
+ align-items: center;
790
+ justify-content: flex-end;
791
+ gap: 8px;
792
+ padding: 10px 14px;
793
+ background-color: rgba(0, 0, 0, 0.2);
794
+ border-top: 1px solid var(--dsh-border, rgba(255, 255, 255, 0.06));
795
+ }
796
+
797
+ .modalBtnCancel {
798
+ padding: 5px 12px;
799
+ font-size: 12px;
800
+ font-weight: 500;
801
+ border-radius: 4px;
802
+ border: 1px solid rgba(255, 255, 255, 0.15);
803
+ background: transparent;
804
+ color: var(--dsh-fg-base, #cccccc);
805
+ cursor: pointer;
806
+ transition: all 0.15s ease;
807
+ }
808
+
809
+ .modalBtnCancel:hover {
810
+ background-color: rgba(255, 255, 255, 0.08);
811
+ color: #ffffff;
812
+ }
813
+
814
+ .modalBtnDanger {
815
+ padding: 5px 14px;
816
+ font-size: 12px;
817
+ font-weight: 600;
818
+ border-radius: 4px;
819
+ border: none;
820
+ background-color: #d32f2f;
821
+ color: #ffffff;
822
+ cursor: pointer;
823
+ transition: background-color 0.15s ease, opacity 0.15s ease;
824
+ }
825
+
826
+ .modalBtnDanger:hover {
827
+ background-color: #e53935;
828
+ }
829
+
830
+ .modalBtnDanger:disabled {
831
+ opacity: 0.6;
832
+ cursor: not-allowed;
833
+ }