@civilization/dsh-workspace-files 0.3.1 → 0.3.3
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 +8 -0
- package/lib/client.js +618 -558
- package/lib/client.js.map +1 -1
- package/lib/index.js +47 -43
- package/package.json +1 -1
- package/src/client/icons.tsx +39 -0
- package/src/client/index.tsx +562 -576
- package/src/files.ts +14 -0
- package/src/index.ts +37 -34
package/src/client/index.tsx
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser entry for the enhanced workspace files explorer plugin.
|
|
3
|
+
* High-performance virtualized flat-row rendering with DSH Design Tokens and UI Motion.
|
|
3
4
|
* Overrides DSH official 'files' tab using 'priority: extension'.
|
|
4
5
|
*/
|
|
5
6
|
import type { Context } from '@deepseek-ai/cordis'
|
|
6
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
7
|
+
import React, { memo, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
|
|
8
|
+
import type { JSX } from 'react'
|
|
7
9
|
import {
|
|
8
10
|
FileGlyphIcon,
|
|
9
11
|
IconAt,
|
|
10
12
|
IconCheck,
|
|
11
|
-
|
|
13
|
+
IconChevronRight,
|
|
12
14
|
IconClose,
|
|
13
15
|
IconCollapseAll,
|
|
14
16
|
IconCopy,
|
|
@@ -24,6 +26,9 @@ import {
|
|
|
24
26
|
|
|
25
27
|
export const inject = ['slots', 'sidebarRightTabs']
|
|
26
28
|
|
|
29
|
+
/** 构建时由 tsdown define 注入,避免卡片上的版本号与 package.json 漂移。 */
|
|
30
|
+
declare const __PLUGIN_VERSION__: string
|
|
31
|
+
|
|
27
32
|
let globalCtx: Context | null = null
|
|
28
33
|
|
|
29
34
|
const OFFICIAL_FILES_TAB_ID = '@civilization/dsh-workspace-files'
|
|
@@ -90,7 +95,6 @@ export function apply(ctx: Context): void {
|
|
|
90
95
|
const slots = (ctx as any).slots
|
|
91
96
|
|
|
92
97
|
if (sidebarRightTabs && slots) {
|
|
93
|
-
// 关键:kind: 'files', priority: 'extension'(Rank 3 > builtin 2),完美接管官方文件标签页
|
|
94
98
|
ctx.effect(() => sidebarRightTabs.register({
|
|
95
99
|
id: OFFICIAL_FILES_TAB_ID,
|
|
96
100
|
kind: 'files',
|
|
@@ -115,28 +119,16 @@ export function apply(ctx: Context): void {
|
|
|
115
119
|
}, WorkspaceFilesTabTitle)), 'dsh-workspace-files: official tab title')
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
targetCtx.effect?.(() => targetCtx.slots.inject('settings.plugin.item', function* () {
|
|
122
|
-
yield targetCtx.slots.register({
|
|
123
|
-
name: 'settings.plugin.item',
|
|
124
|
-
key: 'workspace-files',
|
|
125
|
-
}, WorkspaceFilesSettingsCard)
|
|
126
|
-
yield targetCtx.slots.register({
|
|
127
|
-
name: 'settings.plugin.item',
|
|
128
|
-
key: '@civilization/dsh-workspace-files',
|
|
129
|
-
}, WorkspaceFilesSettingsCard)
|
|
130
|
-
}), 'dsh-workspace-files: settings card')
|
|
131
|
-
}
|
|
132
|
-
|
|
122
|
+
// 设置中心“插件”页的自定义卡片:DSH 0.2.x 用 settings.plugins.tab(list 槽)承载
|
|
123
|
+
// 各插件自己的配置视图,注册需要 id/order/label;旧版的 settings.plugin.item 槽与
|
|
124
|
+
// settingsScope 服务在 0.2.0 已不存在。
|
|
133
125
|
if (slots) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
})
|
|
126
|
+
ctx.effect(() => slots.inject('settings.plugins.tab', () => slots.register({
|
|
127
|
+
name: 'settings.plugins.tab',
|
|
128
|
+
id: 'workspace-files',
|
|
129
|
+
order: 20,
|
|
130
|
+
label: () => '工作区文件',
|
|
131
|
+
}, WorkspaceFilesSettingsCard)), 'dsh-workspace-files: settings card')
|
|
140
132
|
}
|
|
141
133
|
}
|
|
142
134
|
|
|
@@ -186,16 +178,16 @@ function ensureSettingsStyle() {
|
|
|
186
178
|
style.id = id
|
|
187
179
|
style.textContent = `
|
|
188
180
|
.dsh-wf-card {
|
|
189
|
-
border:
|
|
190
|
-
background: var(--dsw-alias-
|
|
191
|
-
border-radius:
|
|
181
|
+
border: 0.5px solid var(--dsw-alias-border-l1);
|
|
182
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
183
|
+
border-radius: var(--dsw-radius-md, 8px);
|
|
192
184
|
list-style: none;
|
|
193
|
-
transition: border-color .16s, background .16s;
|
|
185
|
+
transition: border-color 0.16s, background 0.16s;
|
|
194
186
|
box-sizing: border-box;
|
|
195
187
|
}
|
|
196
188
|
.dsh-wf-card-open {
|
|
197
|
-
background: var(--dsw-alias-
|
|
198
|
-
border-color: var(--dsw-alias-
|
|
189
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
190
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
199
191
|
}
|
|
200
192
|
.dsh-wf-header {
|
|
201
193
|
-webkit-appearance: none;
|
|
@@ -205,24 +197,24 @@ function ensureSettingsStyle() {
|
|
|
205
197
|
color: inherit;
|
|
206
198
|
text-align: left;
|
|
207
199
|
cursor: pointer;
|
|
208
|
-
background:
|
|
200
|
+
background: transparent;
|
|
209
201
|
border: 0;
|
|
210
|
-
border-radius:
|
|
202
|
+
border-radius: var(--dsw-radius-md, 8px);
|
|
211
203
|
align-items: center;
|
|
212
204
|
gap: 12px;
|
|
213
|
-
padding: 14px
|
|
205
|
+
padding: 12px 14px;
|
|
214
206
|
display: flex;
|
|
215
207
|
}
|
|
216
208
|
.dsh-wf-head-text {
|
|
217
209
|
flex-direction: column;
|
|
218
210
|
flex: 1;
|
|
219
|
-
gap:
|
|
211
|
+
gap: 3px;
|
|
220
212
|
min-width: 0;
|
|
221
213
|
display: flex;
|
|
222
214
|
}
|
|
223
215
|
.dsh-wf-name {
|
|
224
|
-
color: var(--dsw-alias-label-primary
|
|
225
|
-
font-size:
|
|
216
|
+
color: var(--dsw-alias-label-primary);
|
|
217
|
+
font-size: 14px;
|
|
226
218
|
font-weight: 600;
|
|
227
219
|
line-height: 1.4;
|
|
228
220
|
display: flex;
|
|
@@ -230,38 +222,38 @@ function ensureSettingsStyle() {
|
|
|
230
222
|
gap: 8px;
|
|
231
223
|
}
|
|
232
224
|
.dsh-wf-version {
|
|
233
|
-
font-size:
|
|
225
|
+
font-size: 11px;
|
|
234
226
|
font-weight: 400;
|
|
235
|
-
color: var(--dsw-alias-label-tertiary
|
|
227
|
+
color: var(--dsw-alias-label-tertiary);
|
|
236
228
|
}
|
|
237
229
|
.dsh-wf-desc {
|
|
238
|
-
color: var(--dsw-alias-label-
|
|
239
|
-
font-size:
|
|
230
|
+
color: var(--dsw-alias-label-secondary);
|
|
231
|
+
font-size: 12px;
|
|
240
232
|
line-height: 1.5;
|
|
241
233
|
}
|
|
242
234
|
.dsh-wf-chevron {
|
|
243
|
-
color: var(--dsw-alias-label-tertiary
|
|
235
|
+
color: var(--dsw-alias-label-tertiary);
|
|
244
236
|
flex: none;
|
|
245
|
-
transition: transform .
|
|
237
|
+
transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
|
246
238
|
display: inline-flex;
|
|
247
239
|
}
|
|
248
240
|
.dsh-wf-chevron-open {
|
|
249
241
|
transform: rotate(180deg);
|
|
250
242
|
}
|
|
251
243
|
.dsh-wf-body {
|
|
252
|
-
border-top:
|
|
253
|
-
margin: 0
|
|
244
|
+
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
245
|
+
margin: 0 14px;
|
|
254
246
|
padding: 8px 0 12px;
|
|
255
247
|
}
|
|
256
248
|
.dsh-wf-row {
|
|
257
249
|
align-items: center;
|
|
258
250
|
gap: 16px;
|
|
259
|
-
padding:
|
|
251
|
+
padding: 10px 0;
|
|
260
252
|
display: flex;
|
|
261
253
|
justify-content: space-between;
|
|
262
254
|
}
|
|
263
255
|
.dsh-wf-row + .dsh-wf-row {
|
|
264
|
-
border-top:
|
|
256
|
+
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
265
257
|
}
|
|
266
258
|
.dsh-wf-label-box {
|
|
267
259
|
flex-direction: column;
|
|
@@ -271,14 +263,14 @@ function ensureSettingsStyle() {
|
|
|
271
263
|
display: flex;
|
|
272
264
|
}
|
|
273
265
|
.dsh-wf-label {
|
|
274
|
-
font-size:
|
|
266
|
+
font-size: 13px;
|
|
275
267
|
font-weight: 550;
|
|
276
|
-
color: var(--dsw-alias-label-primary
|
|
268
|
+
color: var(--dsw-alias-label-primary);
|
|
277
269
|
}
|
|
278
270
|
.dsh-wf-hint {
|
|
279
|
-
color: var(--dsw-alias-label-tertiary
|
|
280
|
-
font-size:
|
|
281
|
-
line-height:
|
|
271
|
+
color: var(--dsw-alias-label-tertiary);
|
|
272
|
+
font-size: 11.5px;
|
|
273
|
+
line-height: 16px;
|
|
282
274
|
}
|
|
283
275
|
.dsh-wf-ctrl {
|
|
284
276
|
flex-shrink: 0;
|
|
@@ -287,56 +279,56 @@ function ensureSettingsStyle() {
|
|
|
287
279
|
gap: 8px;
|
|
288
280
|
}
|
|
289
281
|
.dsh-wf-seg {
|
|
290
|
-
border:
|
|
291
|
-
border-radius:
|
|
282
|
+
border: 0.5px solid var(--dsw-alias-border-l1);
|
|
283
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
292
284
|
gap: 2px;
|
|
293
285
|
padding: 2px;
|
|
294
286
|
display: inline-flex;
|
|
295
|
-
background: var(--dsw-alias-
|
|
287
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
296
288
|
}
|
|
297
289
|
.dsh-wf-seg-btn {
|
|
298
290
|
font: inherit;
|
|
299
|
-
color: var(--dsw-alias-label-secondary
|
|
291
|
+
color: var(--dsw-alias-label-secondary);
|
|
300
292
|
cursor: pointer;
|
|
301
|
-
background:
|
|
293
|
+
background: transparent;
|
|
302
294
|
border: none;
|
|
303
|
-
border-radius:
|
|
304
|
-
padding:
|
|
305
|
-
font-size:
|
|
306
|
-
|
|
307
|
-
transition: all 0.15s;
|
|
295
|
+
border-radius: 4px;
|
|
296
|
+
padding: 3px 8px;
|
|
297
|
+
font-size: 11.5px;
|
|
298
|
+
transition: all 0.15s cubic-bezier(0.16, 1, 0.3, 1);
|
|
308
299
|
}
|
|
309
300
|
.dsh-wf-seg-btn:hover {
|
|
310
|
-
color: var(--dsw-alias-label-primary
|
|
301
|
+
color: var(--dsw-alias-label-primary);
|
|
302
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
311
303
|
}
|
|
312
304
|
.dsh-wf-seg-active {
|
|
313
|
-
background: var(--dsw-alias-
|
|
314
|
-
color: var(--dsw-alias-
|
|
305
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent);
|
|
306
|
+
color: var(--dsw-alias-state-business-primary);
|
|
315
307
|
font-weight: 600;
|
|
316
|
-
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
|
317
308
|
}
|
|
318
309
|
.dsh-wf-input {
|
|
319
310
|
box-sizing: border-box;
|
|
320
|
-
border:
|
|
321
|
-
background: var(--dsw-alias-
|
|
322
|
-
width:
|
|
311
|
+
border: 0.5px solid var(--dsw-alias-border-l1);
|
|
312
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
313
|
+
width: 58px;
|
|
323
314
|
text-align: center;
|
|
324
|
-
color: var(--dsw-alias-label-primary
|
|
315
|
+
color: var(--dsw-alias-label-primary);
|
|
325
316
|
font: inherit;
|
|
326
|
-
border-radius:
|
|
327
|
-
padding:
|
|
328
|
-
font-size:
|
|
329
|
-
|
|
317
|
+
border-radius: 6px;
|
|
318
|
+
padding: 3px 6px;
|
|
319
|
+
font-size: 12px;
|
|
320
|
+
outline: none;
|
|
321
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
330
322
|
}
|
|
331
323
|
.dsh-wf-input:focus {
|
|
332
|
-
border-color: var(--dsw-alias-
|
|
333
|
-
|
|
324
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
325
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent);
|
|
334
326
|
}
|
|
335
327
|
.dsh-wf-toggle {
|
|
336
328
|
position: relative;
|
|
337
329
|
display: inline-block;
|
|
338
|
-
width:
|
|
339
|
-
height:
|
|
330
|
+
width: 36px;
|
|
331
|
+
height: 20px;
|
|
340
332
|
cursor: pointer;
|
|
341
333
|
}
|
|
342
334
|
.dsh-wf-toggle input {
|
|
@@ -348,55 +340,55 @@ function ensureSettingsStyle() {
|
|
|
348
340
|
.dsh-wf-toggle-track {
|
|
349
341
|
position: absolute;
|
|
350
342
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
351
|
-
background-color: var(--dsw-alias-border-
|
|
352
|
-
transition: .
|
|
353
|
-
border-radius:
|
|
343
|
+
background-color: var(--dsw-alias-border-l1);
|
|
344
|
+
transition: 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
|
345
|
+
border-radius: 20px;
|
|
354
346
|
}
|
|
355
347
|
.dsh-wf-toggle input:checked + .dsh-wf-toggle-track {
|
|
356
|
-
background-color: var(--dsw-alias-
|
|
348
|
+
background-color: var(--dsw-alias-state-business-primary);
|
|
357
349
|
}
|
|
358
350
|
.dsh-wf-toggle-thumb {
|
|
359
351
|
position: absolute;
|
|
360
|
-
height:
|
|
361
|
-
width:
|
|
352
|
+
height: 14px;
|
|
353
|
+
width: 14px;
|
|
362
354
|
left: 3px;
|
|
363
355
|
bottom: 3px;
|
|
364
|
-
background-color:
|
|
365
|
-
transition: .
|
|
356
|
+
background-color: #ffffff;
|
|
357
|
+
transition: 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
|
366
358
|
border-radius: 50%;
|
|
367
|
-
box-shadow: 0 1px
|
|
359
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.18);
|
|
368
360
|
}
|
|
369
361
|
.dsh-wf-toggle input:checked + .dsh-wf-toggle-track .dsh-wf-toggle-thumb {
|
|
370
|
-
transform: translateX(
|
|
362
|
+
transform: translateX(16px);
|
|
371
363
|
}
|
|
372
364
|
.dsh-wf-footer {
|
|
373
|
-
margin-top:
|
|
374
|
-
padding-top:
|
|
375
|
-
border-top:
|
|
365
|
+
margin-top: 6px;
|
|
366
|
+
padding-top: 8px;
|
|
367
|
+
border-top: 0.5px solid var(--dsw-alias-border-l2);
|
|
376
368
|
display: flex;
|
|
377
369
|
align-items: center;
|
|
378
370
|
justify-content: space-between;
|
|
379
371
|
}
|
|
380
372
|
.dsh-wf-saved-hint {
|
|
381
|
-
font-size:
|
|
382
|
-
color: var(--dsw-alias-state-
|
|
373
|
+
font-size: 11px;
|
|
374
|
+
color: var(--dsw-alias-state-business-primary);
|
|
383
375
|
display: flex;
|
|
384
376
|
align-items: center;
|
|
385
377
|
gap: 4px;
|
|
386
378
|
}
|
|
387
379
|
.dsh-wf-btn-reset {
|
|
388
380
|
background: transparent;
|
|
389
|
-
border:
|
|
390
|
-
border-radius:
|
|
391
|
-
padding: 3px
|
|
392
|
-
font-size:
|
|
393
|
-
color: var(--dsw-alias-label-secondary
|
|
381
|
+
border: 0.5px solid var(--dsw-alias-border-l1);
|
|
382
|
+
border-radius: 4px;
|
|
383
|
+
padding: 3px 8px;
|
|
384
|
+
font-size: 11px;
|
|
385
|
+
color: var(--dsw-alias-label-secondary);
|
|
394
386
|
cursor: pointer;
|
|
395
|
-
transition: all 0.
|
|
387
|
+
transition: all 0.12s;
|
|
396
388
|
}
|
|
397
389
|
.dsh-wf-btn-reset:hover {
|
|
398
|
-
background: var(--dsw-alias-bg-
|
|
399
|
-
color: var(--dsw-alias-label-primary
|
|
390
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
391
|
+
color: var(--dsw-alias-label-primary);
|
|
400
392
|
}
|
|
401
393
|
`
|
|
402
394
|
document.head.appendChild(style)
|
|
@@ -438,29 +430,27 @@ export function WorkspaceFilesSettingsCard(): JSX.Element {
|
|
|
438
430
|
const def = { maxDepth: 16, showHidden: false }
|
|
439
431
|
setConfig(def)
|
|
440
432
|
saveWorkspaceFilesConfig(def)
|
|
441
|
-
setSavedTick(true)
|
|
442
|
-
setTimeout(() => setSavedTick(false), 2000)
|
|
443
433
|
}
|
|
444
434
|
|
|
445
435
|
return (
|
|
446
|
-
<div className={
|
|
436
|
+
<div className={`dsh-wf-card ${open ? 'dsh-wf-card-open' : ''}`}>
|
|
447
437
|
<button
|
|
448
438
|
type="button"
|
|
449
439
|
className="dsh-wf-header"
|
|
450
|
-
aria-expanded={open}
|
|
451
440
|
onClick={() => setOpen(!open)}
|
|
452
441
|
>
|
|
442
|
+
<FolderGuideGlyph size={22} />
|
|
453
443
|
<div className="dsh-wf-head-text">
|
|
454
444
|
<div className="dsh-wf-name">
|
|
455
445
|
<span>工作区文件</span>
|
|
456
|
-
<span className="dsh-wf-version">
|
|
446
|
+
<span className="dsh-wf-version">v{__PLUGIN_VERSION__}</span>
|
|
457
447
|
</div>
|
|
458
448
|
<div className="dsh-wf-desc">
|
|
459
|
-
|
|
449
|
+
全量项目文件树浏览、即时检索与 @-mention 会话一键引用。
|
|
460
450
|
</div>
|
|
461
451
|
</div>
|
|
462
|
-
<span className={
|
|
463
|
-
<
|
|
452
|
+
<span className={`dsh-wf-chevron ${open ? 'dsh-wf-chevron-open' : ''}`}>
|
|
453
|
+
<IconChevronRight size={14} style={{ transform: open ? 'rotate(90deg)' : 'rotate(0deg)', transition: 'transform 0.16s' }} />
|
|
464
454
|
</span>
|
|
465
455
|
</button>
|
|
466
456
|
|
|
@@ -468,42 +458,42 @@ export function WorkspaceFilesSettingsCard(): JSX.Element {
|
|
|
468
458
|
<div className="dsh-wf-body">
|
|
469
459
|
<div className="dsh-wf-row">
|
|
470
460
|
<div className="dsh-wf-label-box">
|
|
471
|
-
<
|
|
472
|
-
<
|
|
473
|
-
|
|
474
|
-
</
|
|
461
|
+
<span className="dsh-wf-label">扫描最大深度 (层级)</span>
|
|
462
|
+
<span className="dsh-wf-hint">
|
|
463
|
+
控制文件树深入遍历的最大文件夹嵌套层级,防止庞大项目深度过大产生卡顿。
|
|
464
|
+
</span>
|
|
475
465
|
</div>
|
|
476
466
|
<div className="dsh-wf-ctrl">
|
|
477
467
|
<div className="dsh-wf-seg">
|
|
478
|
-
{[8, 16,
|
|
468
|
+
{[8, 16, 24, 32].map(d => (
|
|
479
469
|
<button
|
|
480
470
|
key={d}
|
|
481
471
|
type="button"
|
|
482
|
-
className={config.maxDepth === d ? 'dsh-wf-seg-
|
|
472
|
+
className={`dsh-wf-seg-btn ${config.maxDepth === d ? 'dsh-wf-seg-active' : ''}`}
|
|
483
473
|
onClick={() => handleDepthSelect(d)}
|
|
484
474
|
>
|
|
485
|
-
{d}
|
|
475
|
+
{d} 层
|
|
486
476
|
</button>
|
|
487
477
|
))}
|
|
488
478
|
</div>
|
|
489
479
|
<input
|
|
490
480
|
type="number"
|
|
491
|
-
min=
|
|
492
|
-
max=
|
|
481
|
+
min={1}
|
|
482
|
+
max={100}
|
|
493
483
|
className="dsh-wf-input"
|
|
494
484
|
value={config.maxDepth}
|
|
495
485
|
onChange={handleDepthInput}
|
|
496
|
-
title="
|
|
486
|
+
title="手动输入层级深度 (1-100)"
|
|
497
487
|
/>
|
|
498
488
|
</div>
|
|
499
489
|
</div>
|
|
500
490
|
|
|
501
491
|
<div className="dsh-wf-row">
|
|
502
492
|
<div className="dsh-wf-label-box">
|
|
503
|
-
<
|
|
504
|
-
<
|
|
505
|
-
|
|
506
|
-
</
|
|
493
|
+
<span className="dsh-wf-label">显示以点开头的隐藏文件</span>
|
|
494
|
+
<span className="dsh-wf-hint">
|
|
495
|
+
开启后,文件树中将展现如 .env, .editorconfig 等以点开头的配置文件(.git 等缓存黑名单除外)。
|
|
496
|
+
</span>
|
|
507
497
|
</div>
|
|
508
498
|
<div className="dsh-wf-ctrl">
|
|
509
499
|
<label className="dsh-wf-toggle">
|
|
@@ -521,14 +511,10 @@ export function WorkspaceFilesSettingsCard(): JSX.Element {
|
|
|
521
511
|
|
|
522
512
|
<div className="dsh-wf-footer">
|
|
523
513
|
<div>
|
|
524
|
-
{savedTick
|
|
514
|
+
{savedTick && (
|
|
525
515
|
<span className="dsh-wf-saved-hint">
|
|
526
516
|
<IconCheck size={12} />
|
|
527
|
-
|
|
528
|
-
</span>
|
|
529
|
-
) : (
|
|
530
|
-
<span style={{ fontSize: 11, color: 'var(--dsw-alias-label-tertiary, #8b93a1)' }}>
|
|
531
|
-
设置改动即时生效并同步至工作区文件树
|
|
517
|
+
配置已自动保存并实时同步
|
|
532
518
|
</span>
|
|
533
519
|
)}
|
|
534
520
|
</div>
|
|
@@ -546,6 +532,90 @@ export function WorkspaceFilesSettingsCard(): JSX.Element {
|
|
|
546
532
|
)
|
|
547
533
|
}
|
|
548
534
|
|
|
535
|
+
/* ── High-Performance Flattened Visible Row Model ── */
|
|
536
|
+
export interface VisibleFlatRow {
|
|
537
|
+
readonly id: string
|
|
538
|
+
readonly name: string
|
|
539
|
+
readonly path: string
|
|
540
|
+
readonly isDirectory: boolean
|
|
541
|
+
readonly depth: number
|
|
542
|
+
readonly isExpanded: boolean
|
|
543
|
+
readonly hasChildren: boolean
|
|
544
|
+
readonly childCount: number
|
|
545
|
+
readonly size?: number
|
|
546
|
+
readonly item: FileItem
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* 线性遍历计算可视行。未展开目录的子节点绝不遍历,彻底解决 DOM 节点爆炸问题。
|
|
551
|
+
*/
|
|
552
|
+
function flattenVisibleTree(
|
|
553
|
+
items: FileItem[],
|
|
554
|
+
expandedPaths: Set<string>,
|
|
555
|
+
depth = 0,
|
|
556
|
+
out: VisibleFlatRow[] = [],
|
|
557
|
+
): VisibleFlatRow[] {
|
|
558
|
+
for (const it of items) {
|
|
559
|
+
const isDir = it.isDirectory
|
|
560
|
+
const hasChildren = isDir && Array.isArray(it.children) && it.children.length > 0
|
|
561
|
+
const isExpanded = isDir && expandedPaths.has(it.path)
|
|
562
|
+
const childCount = hasChildren ? it.children!.length : 0
|
|
563
|
+
|
|
564
|
+
out.push({
|
|
565
|
+
id: it.path,
|
|
566
|
+
name: it.name,
|
|
567
|
+
path: it.path,
|
|
568
|
+
isDirectory: isDir,
|
|
569
|
+
depth,
|
|
570
|
+
isExpanded,
|
|
571
|
+
hasChildren,
|
|
572
|
+
childCount,
|
|
573
|
+
size: it.size,
|
|
574
|
+
item: it,
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
if (isDir && isExpanded && hasChildren) {
|
|
578
|
+
flattenVisibleTree(it.children!, expandedPaths, depth + 1, out)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return out
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 树形过滤模式:仅保留包含匹配项的分支并保持展开
|
|
586
|
+
*/
|
|
587
|
+
function filterTreeWithAutoExpand(
|
|
588
|
+
nodes: FileItem[],
|
|
589
|
+
q: string,
|
|
590
|
+
): { filteredItems: FileItem[]; autoExpandPaths: Set<string> } {
|
|
591
|
+
const autoExpand = new Set<string>()
|
|
592
|
+
|
|
593
|
+
function filterNode(node: FileItem): FileItem | null {
|
|
594
|
+
const matchSelf = node.name.toLowerCase().includes(q) || node.path.toLowerCase().includes(q)
|
|
595
|
+
if (node.isDirectory) {
|
|
596
|
+
const children: FileItem[] = []
|
|
597
|
+
for (const child of node.children || []) {
|
|
598
|
+
const c = filterNode(child)
|
|
599
|
+
if (c) children.push(c)
|
|
600
|
+
}
|
|
601
|
+
if (matchSelf || children.length > 0) {
|
|
602
|
+
autoExpand.add(node.path)
|
|
603
|
+
return { ...node, children }
|
|
604
|
+
}
|
|
605
|
+
return null
|
|
606
|
+
}
|
|
607
|
+
return matchSelf ? node : null
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const result: FileItem[] = []
|
|
611
|
+
for (const it of nodes) {
|
|
612
|
+
const f = filterNode(it)
|
|
613
|
+
if (f) result.push(f)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return { filteredItems: result, autoExpandPaths: autoExpand }
|
|
617
|
+
}
|
|
618
|
+
|
|
549
619
|
/* ── Main Workspace Files View ── */
|
|
550
620
|
function WorkspaceFilesView({
|
|
551
621
|
sessionId,
|
|
@@ -559,11 +629,15 @@ function WorkspaceFilesView({
|
|
|
559
629
|
const [data, setData] = useState<WorkspaceFilesResult | null>(null)
|
|
560
630
|
const [loading, setLoading] = useState(true)
|
|
561
631
|
const [error, setError] = useState<string | null>(null)
|
|
562
|
-
const [
|
|
632
|
+
const [inputQuery, setInputQuery] = useState('')
|
|
563
633
|
const [toast, setToast] = useState<string | null>(null)
|
|
564
634
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set())
|
|
565
635
|
const [activeConfig, setActiveConfig] = useState<WorkspaceFilesConfig>(() => loadWorkspaceFilesConfig())
|
|
566
636
|
const [showHidden, setShowHidden] = useState(() => loadWorkspaceFilesConfig().showHidden)
|
|
637
|
+
const [searchViewMode, setSearchViewMode] = useState<'flat' | 'tree'>('flat')
|
|
638
|
+
|
|
639
|
+
// 使用 React 并发特性分离高频打字与重度树过滤
|
|
640
|
+
const deferredQuery = useDeferredValue(inputQuery.trim().toLowerCase())
|
|
567
641
|
|
|
568
642
|
const toastTimer = useRef<number | null>(null)
|
|
569
643
|
|
|
@@ -573,6 +647,10 @@ function WorkspaceFilesView({
|
|
|
573
647
|
toastTimer.current = window.setTimeout(() => setToast(null), 2000)
|
|
574
648
|
}, [])
|
|
575
649
|
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
ensureFilesStyle()
|
|
652
|
+
}, [])
|
|
653
|
+
|
|
576
654
|
useEffect(() => {
|
|
577
655
|
const handleCfgChange = (e: any) => {
|
|
578
656
|
if (e.detail) {
|
|
@@ -597,12 +675,10 @@ function WorkspaceFilesView({
|
|
|
597
675
|
})
|
|
598
676
|
setData(res)
|
|
599
677
|
|
|
600
|
-
// 默认折叠,并从 localStorage 恢复已记录的展开状态
|
|
601
678
|
setExpandedPaths(prev => {
|
|
602
679
|
if (prev.size > 0) return prev
|
|
603
680
|
const stored = loadStoredExpandedPaths(res.root || cwd)
|
|
604
681
|
if (stored !== null) return stored
|
|
605
|
-
// 默认全部关闭,不展开任何目录
|
|
606
682
|
return new Set<string>()
|
|
607
683
|
})
|
|
608
684
|
} catch (err: any) {
|
|
@@ -616,7 +692,7 @@ function WorkspaceFilesView({
|
|
|
616
692
|
void loadTree()
|
|
617
693
|
}, [loadTree])
|
|
618
694
|
|
|
619
|
-
const handleToggleExpand = (path: string) => {
|
|
695
|
+
const handleToggleExpand = useCallback((path: string) => {
|
|
620
696
|
setExpandedPaths(prev => {
|
|
621
697
|
const next = new Set(prev)
|
|
622
698
|
if (next.has(path)) next.delete(path)
|
|
@@ -624,9 +700,9 @@ function WorkspaceFilesView({
|
|
|
624
700
|
saveStoredExpandedPaths(data?.root || cwd, next)
|
|
625
701
|
return next
|
|
626
702
|
})
|
|
627
|
-
}
|
|
703
|
+
}, [data?.root, cwd])
|
|
628
704
|
|
|
629
|
-
const handleExpandAll = () => {
|
|
705
|
+
const handleExpandAll = useCallback(() => {
|
|
630
706
|
if (!data) return
|
|
631
707
|
const all = new Set<string>()
|
|
632
708
|
function collect(items: FileItem[]) {
|
|
@@ -640,16 +716,16 @@ function WorkspaceFilesView({
|
|
|
640
716
|
collect(data.items)
|
|
641
717
|
setExpandedPaths(all)
|
|
642
718
|
saveStoredExpandedPaths(data.root || cwd, all)
|
|
643
|
-
}
|
|
719
|
+
}, [data, cwd])
|
|
644
720
|
|
|
645
|
-
const handleCollapseAll = () => {
|
|
721
|
+
const handleCollapseAll = useCallback(() => {
|
|
646
722
|
const empty = new Set<string>()
|
|
647
723
|
setExpandedPaths(empty)
|
|
648
724
|
saveStoredExpandedPaths(data?.root || cwd, empty)
|
|
649
|
-
}
|
|
725
|
+
}, [data?.root, cwd])
|
|
650
726
|
|
|
651
|
-
//
|
|
652
|
-
const handleJumpToFolderInTree = (dirPath: string) => {
|
|
727
|
+
// 搜索时点击文件夹:跳回树形视图并展开该目录及全部祖先路径,平滑定位
|
|
728
|
+
const handleJumpToFolderInTree = useCallback((dirPath: string) => {
|
|
653
729
|
const parts = dirPath.split('/')
|
|
654
730
|
const toExpand: string[] = []
|
|
655
731
|
let acc = ''
|
|
@@ -668,11 +744,9 @@ function WorkspaceFilesView({
|
|
|
668
744
|
return next
|
|
669
745
|
})
|
|
670
746
|
|
|
671
|
-
|
|
672
|
-
setQuery('')
|
|
747
|
+
setInputQuery('')
|
|
673
748
|
showToastMsg(`已定位并展开目录: ${dirPath}/`)
|
|
674
749
|
|
|
675
|
-
// 平滑滚动定位到该目录节点
|
|
676
750
|
setTimeout(() => {
|
|
677
751
|
try {
|
|
678
752
|
const el = document.querySelector(`[data-tree-path="${dirPath}"]`)
|
|
@@ -681,18 +755,16 @@ function WorkspaceFilesView({
|
|
|
681
755
|
}
|
|
682
756
|
} catch {}
|
|
683
757
|
}, 60)
|
|
684
|
-
}
|
|
758
|
+
}, [data?.root, cwd, showToastMsg])
|
|
685
759
|
|
|
686
|
-
|
|
687
|
-
const handleCopyPath = (e: React.MouseEvent, path: string) => {
|
|
760
|
+
const handleCopyPath = useCallback((e: React.MouseEvent, path: string) => {
|
|
688
761
|
e.stopPropagation()
|
|
689
762
|
void navigator.clipboard.writeText(path).then(() => {
|
|
690
763
|
showToastMsg(`已复制路径: ${path}`)
|
|
691
764
|
})
|
|
692
|
-
}
|
|
765
|
+
}, [showToastMsg])
|
|
693
766
|
|
|
694
|
-
|
|
695
|
-
const handleMention = (e: React.MouseEvent, itemOrPath: string | FileItem) => {
|
|
767
|
+
const handleMention = useCallback((e: React.MouseEvent, itemOrPath: string | FileItem) => {
|
|
696
768
|
e.stopPropagation()
|
|
697
769
|
const itemObj: FileItem = typeof itemOrPath === 'string'
|
|
698
770
|
? { name: itemOrPath.split('/').pop() || itemOrPath, path: itemOrPath, isDirectory: false }
|
|
@@ -700,24 +772,17 @@ function WorkspaceFilesView({
|
|
|
700
772
|
|
|
701
773
|
const mentionText = formatMentionText(itemObj.path, itemObj.isDirectory)
|
|
702
774
|
|
|
703
|
-
// 1. 优先尝试向会话输入框插入官方原生 ReferenceChipNode 胶囊卡片
|
|
704
775
|
let inserted = false
|
|
705
776
|
try {
|
|
706
777
|
inserted = tryInsertNativeReferenceChip(globalCtx, sessionId, itemObj)
|
|
707
|
-
} catch
|
|
708
|
-
console.warn('[dsh-workspace-files] 原生 Chip 插入失败:', err)
|
|
709
|
-
}
|
|
778
|
+
} catch {}
|
|
710
779
|
|
|
711
|
-
// 2. 若原生 Chip 插入未成功,无缝降级为规范纯文本插入
|
|
712
780
|
if (!inserted) {
|
|
713
781
|
try {
|
|
714
782
|
inserted = insertMentionToComposer(mentionText)
|
|
715
|
-
} catch
|
|
716
|
-
console.warn('[dsh-workspace-files] 降级纯文本插入失败:', err)
|
|
717
|
-
}
|
|
783
|
+
} catch {}
|
|
718
784
|
}
|
|
719
785
|
|
|
720
|
-
// 写入剪贴板作为双重保障
|
|
721
786
|
const displayName = itemObj.isDirectory ? `${itemObj.name}/` : itemObj.name
|
|
722
787
|
void navigator.clipboard.writeText(mentionText.trim()).then(() => {
|
|
723
788
|
if (inserted) {
|
|
@@ -726,10 +791,9 @@ function WorkspaceFilesView({
|
|
|
726
791
|
showToastMsg(`已复制引用: ${mentionText.trim()}`)
|
|
727
792
|
}
|
|
728
793
|
})
|
|
729
|
-
}
|
|
794
|
+
}, [sessionId, showToastMsg])
|
|
730
795
|
|
|
731
|
-
|
|
732
|
-
const handleRevealInExplorer = (e: React.MouseEvent, item: FileItem) => {
|
|
796
|
+
const handleRevealInExplorer = useCallback((e: React.MouseEvent, item: FileItem) => {
|
|
733
797
|
e.stopPropagation()
|
|
734
798
|
fetch('/dsh-workspace-files/api/files.open', {
|
|
735
799
|
method: 'POST',
|
|
@@ -753,10 +817,9 @@ function WorkspaceFilesView({
|
|
|
753
817
|
.catch(err => {
|
|
754
818
|
showToastMsg(`请求失败: ${err?.message || String(err)}`)
|
|
755
819
|
})
|
|
756
|
-
}
|
|
820
|
+
}, [sessionId, cwd, showToastMsg])
|
|
757
821
|
|
|
758
|
-
|
|
759
|
-
const handleOpenDefaultApp = (e: React.MouseEvent, item: FileItem) => {
|
|
822
|
+
const handleOpenDefaultApp = useCallback((e: React.MouseEvent, item: FileItem) => {
|
|
760
823
|
e.stopPropagation()
|
|
761
824
|
fetch('/dsh-workspace-files/api/files.open', {
|
|
762
825
|
method: 'POST',
|
|
@@ -780,88 +843,49 @@ function WorkspaceFilesView({
|
|
|
780
843
|
.catch(err => {
|
|
781
844
|
showToastMsg(`请求失败: ${err?.message || String(err)}`)
|
|
782
845
|
})
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const [searchViewMode, setSearchViewMode] = useState<'flat' | 'tree'>('flat')
|
|
786
|
-
const [searchExpandedPaths, setSearchExpandedPaths] = useState<Set<string>>(() => new Set())
|
|
846
|
+
}, [sessionId, cwd, showToastMsg])
|
|
787
847
|
|
|
788
|
-
|
|
789
|
-
const { filteredTreeItems, flatSearchResults } = useMemo(() => {
|
|
790
|
-
if (!data) return { filteredTreeItems: [], flatSearchResults: [] }
|
|
791
|
-
const q = query.trim().toLowerCase()
|
|
792
|
-
if (!q) return { filteredTreeItems: data.items, flatSearchResults: [] }
|
|
848
|
+
const isSearchActive = Boolean(deferredQuery)
|
|
793
849
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
850
|
+
// 1. 扁平搜索结果计算(限定前 150 项,秒速完成)
|
|
851
|
+
const flatSearchResults = useMemo(() => {
|
|
852
|
+
if (!data || !isSearchActive) return []
|
|
853
|
+
const results: Array<{ item: FileItem; parentDir: string }> = []
|
|
854
|
+
function traverse(items: FileItem[], parentDir: string) {
|
|
797
855
|
for (const it of items) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
856
|
+
if (it.name.toLowerCase().includes(deferredQuery) || it.path.toLowerCase().includes(deferredQuery)) {
|
|
857
|
+
results.push({ item: it, parentDir })
|
|
858
|
+
if (results.length >= 300) return
|
|
801
859
|
}
|
|
802
860
|
if (it.isDirectory && it.children) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
traverseFlat(data.items, '')
|
|
808
|
-
|
|
809
|
-
// 树形过滤集合
|
|
810
|
-
function filterNode(node: FileItem): FileItem | null {
|
|
811
|
-
const selfMatch = node.name.toLowerCase().includes(q) || node.path.toLowerCase().includes(q)
|
|
812
|
-
if (node.isDirectory) {
|
|
813
|
-
const filteredChildren: FileItem[] = []
|
|
814
|
-
for (const child of node.children || []) {
|
|
815
|
-
const matchedChild = filterNode(child)
|
|
816
|
-
if (matchedChild) filteredChildren.push(matchedChild)
|
|
817
|
-
}
|
|
818
|
-
if (selfMatch || filteredChildren.length > 0) {
|
|
819
|
-
return { ...node, children: filteredChildren }
|
|
861
|
+
traverse(it.children, it.path)
|
|
862
|
+
if (results.length >= 300) return
|
|
820
863
|
}
|
|
821
|
-
return null
|
|
822
864
|
}
|
|
823
|
-
return selfMatch ? node : null
|
|
824
865
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
// 搜索关键字变化时,自动展开匹配的目录树节点,并允许用户后续自由折叠/展开
|
|
836
|
-
useEffect(() => {
|
|
837
|
-
if (!query.trim() || !data) return
|
|
838
|
-
const q = query.trim().toLowerCase()
|
|
839
|
-
const autoExpand = new Set<string>()
|
|
840
|
-
function collect(items: FileItem[]) {
|
|
841
|
-
for (const it of items) {
|
|
842
|
-
if (it.isDirectory) {
|
|
843
|
-
const selfMatch = it.name.toLowerCase().includes(q) || it.path.toLowerCase().includes(q)
|
|
844
|
-
const childMatch = it.children?.some(c => c.name.toLowerCase().includes(q) || c.path.toLowerCase().includes(q))
|
|
845
|
-
if (selfMatch || childMatch) autoExpand.add(it.path)
|
|
846
|
-
if (it.children) collect(it.children)
|
|
847
|
-
}
|
|
848
|
-
}
|
|
866
|
+
traverse(data.items, '')
|
|
867
|
+
return results
|
|
868
|
+
}, [data, deferredQuery, isSearchActive])
|
|
869
|
+
|
|
870
|
+
// 2. 扁平可见行计算(常规树与搜索树),O(N_visible) 极速生成
|
|
871
|
+
const visibleFlatRows = useMemo(() => {
|
|
872
|
+
if (!data) return []
|
|
873
|
+
if (!isSearchActive) {
|
|
874
|
+
return flattenVisibleTree(data.items, expandedPaths)
|
|
849
875
|
}
|
|
850
|
-
collect(data.items)
|
|
851
|
-
setSearchExpandedPaths(autoExpand)
|
|
852
|
-
}, [query, data])
|
|
853
876
|
|
|
854
|
-
|
|
877
|
+
// 搜索树模式
|
|
878
|
+
const { filteredItems, autoExpandPaths } = filterTreeWithAutoExpand(data.items, deferredQuery)
|
|
879
|
+
return flattenVisibleTree(filteredItems, autoExpandPaths)
|
|
880
|
+
}, [data, isSearchActive, deferredQuery, expandedPaths])
|
|
855
881
|
|
|
856
882
|
return (
|
|
857
883
|
<div className="dsh-files-root">
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
{/* Header */}
|
|
884
|
+
{/* 1. Header Banner */}
|
|
861
885
|
<header className="dsh-files-header">
|
|
862
886
|
<div className="dsh-files-header-info">
|
|
863
887
|
<div className="dsh-files-title-row">
|
|
864
|
-
<span className="dsh-files-title"
|
|
888
|
+
<span className="dsh-files-title">工作区文件</span>
|
|
865
889
|
{data && (
|
|
866
890
|
<span className="dsh-files-stats-badge">
|
|
867
891
|
{data.totalFiles} 文件 · {data.totalDirs} 目录
|
|
@@ -874,26 +898,26 @@ function WorkspaceFilesView({
|
|
|
874
898
|
</div>
|
|
875
899
|
</header>
|
|
876
900
|
|
|
877
|
-
{/* Search
|
|
901
|
+
{/* 2. Search Toolbar (Clean Elevated Input + Motion Action Pills) */}
|
|
878
902
|
<div className="dsh-files-toolbar">
|
|
879
903
|
<div className="dsh-files-search-wrap">
|
|
880
904
|
<span className="dsh-files-search-icon"><IconSearch size={13} /></span>
|
|
881
905
|
<input
|
|
882
906
|
type="text"
|
|
883
907
|
className="dsh-files-search-input"
|
|
884
|
-
placeholder="
|
|
885
|
-
value={
|
|
886
|
-
onChange={e =>
|
|
908
|
+
placeholder="搜索文件或路径 (Esc 清空)..."
|
|
909
|
+
value={inputQuery}
|
|
910
|
+
onChange={e => setInputQuery(e.target.value)}
|
|
887
911
|
onKeyDown={e => {
|
|
888
|
-
if (e.key === 'Escape')
|
|
912
|
+
if (e.key === 'Escape') setInputQuery('')
|
|
889
913
|
}}
|
|
890
914
|
/>
|
|
891
|
-
{
|
|
915
|
+
{inputQuery && (
|
|
892
916
|
<button
|
|
893
917
|
type="button"
|
|
894
918
|
className="dsh-files-clear-btn"
|
|
895
|
-
onClick={() =>
|
|
896
|
-
title="
|
|
919
|
+
onClick={() => setInputQuery('')}
|
|
920
|
+
title="清空搜索"
|
|
897
921
|
>
|
|
898
922
|
<IconClose size={12} />
|
|
899
923
|
</button>
|
|
@@ -906,7 +930,7 @@ function WorkspaceFilesView({
|
|
|
906
930
|
type="button"
|
|
907
931
|
className="dsh-files-tool-btn is-active"
|
|
908
932
|
onClick={() => setSearchViewMode(searchViewMode === 'flat' ? 'tree' : 'flat')}
|
|
909
|
-
title={searchViewMode === 'flat' ? '
|
|
933
|
+
title={searchViewMode === 'flat' ? '切为树形匹配视图' : '切为扁平列表视图'}
|
|
910
934
|
>
|
|
911
935
|
{searchViewMode === 'flat' ? <IconTree size={13} /> : <IconList size={13} />}
|
|
912
936
|
</button>
|
|
@@ -916,7 +940,7 @@ function WorkspaceFilesView({
|
|
|
916
940
|
type="button"
|
|
917
941
|
className="dsh-files-tool-btn"
|
|
918
942
|
onClick={handleExpandAll}
|
|
919
|
-
title="
|
|
943
|
+
title="展开全部目录"
|
|
920
944
|
>
|
|
921
945
|
<IconExpandAll size={13} />
|
|
922
946
|
</button>
|
|
@@ -924,7 +948,7 @@ function WorkspaceFilesView({
|
|
|
924
948
|
type="button"
|
|
925
949
|
className="dsh-files-tool-btn"
|
|
926
950
|
onClick={handleCollapseAll}
|
|
927
|
-
title="
|
|
951
|
+
title="折叠全部目录"
|
|
928
952
|
>
|
|
929
953
|
<IconCollapseAll size={13} />
|
|
930
954
|
</button>
|
|
@@ -935,9 +959,9 @@ function WorkspaceFilesView({
|
|
|
935
959
|
type="button"
|
|
936
960
|
className={`dsh-files-tool-btn ${showHidden ? 'is-active' : ''}`}
|
|
937
961
|
onClick={() => setShowHidden(!showHidden)}
|
|
938
|
-
title={showHidden ? '
|
|
962
|
+
title={showHidden ? '隐藏以点开头的文件' : '显示以点开头的文件'}
|
|
939
963
|
>
|
|
940
|
-
<span style={{ fontSize:
|
|
964
|
+
<span style={{ fontSize: 10.5, fontWeight: 700 }}>.*</span>
|
|
941
965
|
</button>
|
|
942
966
|
<button
|
|
943
967
|
type="button"
|
|
@@ -951,7 +975,7 @@ function WorkspaceFilesView({
|
|
|
951
975
|
</div>
|
|
952
976
|
</div>
|
|
953
977
|
|
|
954
|
-
{/* Error
|
|
978
|
+
{/* Error Alert */}
|
|
955
979
|
{error && (
|
|
956
980
|
<div className="dsh-files-banner-error">
|
|
957
981
|
<span>{error}</span>
|
|
@@ -961,24 +985,24 @@ function WorkspaceFilesView({
|
|
|
961
985
|
</div>
|
|
962
986
|
)}
|
|
963
987
|
|
|
964
|
-
{/*
|
|
988
|
+
{/* 3. High-Performance Virtualized-Ready Content */}
|
|
965
989
|
<div className="dsh-files-body">
|
|
966
990
|
{loading && !data ? (
|
|
967
991
|
<div className="dsh-files-centered">
|
|
968
992
|
<div className="dsh-files-spinner" />
|
|
969
|
-
<span
|
|
993
|
+
<span>正在索引工作区文件…</span>
|
|
970
994
|
</div>
|
|
971
995
|
) : isSearchActive && flatSearchResults.length === 0 ? (
|
|
972
996
|
<div className="dsh-files-empty">
|
|
973
|
-
<div className="dsh-files-empty-icon"><IconFolder size={
|
|
974
|
-
<div className="dsh-files-empty-title"
|
|
975
|
-
<div className="dsh-files-empty-detail"
|
|
997
|
+
<div className="dsh-files-empty-icon"><IconFolder size={28} /></div>
|
|
998
|
+
<div className="dsh-files-empty-title">未匹配到相关文件</div>
|
|
999
|
+
<div className="dsh-files-empty-detail">请尝试更换关键字或按 Esc 恢复完整目录树。</div>
|
|
976
1000
|
</div>
|
|
977
1001
|
) : !isSearchActive && (!data || data.items.length === 0) ? (
|
|
978
1002
|
<div className="dsh-files-empty">
|
|
979
|
-
<div className="dsh-files-empty-icon"><IconFolder size={
|
|
1003
|
+
<div className="dsh-files-empty-icon"><IconFolder size={28} /></div>
|
|
980
1004
|
<div className="dsh-files-empty-title">工作区为空</div>
|
|
981
|
-
<div className="dsh-files-empty-detail"
|
|
1005
|
+
<div className="dsh-files-empty-detail">当前工作目录下暂无可展示的文件。</div>
|
|
982
1006
|
</div>
|
|
983
1007
|
) : isSearchActive && searchViewMode === 'flat' ? (
|
|
984
1008
|
/* ── 扁平搜索结果列表 ── */
|
|
@@ -986,13 +1010,13 @@ function WorkspaceFilesView({
|
|
|
986
1010
|
<div className="dsh-files-search-header">
|
|
987
1011
|
<span className="dsh-files-search-count">
|
|
988
1012
|
找到 {flatSearchResults.length} 个匹配项
|
|
989
|
-
{flatSearchResults.length > 150 ? '
|
|
1013
|
+
{flatSearchResults.length > 150 ? '(已展示前 150 项)' : ''}
|
|
990
1014
|
</span>
|
|
991
1015
|
<button
|
|
992
1016
|
type="button"
|
|
993
1017
|
className="dsh-files-view-toggle-btn"
|
|
994
1018
|
onClick={() => setSearchViewMode('tree')}
|
|
995
|
-
title="
|
|
1019
|
+
title="切换为树形视图"
|
|
996
1020
|
>
|
|
997
1021
|
<IconTree size={12} />
|
|
998
1022
|
<span>切为树形</span>
|
|
@@ -1010,7 +1034,7 @@ function WorkspaceFilesView({
|
|
|
1010
1034
|
onOpenFile(item.path)
|
|
1011
1035
|
}
|
|
1012
1036
|
}}
|
|
1013
|
-
title={item.isDirectory ?
|
|
1037
|
+
title={item.isDirectory ? `点击定位并展开目录: ${item.path}/` : `点击预览打开: ${item.path}`}
|
|
1014
1038
|
>
|
|
1015
1039
|
<span className="dsh-files-flat-icon">
|
|
1016
1040
|
{item.isDirectory ? <IconFolder size={14} /> : <FileGlyphIcon filename={item.name} size={14} />}
|
|
@@ -1018,7 +1042,7 @@ function WorkspaceFilesView({
|
|
|
1018
1042
|
<div className="dsh-files-flat-info">
|
|
1019
1043
|
<div className="dsh-files-flat-top">
|
|
1020
1044
|
<span className="dsh-files-flat-name">
|
|
1021
|
-
<HighlightText text={item.name} query={
|
|
1045
|
+
<HighlightText text={item.name} query={deferredQuery} />
|
|
1022
1046
|
{item.isDirectory && '/'}
|
|
1023
1047
|
</span>
|
|
1024
1048
|
{item.size !== undefined && (
|
|
@@ -1039,16 +1063,16 @@ function WorkspaceFilesView({
|
|
|
1039
1063
|
type="button"
|
|
1040
1064
|
className="dsh-files-action-btn dsh-files-action-at"
|
|
1041
1065
|
onClick={e => handleMention(e, item)}
|
|
1042
|
-
title={
|
|
1066
|
+
title={`@ 引用${item.isDirectory ? '目录' : '文件'}到会话`}
|
|
1043
1067
|
>
|
|
1044
|
-
<IconAt size={
|
|
1068
|
+
<IconAt size={12} />
|
|
1045
1069
|
<span className="dsh-files-action-at-text">@引用</span>
|
|
1046
1070
|
</button>
|
|
1047
1071
|
<button
|
|
1048
1072
|
type="button"
|
|
1049
1073
|
className="dsh-files-action-btn"
|
|
1050
1074
|
onClick={e => handleRevealInExplorer(e, item)}
|
|
1051
|
-
title=
|
|
1075
|
+
title="在文件资源管理器中显示"
|
|
1052
1076
|
>
|
|
1053
1077
|
<IconExplorer size={12} />
|
|
1054
1078
|
</button>
|
|
@@ -1056,7 +1080,7 @@ function WorkspaceFilesView({
|
|
|
1056
1080
|
type="button"
|
|
1057
1081
|
className="dsh-files-action-btn"
|
|
1058
1082
|
onClick={e => handleOpenDefaultApp(e, item)}
|
|
1059
|
-
title=
|
|
1083
|
+
title="用系统默认应用打开"
|
|
1060
1084
|
>
|
|
1061
1085
|
<IconExternalApp size={12} />
|
|
1062
1086
|
</button>
|
|
@@ -1071,19 +1095,14 @@ function WorkspaceFilesView({
|
|
|
1071
1095
|
</div>
|
|
1072
1096
|
</div>
|
|
1073
1097
|
))}
|
|
1074
|
-
{flatSearchResults.length > 150 && (
|
|
1075
|
-
<div style={{ padding: '10px 14px', fontSize: 11, textAlign: 'center', color: 'var(--dsw-alias-label-tertiary, #8b93a1)', borderTop: '1px dashed var(--dsw-alias-border-l2, #e5e7eb)' }}>
|
|
1076
|
-
匹配项较多,已限制展示前 150 项以保证流畅度,请输入更详细关键字
|
|
1077
|
-
</div>
|
|
1078
|
-
)}
|
|
1079
1098
|
</div>
|
|
1080
1099
|
</div>
|
|
1081
1100
|
) : (
|
|
1082
|
-
/* ──
|
|
1101
|
+
/* ── 扁平单层映射可视树(高帧率 0 递归开销) ── */
|
|
1083
1102
|
<div className="dsh-files-tree">
|
|
1084
1103
|
{isSearchActive && (
|
|
1085
1104
|
<div className="dsh-files-search-header">
|
|
1086
|
-
<span className="dsh-files-search-count"
|
|
1105
|
+
<span className="dsh-files-search-count">树形匹配(共 {visibleFlatRows.length} 项)</span>
|
|
1087
1106
|
<button
|
|
1088
1107
|
type="button"
|
|
1089
1108
|
className="dsh-files-view-toggle-btn"
|
|
@@ -1095,38 +1114,24 @@ function WorkspaceFilesView({
|
|
|
1095
1114
|
</button>
|
|
1096
1115
|
</div>
|
|
1097
1116
|
)}
|
|
1098
|
-
{
|
|
1099
|
-
<
|
|
1100
|
-
key={
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
isSearchMode={isSearchActive}
|
|
1105
|
-
expandedPaths={isSearchActive ? searchExpandedPaths : expandedPaths}
|
|
1106
|
-
onToggleExpand={(path: string) => {
|
|
1107
|
-
if (isSearchActive) {
|
|
1108
|
-
setSearchExpandedPaths(prev => {
|
|
1109
|
-
const next = new Set(prev)
|
|
1110
|
-
if (next.has(path)) next.delete(path)
|
|
1111
|
-
else next.add(path)
|
|
1112
|
-
return next
|
|
1113
|
-
})
|
|
1114
|
-
} else {
|
|
1115
|
-
handleToggleExpand(path)
|
|
1116
|
-
}
|
|
1117
|
-
}}
|
|
1117
|
+
{visibleFlatRows.map(row => (
|
|
1118
|
+
<FileTreeRow
|
|
1119
|
+
key={row.id}
|
|
1120
|
+
row={row}
|
|
1121
|
+
query={deferredQuery}
|
|
1122
|
+
onToggleExpand={handleToggleExpand}
|
|
1118
1123
|
onOpenFile={onOpenFile}
|
|
1119
1124
|
onMention={handleMention}
|
|
1120
|
-
onCopyPath={handleCopyPath}
|
|
1121
1125
|
onRevealInExplorer={handleRevealInExplorer}
|
|
1122
1126
|
onOpenDefaultApp={handleOpenDefaultApp}
|
|
1127
|
+
onCopyPath={handleCopyPath}
|
|
1123
1128
|
/>
|
|
1124
1129
|
))}
|
|
1125
1130
|
</div>
|
|
1126
1131
|
)}
|
|
1127
1132
|
</div>
|
|
1128
1133
|
|
|
1129
|
-
{/* Toast
|
|
1134
|
+
{/* Floating Animated Toast */}
|
|
1130
1135
|
{toast && (
|
|
1131
1136
|
<div className="dsh-files-toast">
|
|
1132
1137
|
<IconCheck size={12} />
|
|
@@ -1137,165 +1142,141 @@ function WorkspaceFilesView({
|
|
|
1137
1142
|
)
|
|
1138
1143
|
}
|
|
1139
1144
|
|
|
1140
|
-
/* ── Tree
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
depth: number
|
|
1145
|
+
/* ── Memoized Flat Tree Row Component ── */
|
|
1146
|
+
interface FileTreeRowProps {
|
|
1147
|
+
row: VisibleFlatRow
|
|
1144
1148
|
query: string
|
|
1145
|
-
isSearchMode: boolean
|
|
1146
|
-
expandedPaths: Set<string>
|
|
1147
1149
|
onToggleExpand: (path: string) => void
|
|
1148
1150
|
onOpenFile: (path: string) => void
|
|
1149
1151
|
onMention: (e: React.MouseEvent, item: FileItem) => void
|
|
1150
|
-
onCopyPath: (e: React.MouseEvent, path: string) => void
|
|
1151
1152
|
onRevealInExplorer: (e: React.MouseEvent, item: FileItem) => void
|
|
1152
1153
|
onOpenDefaultApp: (e: React.MouseEvent, item: FileItem) => void
|
|
1153
|
-
|
|
1154
|
+
onCopyPath: (e: React.MouseEvent, path: string) => void
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const FileTreeRow = memo(function FileTreeRow(props: FileTreeRowProps): JSX.Element {
|
|
1154
1158
|
const {
|
|
1155
|
-
|
|
1156
|
-
depth,
|
|
1159
|
+
row,
|
|
1157
1160
|
query,
|
|
1158
|
-
isSearchMode,
|
|
1159
|
-
expandedPaths,
|
|
1160
1161
|
onToggleExpand,
|
|
1161
1162
|
onOpenFile,
|
|
1162
1163
|
onMention,
|
|
1163
|
-
onCopyPath,
|
|
1164
1164
|
onRevealInExplorer,
|
|
1165
1165
|
onOpenDefaultApp,
|
|
1166
|
+
onCopyPath,
|
|
1166
1167
|
} = props
|
|
1167
|
-
const isExpanded = expandedPaths.has(item.path)
|
|
1168
1168
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1169
|
+
const indentPx = row.depth * 14 + 8
|
|
1170
|
+
|
|
1171
|
+
if (row.isDirectory) {
|
|
1171
1172
|
return (
|
|
1172
|
-
<div
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
>
|
|
1179
|
-
<
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
</
|
|
1173
|
+
<div
|
|
1174
|
+
className="dsh-files-dir-row"
|
|
1175
|
+
data-tree-path={row.path}
|
|
1176
|
+
style={{ paddingLeft: `${indentPx}px` }}
|
|
1177
|
+
onClick={() => onToggleExpand(row.path)}
|
|
1178
|
+
>
|
|
1179
|
+
<span className={`dsh-files-chevron-wrap ${row.isExpanded ? 'is-expanded' : ''}`}>
|
|
1180
|
+
<IconChevronRight size={11} />
|
|
1181
|
+
</span>
|
|
1182
|
+
<span className="dsh-files-folder-icon">
|
|
1183
|
+
<IconFolder size={14} open={row.isExpanded} />
|
|
1184
|
+
</span>
|
|
1185
|
+
<span className="dsh-files-folder-name" title={row.path}>
|
|
1186
|
+
<HighlightText text={row.name} query={query} />
|
|
1187
|
+
</span>
|
|
1188
|
+
<span className="dsh-files-count-badge">{row.childCount}</span>
|
|
1189
|
+
|
|
1190
|
+
<div className="dsh-files-row-actions">
|
|
1191
|
+
<button
|
|
1192
|
+
type="button"
|
|
1193
|
+
className="dsh-files-action-btn dsh-files-action-at"
|
|
1194
|
+
onClick={e => onMention(e, row.item)}
|
|
1195
|
+
title="一键 @ 引用此目录到会话"
|
|
1196
|
+
>
|
|
1197
|
+
<IconAt size={12} />
|
|
1198
|
+
<span className="dsh-files-action-at-text">@引用</span>
|
|
1199
|
+
</button>
|
|
1200
|
+
<button
|
|
1201
|
+
type="button"
|
|
1202
|
+
className="dsh-files-action-btn"
|
|
1203
|
+
onClick={e => onRevealInExplorer(e, row.item)}
|
|
1204
|
+
title="在文件资源管理器中打开此目录"
|
|
1205
|
+
>
|
|
1206
|
+
<IconExplorer size={12} />
|
|
1207
|
+
</button>
|
|
1208
|
+
<button
|
|
1209
|
+
type="button"
|
|
1210
|
+
className="dsh-files-action-btn"
|
|
1211
|
+
onClick={e => onOpenDefaultApp(e, row.item)}
|
|
1212
|
+
title="用系统默认应用打开此目录"
|
|
1213
|
+
>
|
|
1214
|
+
<IconExternalApp size={12} />
|
|
1215
|
+
</button>
|
|
1216
|
+
<button
|
|
1217
|
+
type="button"
|
|
1218
|
+
className="dsh-files-action-btn"
|
|
1219
|
+
onClick={e => onCopyPath(e, row.path)}
|
|
1220
|
+
title="复制目录路径"
|
|
1221
|
+
>
|
|
1222
|
+
<IconCopy size={12} />
|
|
1223
|
+
</button>
|
|
1223
1224
|
</div>
|
|
1224
|
-
{isExpanded && item.children && (
|
|
1225
|
-
<div className="dsh-files-children">
|
|
1226
|
-
{item.children.map(child => (
|
|
1227
|
-
<FileTreeNode
|
|
1228
|
-
key={child.path}
|
|
1229
|
-
item={child}
|
|
1230
|
-
depth={depth + 1}
|
|
1231
|
-
query={query}
|
|
1232
|
-
isSearchMode={isSearchMode}
|
|
1233
|
-
expandedPaths={expandedPaths}
|
|
1234
|
-
onToggleExpand={onToggleExpand}
|
|
1235
|
-
onOpenFile={onOpenFile}
|
|
1236
|
-
onMention={onMention}
|
|
1237
|
-
onCopyPath={onCopyPath}
|
|
1238
|
-
onRevealInExplorer={onRevealInExplorer}
|
|
1239
|
-
onOpenDefaultApp={onOpenDefaultApp}
|
|
1240
|
-
/>
|
|
1241
|
-
))}
|
|
1242
|
-
</div>
|
|
1243
|
-
)}
|
|
1244
1225
|
</div>
|
|
1245
1226
|
)
|
|
1246
1227
|
}
|
|
1247
1228
|
|
|
1248
|
-
// File item row
|
|
1249
1229
|
return (
|
|
1250
1230
|
<div
|
|
1251
1231
|
className="dsh-files-row"
|
|
1252
|
-
data-tree-path={
|
|
1253
|
-
style={{ paddingLeft: `${
|
|
1254
|
-
onClick={() => onOpenFile(
|
|
1255
|
-
title={
|
|
1232
|
+
data-tree-path={row.path}
|
|
1233
|
+
style={{ paddingLeft: `${indentPx}px` }}
|
|
1234
|
+
onClick={() => onOpenFile(row.path)}
|
|
1235
|
+
title={`点击打开文件: ${row.path}`}
|
|
1256
1236
|
>
|
|
1257
|
-
<
|
|
1237
|
+
<span className="dsh-files-file-glyph">
|
|
1238
|
+
<FileGlyphIcon filename={row.name} size={14} />
|
|
1239
|
+
</span>
|
|
1258
1240
|
<span className="dsh-files-name">
|
|
1259
|
-
<HighlightText text={
|
|
1241
|
+
<HighlightText text={row.name} query={query} />
|
|
1260
1242
|
</span>
|
|
1261
1243
|
|
|
1262
|
-
{
|
|
1263
|
-
<span className="dsh-files-size" title={`大小: ${
|
|
1264
|
-
{formatFileSize(
|
|
1244
|
+
{row.size !== undefined && (
|
|
1245
|
+
<span className="dsh-files-size" title={`大小: ${row.size} 字节`}>
|
|
1246
|
+
{formatFileSize(row.size)}
|
|
1265
1247
|
</span>
|
|
1266
1248
|
)}
|
|
1267
1249
|
|
|
1268
|
-
{/* Row Hover Actions */}
|
|
1269
1250
|
<div className="dsh-files-row-actions">
|
|
1270
1251
|
<button
|
|
1271
1252
|
type="button"
|
|
1272
1253
|
className="dsh-files-action-btn dsh-files-action-at"
|
|
1273
|
-
onClick={e => onMention(e, item)}
|
|
1274
|
-
title="一键 @
|
|
1254
|
+
onClick={e => onMention(e, row.item)}
|
|
1255
|
+
title="一键 @ 引用此文件到会话"
|
|
1275
1256
|
>
|
|
1276
|
-
<IconAt size={
|
|
1257
|
+
<IconAt size={12} />
|
|
1277
1258
|
<span className="dsh-files-action-at-text">@引用</span>
|
|
1278
1259
|
</button>
|
|
1279
1260
|
<button
|
|
1280
1261
|
type="button"
|
|
1281
1262
|
className="dsh-files-action-btn"
|
|
1282
|
-
onClick={e => onRevealInExplorer(e, item)}
|
|
1283
|
-
title="
|
|
1263
|
+
onClick={e => onRevealInExplorer(e, row.item)}
|
|
1264
|
+
title="在文件资源管理器中定位"
|
|
1284
1265
|
>
|
|
1285
1266
|
<IconExplorer size={12} />
|
|
1286
1267
|
</button>
|
|
1287
1268
|
<button
|
|
1288
1269
|
type="button"
|
|
1289
1270
|
className="dsh-files-action-btn"
|
|
1290
|
-
onClick={e => onOpenDefaultApp(e, item)}
|
|
1291
|
-
title="
|
|
1271
|
+
onClick={e => onOpenDefaultApp(e, row.item)}
|
|
1272
|
+
title="用系统默认应用打开"
|
|
1292
1273
|
>
|
|
1293
1274
|
<IconExternalApp size={12} />
|
|
1294
1275
|
</button>
|
|
1295
1276
|
<button
|
|
1296
1277
|
type="button"
|
|
1297
1278
|
className="dsh-files-action-btn"
|
|
1298
|
-
onClick={e => onCopyPath(e,
|
|
1279
|
+
onClick={e => onCopyPath(e, row.path)}
|
|
1299
1280
|
title="复制相对路径"
|
|
1300
1281
|
>
|
|
1301
1282
|
<IconCopy size={12} />
|
|
@@ -1303,18 +1284,25 @@ function FileTreeNode(props: {
|
|
|
1303
1284
|
</div>
|
|
1304
1285
|
</div>
|
|
1305
1286
|
)
|
|
1306
|
-
}
|
|
1287
|
+
}, (prev, next) => {
|
|
1288
|
+
return (
|
|
1289
|
+
prev.row.id === next.row.id &&
|
|
1290
|
+
prev.row.isExpanded === next.row.isExpanded &&
|
|
1291
|
+
prev.row.depth === next.row.depth &&
|
|
1292
|
+
prev.row.childCount === next.row.childCount &&
|
|
1293
|
+
prev.query === next.query
|
|
1294
|
+
)
|
|
1295
|
+
})
|
|
1307
1296
|
|
|
1308
1297
|
/* ── Helpers ── */
|
|
1309
1298
|
function HighlightText({ text, query }: { text: string; query: string }): JSX.Element {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
const index = text.toLowerCase().indexOf(q)
|
|
1299
|
+
if (!query) return <span>{text}</span>
|
|
1300
|
+
const index = text.toLowerCase().indexOf(query)
|
|
1313
1301
|
if (index === -1) return <span>{text}</span>
|
|
1314
1302
|
|
|
1315
1303
|
const before = text.slice(0, index)
|
|
1316
|
-
const match = text.slice(index, index +
|
|
1317
|
-
const after = text.slice(index +
|
|
1304
|
+
const match = text.slice(index, index + query.length)
|
|
1305
|
+
const after = text.slice(index + query.length)
|
|
1318
1306
|
|
|
1319
1307
|
return (
|
|
1320
1308
|
<span>
|
|
@@ -1333,12 +1321,6 @@ function formatFileSize(bytes: number): string {
|
|
|
1333
1321
|
return `${mb.toFixed(1)} MB`
|
|
1334
1322
|
}
|
|
1335
1323
|
|
|
1336
|
-
/**
|
|
1337
|
-
* 按照 DSH 官方规范格式化引用路径:
|
|
1338
|
-
* - 纯路径无空格:@path
|
|
1339
|
-
* - 路径含空格:@"path"
|
|
1340
|
-
* - 目录末尾保留斜杠:@folder/ 或 @"folder/"
|
|
1341
|
-
*/
|
|
1342
1324
|
function formatMentionText(path: string, isDirectory = false): string {
|
|
1343
1325
|
const cleanPath = path.replace(/\\/g, '/').replace(/^\.?\//, '')
|
|
1344
1326
|
const finalPath = isDirectory ? (cleanPath.endsWith('/') ? cleanPath : `${cleanPath}/`) : cleanPath
|
|
@@ -1371,11 +1353,7 @@ function saveStoredExpandedPaths(workspaceRoot: string | undefined, paths: Set<s
|
|
|
1371
1353
|
} catch {}
|
|
1372
1354
|
}
|
|
1373
1355
|
|
|
1374
|
-
/**
|
|
1375
|
-
* 从当前 DOM 的 React Fiber 树或 Cordis RootContext 中提取当前活跃输入框的 SessionInputShell 实例
|
|
1376
|
-
*/
|
|
1377
1356
|
function findComposerShell(ctx: Context | null, sessionId?: string): any {
|
|
1378
|
-
// 1. 优先从 DOM 的 React Fiber 提取(直接命中当前活跃输入框的组件 props)
|
|
1379
1357
|
try {
|
|
1380
1358
|
const composerDom = document.querySelector<HTMLElement>(
|
|
1381
1359
|
'[data-composer-input="true"], [data-composer-input], [data-composer-card], div[role="textbox"][contenteditable="true"]'
|
|
@@ -1400,11 +1378,8 @@ function findComposerShell(ctx: Context | null, sessionId?: string): any {
|
|
|
1400
1378
|
}
|
|
1401
1379
|
}
|
|
1402
1380
|
}
|
|
1403
|
-
} catch
|
|
1404
|
-
console.debug('[dsh-workspace-files] 从 Fiber 查找 keyboard 失败:', e)
|
|
1405
|
-
}
|
|
1381
|
+
} catch {}
|
|
1406
1382
|
|
|
1407
|
-
// 2. 从 Cordis RootContext 查找
|
|
1408
1383
|
if (ctx) {
|
|
1409
1384
|
try {
|
|
1410
1385
|
const root: any = (ctx as any).root || ctx
|
|
@@ -1420,30 +1395,22 @@ function findComposerShell(ctx: Context | null, sessionId?: string): any {
|
|
|
1420
1395
|
return values[values.length - 1]
|
|
1421
1396
|
}
|
|
1422
1397
|
}
|
|
1423
|
-
} catch
|
|
1424
|
-
console.debug('[dsh-workspace-files] 从 RootContext 查找 shell 失败:', e)
|
|
1425
|
-
}
|
|
1398
|
+
} catch {}
|
|
1426
1399
|
}
|
|
1427
1400
|
|
|
1428
1401
|
return null
|
|
1429
1402
|
}
|
|
1430
1403
|
|
|
1431
|
-
/**
|
|
1432
|
-
* 尝试向会话输入框插入原生的 ReferenceChipNode(官方胶囊 Tag 卡片)
|
|
1433
|
-
*/
|
|
1434
1404
|
function tryInsertNativeReferenceChip(ctx: Context | null, sessionId: string | undefined, item: FileItem): boolean {
|
|
1435
1405
|
if (typeof document === 'undefined') return false
|
|
1436
1406
|
try {
|
|
1437
|
-
// 聚焦输入框
|
|
1438
1407
|
const composerDom = document.querySelector<HTMLElement>(
|
|
1439
1408
|
'[data-composer-input="true"], [data-composer-input], [data-composer-card] [contenteditable="true"], div[role="textbox"][contenteditable="true"]'
|
|
1440
1409
|
)
|
|
1441
1410
|
if (composerDom) composerDom.focus()
|
|
1442
1411
|
|
|
1443
1412
|
const shell = findComposerShell(ctx, sessionId)
|
|
1444
|
-
if (!shell)
|
|
1445
|
-
return false
|
|
1446
|
-
}
|
|
1413
|
+
if (!shell) return false
|
|
1447
1414
|
|
|
1448
1415
|
const cleanPath = item.path.replace(/\\/g, '/').replace(/^\.?\//, '')
|
|
1449
1416
|
const mention = item.isDirectory ? (cleanPath.endsWith('/') ? `@${cleanPath}` : `@${cleanPath}/`) : `@${cleanPath}`
|
|
@@ -1462,7 +1429,6 @@ function tryInsertNativeReferenceChip(ctx: Context | null, sessionId: string | u
|
|
|
1462
1429
|
let start = caret
|
|
1463
1430
|
const end = caret
|
|
1464
1431
|
|
|
1465
|
-
// 智能消除:若当前光标前是 '@',回退一个字符将其吃掉替换
|
|
1466
1432
|
if (start > 0 && text[start - 1] === '@') {
|
|
1467
1433
|
start = start - 1
|
|
1468
1434
|
}
|
|
@@ -1473,33 +1439,23 @@ function tryInsertNativeReferenceChip(ctx: Context | null, sessionId: string | u
|
|
|
1473
1439
|
end,
|
|
1474
1440
|
}
|
|
1475
1441
|
|
|
1476
|
-
// 优先调用原生 shell.insertReference 插入 Chip Tag
|
|
1477
1442
|
if (typeof shell.insertReference === 'function') {
|
|
1478
1443
|
const ok = shell.insertReference(reference, span)
|
|
1479
1444
|
if (ok) return true
|
|
1480
1445
|
}
|
|
1481
1446
|
|
|
1482
|
-
// 备用:尝试通过 session actx 派发事件
|
|
1483
1447
|
const actx = shell.deps?.actx
|
|
1484
1448
|
if (actx && typeof actx.bail === 'function') {
|
|
1485
1449
|
const ok = actx.bail(actx, 'slash/input-insert-reference', { reference, span })
|
|
1486
1450
|
if (ok === true) return true
|
|
1487
1451
|
}
|
|
1488
|
-
} catch
|
|
1489
|
-
console.debug('[dsh-workspace-files] 插入原生 Chip 异常:', err)
|
|
1490
|
-
}
|
|
1452
|
+
} catch {}
|
|
1491
1453
|
return false
|
|
1492
1454
|
}
|
|
1493
1455
|
|
|
1494
|
-
/**
|
|
1495
|
-
* 将引用文本注入到 DSH 当前会话的主输入框。
|
|
1496
|
-
* 兼容 DSH 官方基于 Lexical 的 ComposerContentEditable (div[data-composer-input])
|
|
1497
|
-
* 以及传统的 textarea / input,并自动消除用户手动输入的冗余 '@'。
|
|
1498
|
-
*/
|
|
1499
1456
|
function insertMentionToComposer(mentionText: string): boolean {
|
|
1500
1457
|
if (typeof document === 'undefined') return false
|
|
1501
1458
|
|
|
1502
|
-
// 1. 查找 DSH 官方输入框(Lexical contenteditable 容器优先)
|
|
1503
1459
|
const candidates = [
|
|
1504
1460
|
document.querySelector<HTMLElement>('[data-composer-input="true"]'),
|
|
1505
1461
|
document.querySelector<HTMLElement>('[data-composer-input]'),
|
|
@@ -1513,7 +1469,6 @@ function insertMentionToComposer(mentionText: string): boolean {
|
|
|
1513
1469
|
|
|
1514
1470
|
target.focus()
|
|
1515
1471
|
|
|
1516
|
-
// 策略 A: contenteditable (DSH 官方 Lexical 编辑器)
|
|
1517
1472
|
if (target.getAttribute('contenteditable') === 'true' || target.isContentEditable) {
|
|
1518
1473
|
const sel = window.getSelection()
|
|
1519
1474
|
if (sel) {
|
|
@@ -1527,11 +1482,10 @@ function insertMentionToComposer(mentionText: string): boolean {
|
|
|
1527
1482
|
if (!isInTarget) {
|
|
1528
1483
|
const range = document.createRange()
|
|
1529
1484
|
range.selectNodeContents(target)
|
|
1530
|
-
range.collapse(false)
|
|
1485
|
+
range.collapse(false)
|
|
1531
1486
|
sel.removeAllRanges()
|
|
1532
1487
|
sel.addRange(range)
|
|
1533
1488
|
} else {
|
|
1534
|
-
// 智能消除:若光标前一个字符是已键入的 '@',将其纳入选区进行覆盖替换,避免出现 '@@file'
|
|
1535
1489
|
const range = sel.getRangeAt(0)
|
|
1536
1490
|
if (range.collapsed && range.startContainer.nodeType === Node.TEXT_NODE) {
|
|
1537
1491
|
const nodeText = range.startContainer.textContent || ''
|
|
@@ -1545,7 +1499,6 @@ function insertMentionToComposer(mentionText: string): boolean {
|
|
|
1545
1499
|
}
|
|
1546
1500
|
}
|
|
1547
1501
|
|
|
1548
|
-
// 原生 execCommand('insertText') 会直接触发 Lexical 的 input/beforeinput 事件流并更新 React 状态
|
|
1549
1502
|
let success = false
|
|
1550
1503
|
try {
|
|
1551
1504
|
success = document.execCommand('insertText', false, mentionText)
|
|
@@ -1567,13 +1520,11 @@ function insertMentionToComposer(mentionText: string): boolean {
|
|
|
1567
1520
|
return success
|
|
1568
1521
|
}
|
|
1569
1522
|
|
|
1570
|
-
// 策略 B: 普通 textarea / input
|
|
1571
1523
|
if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) {
|
|
1572
1524
|
let start = target.selectionStart ?? target.value.length
|
|
1573
1525
|
const end = target.selectionEnd ?? target.value.length
|
|
1574
1526
|
const prev = target.value
|
|
1575
1527
|
|
|
1576
|
-
// 智能消除:若前面紧随 '@',覆盖它
|
|
1577
1528
|
if (start > 0 && prev[start - 1] === '@') {
|
|
1578
1529
|
start = start - 1
|
|
1579
1530
|
}
|
|
@@ -1637,18 +1588,23 @@ async function call<T = any>(method: string, payload: Record<string, unknown> =
|
|
|
1637
1588
|
return envelope.value
|
|
1638
1589
|
}
|
|
1639
1590
|
|
|
1640
|
-
/* ── CSS
|
|
1641
|
-
function
|
|
1642
|
-
|
|
1643
|
-
|
|
1591
|
+
/* ── Singleton Dynamic CSS Injection (Zero React re-evaluation) ── */
|
|
1592
|
+
function ensureFilesStyle() {
|
|
1593
|
+
if (typeof document === 'undefined') return
|
|
1594
|
+
const id = 'dsh-workspace-files-core-style'
|
|
1595
|
+
if (document.getElementById(id)) return
|
|
1596
|
+
|
|
1597
|
+
const style = document.createElement('style')
|
|
1598
|
+
style.id = id
|
|
1599
|
+
style.textContent = `
|
|
1644
1600
|
.dsh-files-root {
|
|
1645
1601
|
display: flex;
|
|
1646
1602
|
flex-direction: column;
|
|
1647
1603
|
height: 100%;
|
|
1648
1604
|
min-height: 0;
|
|
1649
1605
|
box-sizing: border-box;
|
|
1650
|
-
background: var(--dsw-alias-
|
|
1651
|
-
color: var(--dsw-alias-label-primary
|
|
1606
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
1607
|
+
color: var(--dsw-alias-label-primary);
|
|
1652
1608
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
1653
1609
|
font-size: 12px;
|
|
1654
1610
|
line-height: 1.4;
|
|
@@ -1664,7 +1620,8 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1664
1620
|
justify-content: space-between;
|
|
1665
1621
|
gap: 8px;
|
|
1666
1622
|
padding: 8px 12px;
|
|
1667
|
-
border-bottom:
|
|
1623
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l1);
|
|
1624
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
1668
1625
|
}
|
|
1669
1626
|
.dsh-files-header-info {
|
|
1670
1627
|
flex: 1;
|
|
@@ -1678,33 +1635,34 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1678
1635
|
.dsh-files-title {
|
|
1679
1636
|
font-weight: 600;
|
|
1680
1637
|
font-size: 13px;
|
|
1681
|
-
color: var(--dsw-alias-label-primary
|
|
1638
|
+
color: var(--dsw-alias-label-primary);
|
|
1682
1639
|
}
|
|
1683
1640
|
.dsh-files-stats-badge {
|
|
1684
1641
|
font-size: 10px;
|
|
1685
1642
|
font-weight: 500;
|
|
1686
|
-
color: var(--dsw-alias-label-tertiary
|
|
1687
|
-
background: var(--dsw-alias-
|
|
1688
|
-
padding: 1px
|
|
1643
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1644
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);
|
|
1645
|
+
padding: 1px 7px;
|
|
1689
1646
|
border-radius: 99px;
|
|
1690
1647
|
}
|
|
1691
1648
|
.dsh-files-root-path {
|
|
1692
1649
|
font-size: 11px;
|
|
1693
|
-
color: var(--dsw-alias-label-tertiary
|
|
1650
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1694
1651
|
overflow: hidden;
|
|
1695
1652
|
text-overflow: ellipsis;
|
|
1696
1653
|
white-space: nowrap;
|
|
1697
|
-
margin-top:
|
|
1654
|
+
margin-top: 2px;
|
|
1698
1655
|
}
|
|
1699
1656
|
|
|
1700
|
-
/* ── Toolbar &
|
|
1657
|
+
/* ── Toolbar & Pure White Input ── */
|
|
1701
1658
|
.dsh-files-toolbar {
|
|
1702
1659
|
display: flex;
|
|
1703
1660
|
align-items: center;
|
|
1704
1661
|
gap: 6px;
|
|
1705
|
-
padding: 6px
|
|
1706
|
-
border-bottom:
|
|
1707
|
-
background: var(--dsw-alias-
|
|
1662
|
+
padding: 6px 10px;
|
|
1663
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
1664
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
1665
|
+
flex-shrink: 0;
|
|
1708
1666
|
}
|
|
1709
1667
|
.dsh-files-search-wrap {
|
|
1710
1668
|
position: relative;
|
|
@@ -1715,70 +1673,77 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1715
1673
|
}
|
|
1716
1674
|
.dsh-files-search-icon {
|
|
1717
1675
|
position: absolute;
|
|
1718
|
-
left:
|
|
1719
|
-
color: var(--dsw-alias-label-tertiary
|
|
1676
|
+
left: 8px;
|
|
1677
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1720
1678
|
pointer-events: none;
|
|
1721
1679
|
display: flex;
|
|
1722
1680
|
}
|
|
1723
1681
|
.dsh-files-search-input {
|
|
1724
1682
|
box-sizing: border-box;
|
|
1725
1683
|
width: 100%;
|
|
1726
|
-
height:
|
|
1727
|
-
padding: 0
|
|
1728
|
-
border:
|
|
1729
|
-
border-radius:
|
|
1730
|
-
background: var(--dsw-alias-
|
|
1731
|
-
color:
|
|
1732
|
-
font-size:
|
|
1684
|
+
height: 28px;
|
|
1685
|
+
padding: 0 24px 0 28px;
|
|
1686
|
+
border: 0.5px solid var(--dsw-alias-border-l1);
|
|
1687
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
1688
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
1689
|
+
color: var(--dsw-alias-label-primary);
|
|
1690
|
+
font-size: 11px;
|
|
1733
1691
|
outline: none;
|
|
1734
|
-
transition:
|
|
1692
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
|
|
1735
1693
|
}
|
|
1736
1694
|
.dsh-files-search-input:focus {
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-
|
|
1695
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
1696
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
1697
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent);
|
|
1740
1698
|
}
|
|
1741
1699
|
.dsh-files-clear-btn {
|
|
1742
1700
|
position: absolute;
|
|
1743
|
-
right:
|
|
1701
|
+
right: 6px;
|
|
1744
1702
|
border: none;
|
|
1745
1703
|
background: transparent;
|
|
1746
|
-
color: var(--dsw-alias-label-tertiary
|
|
1704
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1747
1705
|
cursor: pointer;
|
|
1748
1706
|
padding: 2px;
|
|
1749
1707
|
display: flex;
|
|
1708
|
+
border-radius: 50%;
|
|
1709
|
+
transition: all 0.12s ease;
|
|
1750
1710
|
}
|
|
1751
1711
|
.dsh-files-clear-btn:hover {
|
|
1752
|
-
color: var(--dsw-alias-label-primary
|
|
1712
|
+
color: var(--dsw-alias-label-primary);
|
|
1713
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1753
1714
|
}
|
|
1754
1715
|
.dsh-files-toolbar-btns {
|
|
1755
1716
|
display: flex;
|
|
1756
1717
|
align-items: center;
|
|
1757
1718
|
gap: 2px;
|
|
1719
|
+
flex-shrink: 0;
|
|
1758
1720
|
}
|
|
1759
1721
|
.dsh-files-tool-btn {
|
|
1760
1722
|
display: inline-flex;
|
|
1761
1723
|
align-items: center;
|
|
1762
1724
|
justify-content: center;
|
|
1763
|
-
width:
|
|
1764
|
-
height:
|
|
1725
|
+
width: 26px;
|
|
1726
|
+
height: 26px;
|
|
1765
1727
|
border: none;
|
|
1766
|
-
border-radius: 4px;
|
|
1728
|
+
border-radius: var(--dsw-radius-xs, 4px);
|
|
1767
1729
|
background: transparent;
|
|
1768
|
-
color: var(--dsw-alias-label-secondary
|
|
1730
|
+
color: var(--dsw-alias-label-secondary);
|
|
1769
1731
|
cursor: pointer;
|
|
1770
|
-
transition: all 0.
|
|
1732
|
+
transition: all 0.15s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1771
1733
|
}
|
|
1772
1734
|
.dsh-files-tool-btn:hover:not(:disabled) {
|
|
1773
|
-
color: var(--dsw-alias-label-primary
|
|
1774
|
-
background: var(--dsw-alias-interactive-bg-hover
|
|
1735
|
+
color: var(--dsw-alias-label-primary);
|
|
1736
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1737
|
+
}
|
|
1738
|
+
.dsh-files-tool-btn:active:not(:disabled) {
|
|
1739
|
+
transform: scale(0.94);
|
|
1775
1740
|
}
|
|
1776
1741
|
.dsh-files-tool-btn.is-active {
|
|
1777
|
-
color: var(--dsw-alias-
|
|
1778
|
-
background: var(--dsw-alias-
|
|
1742
|
+
color: var(--dsw-alias-state-business-primary);
|
|
1743
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 14%, transparent);
|
|
1779
1744
|
}
|
|
1780
1745
|
.dsh-files-tool-btn:disabled {
|
|
1781
|
-
opacity: 0.
|
|
1746
|
+
opacity: 0.35;
|
|
1782
1747
|
cursor: not-allowed;
|
|
1783
1748
|
}
|
|
1784
1749
|
|
|
@@ -1790,10 +1755,8 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1790
1755
|
overflow-x: hidden;
|
|
1791
1756
|
padding: 4px 0;
|
|
1792
1757
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
flex-direction: column;
|
|
1796
|
-
}
|
|
1758
|
+
|
|
1759
|
+
/* ── Directory Row (with Motion Chevron) ── */
|
|
1797
1760
|
.dsh-files-dir-row {
|
|
1798
1761
|
display: flex;
|
|
1799
1762
|
align-items: center;
|
|
@@ -1803,29 +1766,35 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1803
1766
|
cursor: pointer;
|
|
1804
1767
|
user-select: none;
|
|
1805
1768
|
position: relative;
|
|
1806
|
-
color: var(--dsw-alias-label-
|
|
1769
|
+
color: var(--dsw-alias-label-primary);
|
|
1770
|
+
border-radius: var(--dsw-radius-xs, 4px);
|
|
1771
|
+
margin: 0 4px;
|
|
1807
1772
|
transition: background 0.12s ease;
|
|
1808
1773
|
}
|
|
1809
1774
|
.dsh-files-dir-row:hover {
|
|
1810
|
-
background: var(--dsw-alias-interactive-bg-hover
|
|
1811
|
-
color: var(--dsw-alias-label-primary, inherit);
|
|
1775
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1812
1776
|
}
|
|
1813
|
-
.dsh-files-chevron {
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
display: inline-
|
|
1817
|
-
|
|
1777
|
+
.dsh-files-chevron-wrap {
|
|
1778
|
+
width: 12px;
|
|
1779
|
+
height: 12px;
|
|
1780
|
+
display: inline-flex;
|
|
1781
|
+
align-items: center;
|
|
1782
|
+
justify-content: center;
|
|
1783
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1784
|
+
transition: transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1785
|
+
flex-shrink: 0;
|
|
1818
1786
|
}
|
|
1819
|
-
.dsh-files-chevron
|
|
1820
|
-
transform: rotate(
|
|
1787
|
+
.dsh-files-chevron-wrap.is-expanded {
|
|
1788
|
+
transform: rotate(90deg);
|
|
1821
1789
|
}
|
|
1822
1790
|
.dsh-files-folder-icon {
|
|
1823
1791
|
display: flex;
|
|
1792
|
+
flex-shrink: 0;
|
|
1824
1793
|
}
|
|
1825
1794
|
.dsh-files-folder-name {
|
|
1826
1795
|
flex: 1;
|
|
1827
1796
|
min-width: 0;
|
|
1828
|
-
font-weight:
|
|
1797
|
+
font-weight: 550;
|
|
1829
1798
|
font-size: 12px;
|
|
1830
1799
|
overflow: hidden;
|
|
1831
1800
|
text-overflow: ellipsis;
|
|
@@ -1836,8 +1805,8 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1836
1805
|
font-weight: 600;
|
|
1837
1806
|
padding: 0 5px;
|
|
1838
1807
|
border-radius: 99px;
|
|
1839
|
-
background: var(--dsw-alias-
|
|
1840
|
-
color: var(--dsw-alias-label-tertiary
|
|
1808
|
+
background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);
|
|
1809
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1841
1810
|
}
|
|
1842
1811
|
|
|
1843
1812
|
/* ── File Row ── */
|
|
@@ -1850,44 +1819,95 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1850
1819
|
cursor: pointer;
|
|
1851
1820
|
user-select: none;
|
|
1852
1821
|
position: relative;
|
|
1822
|
+
border-radius: var(--dsw-radius-xs, 4px);
|
|
1823
|
+
margin: 0 4px;
|
|
1853
1824
|
transition: background 0.12s ease;
|
|
1854
1825
|
}
|
|
1855
1826
|
.dsh-files-row:hover {
|
|
1856
|
-
background: var(--dsw-alias-interactive-bg-hover
|
|
1827
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1828
|
+
}
|
|
1829
|
+
.dsh-files-file-glyph {
|
|
1830
|
+
margin-left: 12px;
|
|
1831
|
+
display: inline-flex;
|
|
1832
|
+
align-items: center;
|
|
1833
|
+
justify-content: center;
|
|
1857
1834
|
}
|
|
1858
1835
|
.dsh-files-name {
|
|
1859
1836
|
flex: 1;
|
|
1860
1837
|
min-width: 0;
|
|
1861
1838
|
font-size: 12px;
|
|
1862
|
-
color: var(--dsw-alias-label-primary
|
|
1839
|
+
color: var(--dsw-alias-label-primary);
|
|
1863
1840
|
overflow: hidden;
|
|
1864
1841
|
text-overflow: ellipsis;
|
|
1865
1842
|
white-space: nowrap;
|
|
1866
1843
|
}
|
|
1867
1844
|
.dsh-files-size {
|
|
1868
1845
|
font-size: 10px;
|
|
1869
|
-
color: var(--dsw-alias-label-tertiary
|
|
1846
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1870
1847
|
flex-shrink: 0;
|
|
1871
1848
|
margin-right: 4px;
|
|
1872
1849
|
}
|
|
1873
1850
|
|
|
1874
|
-
/* ── Hover
|
|
1851
|
+
/* ── Micro-Interaction Hover Action Pills ── */
|
|
1875
1852
|
.dsh-files-row-actions {
|
|
1876
|
-
display:
|
|
1853
|
+
display: flex;
|
|
1877
1854
|
align-items: center;
|
|
1878
|
-
gap:
|
|
1855
|
+
gap: 2px;
|
|
1879
1856
|
flex-shrink: 0;
|
|
1857
|
+
opacity: 0;
|
|
1858
|
+
transform: translateX(4px);
|
|
1859
|
+
pointer-events: none;
|
|
1860
|
+
transition: opacity 0.15s cubic-bezier(0.16, 1, 0.3, 1), transform 0.15s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1861
|
+
margin-left: auto;
|
|
1880
1862
|
}
|
|
1881
1863
|
.dsh-files-row:hover .dsh-files-row-actions,
|
|
1882
1864
|
.dsh-files-dir-row:hover .dsh-files-row-actions,
|
|
1883
1865
|
.dsh-files-flat-row:hover .dsh-files-row-actions {
|
|
1884
|
-
|
|
1866
|
+
opacity: 1;
|
|
1867
|
+
transform: translateX(0);
|
|
1868
|
+
pointer-events: auto;
|
|
1885
1869
|
}
|
|
1886
1870
|
.dsh-files-row:hover .dsh-files-size,
|
|
1887
1871
|
.dsh-files-dir-row:hover .dsh-files-count-badge,
|
|
1888
1872
|
.dsh-files-flat-row:hover .dsh-files-size {
|
|
1889
1873
|
display: none;
|
|
1890
1874
|
}
|
|
1875
|
+
.dsh-files-action-btn {
|
|
1876
|
+
display: inline-flex;
|
|
1877
|
+
align-items: center;
|
|
1878
|
+
justify-content: center;
|
|
1879
|
+
gap: 3px;
|
|
1880
|
+
height: 20px;
|
|
1881
|
+
padding: 0 5px;
|
|
1882
|
+
border: 0.5px solid var(--dsw-alias-border-l1);
|
|
1883
|
+
border-radius: var(--dsw-radius-xs, 3px);
|
|
1884
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
1885
|
+
color: var(--dsw-alias-label-secondary);
|
|
1886
|
+
cursor: pointer;
|
|
1887
|
+
transition: all 0.12s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1888
|
+
}
|
|
1889
|
+
.dsh-files-action-btn:hover {
|
|
1890
|
+
color: var(--dsw-alias-label-primary);
|
|
1891
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
1892
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1893
|
+
}
|
|
1894
|
+
.dsh-files-action-btn:active {
|
|
1895
|
+
transform: scale(0.92);
|
|
1896
|
+
}
|
|
1897
|
+
.dsh-files-action-at {
|
|
1898
|
+
color: var(--dsw-alias-state-business-primary);
|
|
1899
|
+
border-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 35%, transparent);
|
|
1900
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);
|
|
1901
|
+
}
|
|
1902
|
+
.dsh-files-action-at:hover {
|
|
1903
|
+
color: var(--dsw-alias-label-primary-inverted, #ffffff);
|
|
1904
|
+
background: var(--dsw-alias-state-business-primary);
|
|
1905
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
1906
|
+
}
|
|
1907
|
+
.dsh-files-action-at-text {
|
|
1908
|
+
font-size: 10px;
|
|
1909
|
+
font-weight: 600;
|
|
1910
|
+
}
|
|
1891
1911
|
|
|
1892
1912
|
/* ── Flat Search Result List ── */
|
|
1893
1913
|
.dsh-files-search-container {
|
|
@@ -1900,9 +1920,9 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1900
1920
|
justify-content: space-between;
|
|
1901
1921
|
padding: 6px 12px;
|
|
1902
1922
|
font-size: 11px;
|
|
1903
|
-
color: var(--dsw-alias-label-tertiary
|
|
1904
|
-
background: color-mix(in srgb, var(--dsw-alias-
|
|
1905
|
-
border-bottom:
|
|
1923
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1924
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 4%, transparent);
|
|
1925
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
1906
1926
|
}
|
|
1907
1927
|
.dsh-files-search-count {
|
|
1908
1928
|
font-weight: 500;
|
|
@@ -1911,20 +1931,20 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1911
1931
|
display: inline-flex;
|
|
1912
1932
|
align-items: center;
|
|
1913
1933
|
gap: 4px;
|
|
1914
|
-
border:
|
|
1915
|
-
background: var(--dsw-alias-
|
|
1916
|
-
color: var(--dsw-alias-
|
|
1934
|
+
border: 0.5px solid var(--dsw-alias-border-l1);
|
|
1935
|
+
background: var(--dsw-alias-fill-base, #ffffff);
|
|
1936
|
+
color: var(--dsw-alias-state-business-primary);
|
|
1917
1937
|
cursor: pointer;
|
|
1918
1938
|
font-size: 10.5px;
|
|
1919
1939
|
font-weight: 500;
|
|
1920
1940
|
padding: 2px 7px;
|
|
1921
|
-
border-radius: 4px;
|
|
1922
|
-
transition: all 0.12s
|
|
1941
|
+
border-radius: var(--dsw-radius-xs, 4px);
|
|
1942
|
+
transition: all 0.12s cubic-bezier(0.16, 1, 0.3, 1);
|
|
1923
1943
|
}
|
|
1924
1944
|
.dsh-files-view-toggle-btn:hover {
|
|
1925
|
-
background: var(--dsw-alias-
|
|
1926
|
-
color: #ffffff;
|
|
1927
|
-
border-color: var(--dsw-alias-
|
|
1945
|
+
background: var(--dsw-alias-state-business-primary);
|
|
1946
|
+
color: var(--dsw-alias-label-primary-inverted, #ffffff);
|
|
1947
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
1928
1948
|
}
|
|
1929
1949
|
.dsh-files-flat-list {
|
|
1930
1950
|
padding: 4px 0;
|
|
@@ -1933,7 +1953,7 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1933
1953
|
display: flex;
|
|
1934
1954
|
align-items: center;
|
|
1935
1955
|
gap: 8px;
|
|
1936
|
-
padding:
|
|
1956
|
+
padding: 5px 12px;
|
|
1937
1957
|
cursor: pointer;
|
|
1938
1958
|
user-select: none;
|
|
1939
1959
|
position: relative;
|
|
@@ -1941,7 +1961,7 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1941
1961
|
min-height: 32px;
|
|
1942
1962
|
}
|
|
1943
1963
|
.dsh-files-flat-row:hover {
|
|
1944
|
-
background: var(--dsw-alias-interactive-bg-hover
|
|
1964
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1945
1965
|
}
|
|
1946
1966
|
.dsh-files-flat-icon {
|
|
1947
1967
|
display: flex;
|
|
@@ -1965,22 +1985,21 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1965
1985
|
.dsh-files-flat-name {
|
|
1966
1986
|
font-size: 12px;
|
|
1967
1987
|
font-weight: 500;
|
|
1968
|
-
color: var(--dsw-alias-label-primary
|
|
1988
|
+
color: var(--dsw-alias-label-primary);
|
|
1969
1989
|
overflow: hidden;
|
|
1970
1990
|
text-overflow: ellipsis;
|
|
1971
1991
|
white-space: nowrap;
|
|
1972
1992
|
}
|
|
1973
1993
|
.dsh-files-flat-parent {
|
|
1974
1994
|
font-size: 10.5px;
|
|
1975
|
-
color: var(--dsw-alias-label-tertiary
|
|
1995
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1976
1996
|
overflow: hidden;
|
|
1977
1997
|
text-overflow: ellipsis;
|
|
1978
1998
|
white-space: nowrap;
|
|
1979
|
-
opacity: 0.8;
|
|
1980
1999
|
}
|
|
1981
2000
|
.dsh-files-jump-hint {
|
|
1982
2001
|
font-size: 10px;
|
|
1983
|
-
color: var(--dsw-alias-
|
|
2002
|
+
color: var(--dsw-alias-state-business-primary);
|
|
1984
2003
|
opacity: 0;
|
|
1985
2004
|
transition: opacity 0.12s ease;
|
|
1986
2005
|
margin-right: 6px;
|
|
@@ -1988,49 +2007,16 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
1988
2007
|
white-space: nowrap;
|
|
1989
2008
|
}
|
|
1990
2009
|
.dsh-files-flat-row:hover .dsh-files-jump-hint {
|
|
1991
|
-
opacity: 0.
|
|
2010
|
+
opacity: 0.9;
|
|
1992
2011
|
}
|
|
1993
2012
|
.dsh-files-flat-row.is-directory:hover {
|
|
1994
|
-
background: color-mix(in srgb, var(--dsw-alias-
|
|
1995
|
-
}
|
|
1996
|
-
.dsh-files-action-btn {
|
|
1997
|
-
display: inline-flex;
|
|
1998
|
-
align-items: center;
|
|
1999
|
-
justify-content: center;
|
|
2000
|
-
gap: 3px;
|
|
2001
|
-
height: 20px;
|
|
2002
|
-
padding: 0 5px;
|
|
2003
|
-
border: 1px solid var(--dsw-alias-border-l2, rgba(118,137,166,.25));
|
|
2004
|
-
border-radius: 3px;
|
|
2005
|
-
background: var(--dsw-alias-container-bg, var(--dsw-alias-bg-base, #ffffff));
|
|
2006
|
-
color: var(--dsw-alias-label-secondary, #6f7f9b);
|
|
2007
|
-
cursor: pointer;
|
|
2008
|
-
transition: all 0.12s ease;
|
|
2009
|
-
}
|
|
2010
|
-
.dsh-files-action-btn:hover {
|
|
2011
|
-
color: var(--dsw-alias-label-primary, inherit);
|
|
2012
|
-
border-color: var(--dsw-alias-border-l3, rgba(118,137,166,.4));
|
|
2013
|
-
background: var(--dsw-alias-interactive-bg-hover, rgba(100,120,150,.12));
|
|
2014
|
-
}
|
|
2015
|
-
.dsh-files-action-at {
|
|
2016
|
-
color: var(--dsw-alias-brand-primary, #4b70e2);
|
|
2017
|
-
border-color: color-mix(in srgb, var(--dsw-alias-brand-primary, #4b70e2) 30%, transparent);
|
|
2018
|
-
background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4b70e2) 8%, transparent);
|
|
2019
|
-
}
|
|
2020
|
-
.dsh-files-action-at:hover {
|
|
2021
|
-
color: #ffffff;
|
|
2022
|
-
background: var(--dsw-alias-brand-primary, #4b70e2);
|
|
2023
|
-
border-color: var(--dsw-alias-brand-primary, #4b70e2);
|
|
2024
|
-
}
|
|
2025
|
-
.dsh-files-action-at-text {
|
|
2026
|
-
font-size: 10px;
|
|
2027
|
-
font-weight: 600;
|
|
2013
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent);
|
|
2028
2014
|
}
|
|
2029
2015
|
|
|
2030
2016
|
/* ── Highlight & Toast ── */
|
|
2031
2017
|
.dsh-files-highlight {
|
|
2032
|
-
background: color-mix(in srgb, var(--dsw-alias-
|
|
2033
|
-
color: var(--dsw-alias-
|
|
2018
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 22%, transparent);
|
|
2019
|
+
color: var(--dsw-alias-state-business-primary);
|
|
2034
2020
|
font-weight: 600;
|
|
2035
2021
|
border-radius: 2px;
|
|
2036
2022
|
padding: 0 1px;
|
|
@@ -2044,12 +2030,12 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
2044
2030
|
align-items: center;
|
|
2045
2031
|
gap: 6px;
|
|
2046
2032
|
padding: 6px 12px;
|
|
2047
|
-
background: var(--dsw-alias-label-primary
|
|
2048
|
-
color: var(--dsw-alias-
|
|
2033
|
+
background: var(--dsw-alias-label-primary);
|
|
2034
|
+
color: var(--dsw-alias-fill-base, #ffffff);
|
|
2049
2035
|
font-size: 11px;
|
|
2050
2036
|
border-radius: 99px;
|
|
2051
|
-
box-shadow: 0 4px 16px rgba(0,0,0
|
|
2052
|
-
animation: dsh-files-fade-in 0.
|
|
2037
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.22);
|
|
2038
|
+
animation: dsh-files-fade-in 0.16s cubic-bezier(0.16, 1, 0.3, 1);
|
|
2053
2039
|
z-index: 100;
|
|
2054
2040
|
pointer-events: none;
|
|
2055
2041
|
white-space: nowrap;
|
|
@@ -2067,13 +2053,13 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
2067
2053
|
justify-content: center;
|
|
2068
2054
|
height: 180px;
|
|
2069
2055
|
gap: 10px;
|
|
2070
|
-
color: var(--dsw-alias-label-secondary
|
|
2056
|
+
color: var(--dsw-alias-label-secondary);
|
|
2071
2057
|
}
|
|
2072
2058
|
.dsh-files-spinner {
|
|
2073
2059
|
width: 18px;
|
|
2074
2060
|
height: 18px;
|
|
2075
|
-
border: 2px solid var(--dsw-alias-border-
|
|
2076
|
-
border-top-color: var(--dsw-alias-
|
|
2061
|
+
border: 2px solid var(--dsw-alias-border-l1);
|
|
2062
|
+
border-top-color: var(--dsw-alias-state-business-primary);
|
|
2077
2063
|
border-radius: 50%;
|
|
2078
2064
|
animation: dsh-files-spin 0.8s linear infinite;
|
|
2079
2065
|
}
|
|
@@ -2087,7 +2073,7 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
2087
2073
|
justify-content: center;
|
|
2088
2074
|
padding: 36px 16px;
|
|
2089
2075
|
text-align: center;
|
|
2090
|
-
color: var(--dsw-alias-label-secondary
|
|
2076
|
+
color: var(--dsw-alias-label-secondary);
|
|
2091
2077
|
}
|
|
2092
2078
|
.dsh-files-empty-icon {
|
|
2093
2079
|
margin-bottom: 8px;
|
|
@@ -2100,7 +2086,7 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
2100
2086
|
}
|
|
2101
2087
|
.dsh-files-empty-detail {
|
|
2102
2088
|
font-size: 11.5px;
|
|
2103
|
-
color: var(--dsw-alias-label-tertiary
|
|
2089
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2104
2090
|
}
|
|
2105
2091
|
.dsh-files-banner-error {
|
|
2106
2092
|
display: flex;
|
|
@@ -2109,9 +2095,9 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
2109
2095
|
gap: 8px;
|
|
2110
2096
|
padding: 6px 12px;
|
|
2111
2097
|
font-size: 11.5px;
|
|
2112
|
-
color: var(--dsw-alias-state-
|
|
2113
|
-
background: color-mix(in srgb, var(--dsw-alias-state-
|
|
2114
|
-
border-bottom:
|
|
2098
|
+
color: var(--dsw-alias-state-danger-primary, #e34c59);
|
|
2099
|
+
background: color-mix(in srgb, var(--dsw-alias-state-danger-primary, #e34c59) 10%, transparent);
|
|
2100
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l1);
|
|
2115
2101
|
}
|
|
2116
2102
|
.dsh-files-banner-close {
|
|
2117
2103
|
border: none;
|
|
@@ -2119,6 +2105,6 @@ function GlobalFilesStyle(): JSX.Element {
|
|
|
2119
2105
|
color: inherit;
|
|
2120
2106
|
cursor: pointer;
|
|
2121
2107
|
}
|
|
2122
|
-
`
|
|
2123
|
-
)
|
|
2108
|
+
`
|
|
2109
|
+
document.head.appendChild(style)
|
|
2124
2110
|
}
|