@lijian-ui/dsh-file-manager 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +38 -38
- package/README.i18n.yaml +6 -6
- package/README.md +66 -66
- package/README.zh.md +66 -66
- package/cordis.patch.yml +13 -13
- package/lib/client.js +27354 -355
- package/lib/client.js.map +1 -1
- package/lib/index.js +474 -42
- package/lib/tsconfig.client.tsbuildinfo +1 -1
- package/lib/tsconfig.host.tsbuildinfo +1 -1
- package/lib/types/client/file-source.d.ts +10 -0
- package/lib/types/client/file-source.d.ts.map +1 -1
- package/lib/types/client/fileType.d.ts +19 -4
- package/lib/types/client/fileType.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +0 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +3 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/mount.d.ts +1 -1
- package/lib/types/client/mount.d.ts.map +1 -1
- package/lib/types/client/picker/FilePickerModal.d.ts +8 -6
- package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
- package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
- package/lib/types/client/preview/content.d.ts +3 -1
- package/lib/types/client/preview/content.d.ts.map +1 -1
- package/lib/types/client/preview/office.d.ts +23 -0
- package/lib/types/client/preview/office.d.ts.map +1 -0
- package/lib/types/client/reference.d.ts +23 -10
- package/lib/types/client/reference.d.ts.map +1 -1
- package/lib/types/client/store.d.ts.map +1 -1
- package/lib/types/host/fs-service.d.ts.map +1 -1
- package/lib/types/host/office-preview.d.ts +13 -0
- package/lib/types/host/office-preview.d.ts.map +1 -0
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +19 -4
- package/src/client/DockItem.tsx +1 -1
- package/src/client/FileManagerSettingsCard.tsx +117 -117
- package/src/client/PluginSettingsCard.tsx +337 -337
- package/src/client/chat/file-ref.ts +144 -144
- package/src/client/chat/mermaid-chat.tsx +102 -102
- package/src/client/chat/placeholder-hint.tsx +46 -46
- package/src/client/components/ExplorerPanel.tsx +534 -534
- package/src/client/components/FileIcon.tsx +46 -46
- package/src/client/components/ScmPanel.tsx +480 -480
- package/src/client/components/a11y.ts +20 -20
- package/src/client/components/icons.tsx +274 -274
- package/src/client/components/overlay.tsx +225 -225
- package/src/client/drag/DragFileInlay.tsx +89 -89
- package/src/client/drag/file-drag.ts +67 -67
- package/src/client/drag.ts +154 -154
- package/src/client/file-source.ts +34 -0
- package/src/client/fileType.ts +32 -5
- package/src/client/hooks/useResizableSplit.ts +91 -91
- package/src/client/hooks/useStore.ts +15 -15
- package/src/client/index.ts +45 -17
- package/src/client/layout.ts +523 -523
- package/src/client/locales.ts +6 -0
- package/src/client/maximize.ts +34 -34
- package/src/client/mount.tsx +2 -1
- package/src/client/persist.ts +194 -194
- package/src/client/picker/FilePickerModal.tsx +526 -507
- package/src/client/picker/file-picker.ts +86 -86
- package/src/client/preview/PreviewPanel.tsx +8 -1
- package/src/client/preview/PreviewTabs.tsx +159 -159
- package/src/client/preview/PreviewToolbar.tsx +3 -2
- package/src/client/preview/content.tsx +7 -2
- package/src/client/preview/markdown.ts +351 -351
- package/src/client/preview/mermaid.ts +274 -274
- package/src/client/preview/office.tsx +321 -0
- package/src/client/reference.ts +62 -8
- package/src/client/settings-card.module.css +316 -316
- package/src/client/settings-form.ts +451 -451
- package/src/client/store.ts +17 -12
- package/src/client/styles/drag.module.css +30 -30
- package/src/client/styles/explorer.module.css +371 -371
- package/src/client/styles/picker.module.css +344 -344
- package/src/client/styles/preview.module.css +18 -11
- package/src/client/styles/scm.module.css +380 -380
- package/src/client/styles/tokens.module.css +376 -367
- package/src/core/types.ts +162 -162
- package/src/host/fs-service.ts +7 -1
- package/src/host/gate.ts +73 -73
- package/src/host/git-runner.ts +116 -116
- package/src/host/git-service.ts +395 -395
- package/src/host/loopback.ts +63 -63
- package/src/host/office-preview.ts +399 -0
- package/src/host/poll-guard.ts +109 -109
- package/src/host/routes.ts +72 -1
- package/src/index.ts +3 -1
- package/src/mount-once.ts +48 -48
- package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
- package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
- package/lib/types/client/floating.d.ts +0 -27
- package/lib/types/client/floating.d.ts.map +0 -1
- package/lib/types/client/mention.d.ts +0 -44
- package/lib/types/client/mention.d.ts.map +0 -1
- package/src/client/styles/chip.module.css +0 -38
|
@@ -1,225 +1,225 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal overlay primitives for the panel: a toast and a context menu,
|
|
3
|
-
* rendered through plain DOM + portals so they can live outside the grid
|
|
4
|
-
* columns (fixed positioning, high z-index).
|
|
5
|
-
* @module dsh-filemgr/client/components/overlay
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
9
|
-
import { createPortal } from 'react-dom'
|
|
10
|
-
import type { JSX } from 'react'
|
|
11
|
-
import { t } from '../locales.ts'
|
|
12
|
-
import type { FileManagerPanelKey } from '../locales.ts'
|
|
13
|
-
|
|
14
|
-
/** One transient toast message. */
|
|
15
|
-
export function toast(message: string): void {
|
|
16
|
-
const el = document.createElement('div')
|
|
17
|
-
el.className = 'filemgr-toast'
|
|
18
|
-
el.textContent = message
|
|
19
|
-
document.body.appendChild(el)
|
|
20
|
-
setTimeout(() => {
|
|
21
|
-
el.style.opacity = '0'
|
|
22
|
-
el.style.transition = 'opacity 0.2s ease'
|
|
23
|
-
}, 1800)
|
|
24
|
-
setTimeout(() => el.remove(), 2100)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** One context-menu entry (separators carry no handler). */
|
|
28
|
-
export interface MenuEntry {
|
|
29
|
-
key: string
|
|
30
|
-
label: string
|
|
31
|
-
disabled?: boolean
|
|
32
|
-
danger?: boolean
|
|
33
|
-
onSelect?: () => void
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface MenuState {
|
|
37
|
-
x: number
|
|
38
|
-
y: number
|
|
39
|
-
entries: MenuEntry[]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** The shared context-menu portal host (one at a time). */
|
|
43
|
-
export function ContextMenu({ state, onClose }: { state: MenuState | null; onClose: () => void }): JSX.Element | null {
|
|
44
|
-
const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
|
|
45
|
-
|
|
46
|
-
useLayoutEffect(() => {
|
|
47
|
-
if (state === null) {
|
|
48
|
-
setPosition(null)
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
// Keep the menu inside the viewport.
|
|
52
|
-
const width = 180
|
|
53
|
-
const height = state.entries.length * 28 + 12
|
|
54
|
-
setPosition({
|
|
55
|
-
x: Math.min(state.x, window.innerWidth - width - 8),
|
|
56
|
-
y: Math.min(state.y, window.innerHeight - height - 8),
|
|
57
|
-
})
|
|
58
|
-
}, [state])
|
|
59
|
-
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
if (state === null) return
|
|
62
|
-
const close = (event: Event): void => {
|
|
63
|
-
// A pointerdown inside the menu must not close it — the menu item's own
|
|
64
|
-
// onClick still needs to run (and microtask-less onClose before onSelect
|
|
65
|
-
// would unmount the item mid-click). Only close on outside clicks.
|
|
66
|
-
if (event.target instanceof Element && event.target.closest('[data-menu-root]') !== null) return
|
|
67
|
-
onClose()
|
|
68
|
-
}
|
|
69
|
-
const key = (event: KeyboardEvent): void => {
|
|
70
|
-
if (event.key === 'Escape') onClose()
|
|
71
|
-
}
|
|
72
|
-
window.addEventListener('pointerdown', close, { capture: true })
|
|
73
|
-
window.addEventListener('blur', onClose)
|
|
74
|
-
window.addEventListener('keydown', key)
|
|
75
|
-
window.addEventListener('contextmenu', onClose)
|
|
76
|
-
return () => {
|
|
77
|
-
window.removeEventListener('pointerdown', close, { capture: true })
|
|
78
|
-
window.removeEventListener('blur', onClose)
|
|
79
|
-
window.removeEventListener('keydown', key)
|
|
80
|
-
window.removeEventListener('contextmenu', onClose)
|
|
81
|
-
}
|
|
82
|
-
}, [state, onClose])
|
|
83
|
-
|
|
84
|
-
if (state === null || position === null) return null
|
|
85
|
-
return createPortal(
|
|
86
|
-
<div
|
|
87
|
-
className="filemgr-menu"
|
|
88
|
-
data-menu-root=""
|
|
89
|
-
style={{ left: position.x, top: position.y }}
|
|
90
|
-
onPointerDown={(event) => event.stopPropagation()}
|
|
91
|
-
onContextMenu={(event) => event.preventDefault()}
|
|
92
|
-
>
|
|
93
|
-
{state.entries.map((entry) => (
|
|
94
|
-
<div key={entry.key}>
|
|
95
|
-
{entry.label === '---' ? (
|
|
96
|
-
<div className="filemgr-menu-sep" />
|
|
97
|
-
) : (
|
|
98
|
-
<div
|
|
99
|
-
className={`filemgr-menu-item${entry.disabled === true ? ' filemgr-menu-item-disabled' : ''}`}
|
|
100
|
-
onClick={() => {
|
|
101
|
-
if (entry.disabled === true) return
|
|
102
|
-
onClose()
|
|
103
|
-
entry.onSelect?.()
|
|
104
|
-
}}
|
|
105
|
-
role="menuitem"
|
|
106
|
-
>
|
|
107
|
-
{entry.label}
|
|
108
|
-
</div>
|
|
109
|
-
)}
|
|
110
|
-
</div>
|
|
111
|
-
))}
|
|
112
|
-
</div>,
|
|
113
|
-
document.body,
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** A single-text-input dialog (rename, new file / new folder). */
|
|
118
|
-
export function PromptDialog({
|
|
119
|
-
title,
|
|
120
|
-
initialValue,
|
|
121
|
-
confirmLabel,
|
|
122
|
-
onConfirm,
|
|
123
|
-
onCancel,
|
|
124
|
-
}: {
|
|
125
|
-
title: string
|
|
126
|
-
initialValue?: string
|
|
127
|
-
confirmLabel?: string
|
|
128
|
-
onConfirm: (value: string) => void
|
|
129
|
-
onCancel: () => void
|
|
130
|
-
}): JSX.Element {
|
|
131
|
-
const [value, setValue] = useState(initialValue ?? '')
|
|
132
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
133
|
-
useLayoutEffect(() => {
|
|
134
|
-
inputRef.current?.focus()
|
|
135
|
-
inputRef.current?.select()
|
|
136
|
-
}, [])
|
|
137
|
-
const commit = (): void => {
|
|
138
|
-
const trimmed = value.trim()
|
|
139
|
-
if (trimmed === '') return
|
|
140
|
-
onConfirm(trimmed)
|
|
141
|
-
}
|
|
142
|
-
useEffect(() => {
|
|
143
|
-
const key = (event: KeyboardEvent): void => {
|
|
144
|
-
if (event.key === 'Escape') onCancel()
|
|
145
|
-
}
|
|
146
|
-
window.addEventListener('keydown', key)
|
|
147
|
-
return () => window.removeEventListener('keydown', key)
|
|
148
|
-
}, [onCancel])
|
|
149
|
-
return createPortal(
|
|
150
|
-
<div className="filemgr-overlay" onPointerDown={onCancel}>
|
|
151
|
-
<div className="filemgr-dialog" onPointerDown={(event) => event.stopPropagation()}>
|
|
152
|
-
<div className="filemgr-dialog-title">{title}</div>
|
|
153
|
-
<div className="filemgr-dialog-body">
|
|
154
|
-
<input
|
|
155
|
-
ref={inputRef}
|
|
156
|
-
className="filemgr-input"
|
|
157
|
-
value={value}
|
|
158
|
-
placeholder={initialValue ?? ''}
|
|
159
|
-
aria-label={title}
|
|
160
|
-
onChange={(event) => setValue(event.target.value)}
|
|
161
|
-
onKeyDown={(event) => {
|
|
162
|
-
if (event.key === 'Enter') commit()
|
|
163
|
-
}}
|
|
164
|
-
/>
|
|
165
|
-
</div>
|
|
166
|
-
<div className="filemgr-dialog-actions">
|
|
167
|
-
<button type="button" className="filemgr-btn" onClick={onCancel}>{t('common.cancel')}</button>
|
|
168
|
-
<button
|
|
169
|
-
type="button"
|
|
170
|
-
className="filemgr-btn filemgr-btn-primary"
|
|
171
|
-
disabled={value.trim() === ''}
|
|
172
|
-
onClick={commit}
|
|
173
|
-
>
|
|
174
|
-
{confirmLabel ?? t('common.confirm')}
|
|
175
|
-
</button>
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
</div>,
|
|
179
|
-
document.body,
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** A confirmation dialog (dirty-close confirm, discard confirm). */
|
|
184
|
-
export function ConfirmDialog({
|
|
185
|
-
title,
|
|
186
|
-
body,
|
|
187
|
-
confirmLabel,
|
|
188
|
-
danger,
|
|
189
|
-
onConfirm,
|
|
190
|
-
onCancel,
|
|
191
|
-
}: {
|
|
192
|
-
title: string
|
|
193
|
-
body: string
|
|
194
|
-
confirmLabel?: string
|
|
195
|
-
danger?: boolean
|
|
196
|
-
onConfirm: () => void
|
|
197
|
-
onCancel: () => void
|
|
198
|
-
}): JSX.Element {
|
|
199
|
-
useEffect(() => {
|
|
200
|
-
const key = (event: KeyboardEvent): void => {
|
|
201
|
-
if (event.key === 'Escape') onCancel()
|
|
202
|
-
}
|
|
203
|
-
window.addEventListener('keydown', key)
|
|
204
|
-
return () => window.removeEventListener('keydown', key)
|
|
205
|
-
}, [onCancel])
|
|
206
|
-
return createPortal(
|
|
207
|
-
<div className="filemgr-overlay" onPointerDown={onCancel}>
|
|
208
|
-
<div className="filemgr-dialog" onPointerDown={(event) => event.stopPropagation()}>
|
|
209
|
-
<div className="filemgr-dialog-title">{title}</div>
|
|
210
|
-
<div className="filemgr-dialog-body">{body}</div>
|
|
211
|
-
<div className="filemgr-dialog-actions">
|
|
212
|
-
<button type="button" className="filemgr-btn" onClick={onCancel}>{t('common.cancel')}</button>
|
|
213
|
-
<button
|
|
214
|
-
type="button"
|
|
215
|
-
className={`filemgr-btn ${danger === true ? 'filemgr-btn-danger' : 'filemgr-btn-primary'}`}
|
|
216
|
-
onClick={onConfirm}
|
|
217
|
-
>
|
|
218
|
-
{confirmLabel ?? t('common.confirm')}
|
|
219
|
-
</button>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
</div>,
|
|
223
|
-
document.body,
|
|
224
|
-
)
|
|
225
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Minimal overlay primitives for the panel: a toast and a context menu,
|
|
3
|
+
* rendered through plain DOM + portals so they can live outside the grid
|
|
4
|
+
* columns (fixed positioning, high z-index).
|
|
5
|
+
* @module dsh-filemgr/client/components/overlay
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
9
|
+
import { createPortal } from 'react-dom'
|
|
10
|
+
import type { JSX } from 'react'
|
|
11
|
+
import { t } from '../locales.ts'
|
|
12
|
+
import type { FileManagerPanelKey } from '../locales.ts'
|
|
13
|
+
|
|
14
|
+
/** One transient toast message. */
|
|
15
|
+
export function toast(message: string): void {
|
|
16
|
+
const el = document.createElement('div')
|
|
17
|
+
el.className = 'filemgr-toast'
|
|
18
|
+
el.textContent = message
|
|
19
|
+
document.body.appendChild(el)
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
el.style.opacity = '0'
|
|
22
|
+
el.style.transition = 'opacity 0.2s ease'
|
|
23
|
+
}, 1800)
|
|
24
|
+
setTimeout(() => el.remove(), 2100)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One context-menu entry (separators carry no handler). */
|
|
28
|
+
export interface MenuEntry {
|
|
29
|
+
key: string
|
|
30
|
+
label: string
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
danger?: boolean
|
|
33
|
+
onSelect?: () => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MenuState {
|
|
37
|
+
x: number
|
|
38
|
+
y: number
|
|
39
|
+
entries: MenuEntry[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The shared context-menu portal host (one at a time). */
|
|
43
|
+
export function ContextMenu({ state, onClose }: { state: MenuState | null; onClose: () => void }): JSX.Element | null {
|
|
44
|
+
const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
|
|
45
|
+
|
|
46
|
+
useLayoutEffect(() => {
|
|
47
|
+
if (state === null) {
|
|
48
|
+
setPosition(null)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
// Keep the menu inside the viewport.
|
|
52
|
+
const width = 180
|
|
53
|
+
const height = state.entries.length * 28 + 12
|
|
54
|
+
setPosition({
|
|
55
|
+
x: Math.min(state.x, window.innerWidth - width - 8),
|
|
56
|
+
y: Math.min(state.y, window.innerHeight - height - 8),
|
|
57
|
+
})
|
|
58
|
+
}, [state])
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (state === null) return
|
|
62
|
+
const close = (event: Event): void => {
|
|
63
|
+
// A pointerdown inside the menu must not close it — the menu item's own
|
|
64
|
+
// onClick still needs to run (and microtask-less onClose before onSelect
|
|
65
|
+
// would unmount the item mid-click). Only close on outside clicks.
|
|
66
|
+
if (event.target instanceof Element && event.target.closest('[data-menu-root]') !== null) return
|
|
67
|
+
onClose()
|
|
68
|
+
}
|
|
69
|
+
const key = (event: KeyboardEvent): void => {
|
|
70
|
+
if (event.key === 'Escape') onClose()
|
|
71
|
+
}
|
|
72
|
+
window.addEventListener('pointerdown', close, { capture: true })
|
|
73
|
+
window.addEventListener('blur', onClose)
|
|
74
|
+
window.addEventListener('keydown', key)
|
|
75
|
+
window.addEventListener('contextmenu', onClose)
|
|
76
|
+
return () => {
|
|
77
|
+
window.removeEventListener('pointerdown', close, { capture: true })
|
|
78
|
+
window.removeEventListener('blur', onClose)
|
|
79
|
+
window.removeEventListener('keydown', key)
|
|
80
|
+
window.removeEventListener('contextmenu', onClose)
|
|
81
|
+
}
|
|
82
|
+
}, [state, onClose])
|
|
83
|
+
|
|
84
|
+
if (state === null || position === null) return null
|
|
85
|
+
return createPortal(
|
|
86
|
+
<div
|
|
87
|
+
className="filemgr-menu"
|
|
88
|
+
data-menu-root=""
|
|
89
|
+
style={{ left: position.x, top: position.y }}
|
|
90
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
91
|
+
onContextMenu={(event) => event.preventDefault()}
|
|
92
|
+
>
|
|
93
|
+
{state.entries.map((entry) => (
|
|
94
|
+
<div key={entry.key}>
|
|
95
|
+
{entry.label === '---' ? (
|
|
96
|
+
<div className="filemgr-menu-sep" />
|
|
97
|
+
) : (
|
|
98
|
+
<div
|
|
99
|
+
className={`filemgr-menu-item${entry.disabled === true ? ' filemgr-menu-item-disabled' : ''}`}
|
|
100
|
+
onClick={() => {
|
|
101
|
+
if (entry.disabled === true) return
|
|
102
|
+
onClose()
|
|
103
|
+
entry.onSelect?.()
|
|
104
|
+
}}
|
|
105
|
+
role="menuitem"
|
|
106
|
+
>
|
|
107
|
+
{entry.label}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>,
|
|
113
|
+
document.body,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A single-text-input dialog (rename, new file / new folder). */
|
|
118
|
+
export function PromptDialog({
|
|
119
|
+
title,
|
|
120
|
+
initialValue,
|
|
121
|
+
confirmLabel,
|
|
122
|
+
onConfirm,
|
|
123
|
+
onCancel,
|
|
124
|
+
}: {
|
|
125
|
+
title: string
|
|
126
|
+
initialValue?: string
|
|
127
|
+
confirmLabel?: string
|
|
128
|
+
onConfirm: (value: string) => void
|
|
129
|
+
onCancel: () => void
|
|
130
|
+
}): JSX.Element {
|
|
131
|
+
const [value, setValue] = useState(initialValue ?? '')
|
|
132
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
133
|
+
useLayoutEffect(() => {
|
|
134
|
+
inputRef.current?.focus()
|
|
135
|
+
inputRef.current?.select()
|
|
136
|
+
}, [])
|
|
137
|
+
const commit = (): void => {
|
|
138
|
+
const trimmed = value.trim()
|
|
139
|
+
if (trimmed === '') return
|
|
140
|
+
onConfirm(trimmed)
|
|
141
|
+
}
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const key = (event: KeyboardEvent): void => {
|
|
144
|
+
if (event.key === 'Escape') onCancel()
|
|
145
|
+
}
|
|
146
|
+
window.addEventListener('keydown', key)
|
|
147
|
+
return () => window.removeEventListener('keydown', key)
|
|
148
|
+
}, [onCancel])
|
|
149
|
+
return createPortal(
|
|
150
|
+
<div className="filemgr-overlay" onPointerDown={onCancel}>
|
|
151
|
+
<div className="filemgr-dialog" onPointerDown={(event) => event.stopPropagation()}>
|
|
152
|
+
<div className="filemgr-dialog-title">{title}</div>
|
|
153
|
+
<div className="filemgr-dialog-body">
|
|
154
|
+
<input
|
|
155
|
+
ref={inputRef}
|
|
156
|
+
className="filemgr-input"
|
|
157
|
+
value={value}
|
|
158
|
+
placeholder={initialValue ?? ''}
|
|
159
|
+
aria-label={title}
|
|
160
|
+
onChange={(event) => setValue(event.target.value)}
|
|
161
|
+
onKeyDown={(event) => {
|
|
162
|
+
if (event.key === 'Enter') commit()
|
|
163
|
+
}}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
<div className="filemgr-dialog-actions">
|
|
167
|
+
<button type="button" className="filemgr-btn" onClick={onCancel}>{t('common.cancel')}</button>
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
className="filemgr-btn filemgr-btn-primary"
|
|
171
|
+
disabled={value.trim() === ''}
|
|
172
|
+
onClick={commit}
|
|
173
|
+
>
|
|
174
|
+
{confirmLabel ?? t('common.confirm')}
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>,
|
|
179
|
+
document.body,
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A confirmation dialog (dirty-close confirm, discard confirm). */
|
|
184
|
+
export function ConfirmDialog({
|
|
185
|
+
title,
|
|
186
|
+
body,
|
|
187
|
+
confirmLabel,
|
|
188
|
+
danger,
|
|
189
|
+
onConfirm,
|
|
190
|
+
onCancel,
|
|
191
|
+
}: {
|
|
192
|
+
title: string
|
|
193
|
+
body: string
|
|
194
|
+
confirmLabel?: string
|
|
195
|
+
danger?: boolean
|
|
196
|
+
onConfirm: () => void
|
|
197
|
+
onCancel: () => void
|
|
198
|
+
}): JSX.Element {
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
const key = (event: KeyboardEvent): void => {
|
|
201
|
+
if (event.key === 'Escape') onCancel()
|
|
202
|
+
}
|
|
203
|
+
window.addEventListener('keydown', key)
|
|
204
|
+
return () => window.removeEventListener('keydown', key)
|
|
205
|
+
}, [onCancel])
|
|
206
|
+
return createPortal(
|
|
207
|
+
<div className="filemgr-overlay" onPointerDown={onCancel}>
|
|
208
|
+
<div className="filemgr-dialog" onPointerDown={(event) => event.stopPropagation()}>
|
|
209
|
+
<div className="filemgr-dialog-title">{title}</div>
|
|
210
|
+
<div className="filemgr-dialog-body">{body}</div>
|
|
211
|
+
<div className="filemgr-dialog-actions">
|
|
212
|
+
<button type="button" className="filemgr-btn" onClick={onCancel}>{t('common.cancel')}</button>
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
className={`filemgr-btn ${danger === true ? 'filemgr-btn-danger' : 'filemgr-btn-primary'}`}
|
|
216
|
+
onClick={onConfirm}
|
|
217
|
+
>
|
|
218
|
+
{confirmLabel ?? t('common.confirm')}
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>,
|
|
223
|
+
document.body,
|
|
224
|
+
)
|
|
225
|
+
}
|
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Composer dock inlay: the drop target for explorer file drags. It mounts
|
|
3
|
-
* in the official `conversation.input.dock` band (a session-scoped list
|
|
4
|
-
* slot declared by the shipped ui-conversation rc.6 shell), so it stacks
|
|
5
|
-
* with the git-graph chip above the composer card. While a file row is
|
|
6
|
-
* dragged over the page it shows a hint strip; on drop it splices the
|
|
7
|
-
* workspace-relative path into the active session's draft through the
|
|
8
|
-
* conversation input facade.
|
|
9
|
-
*
|
|
10
|
-
* The document-level listeners only claim drags carrying our custom MIME —
|
|
11
|
-
* the composer host's own drop handling (OS image files) is untouched. The
|
|
12
|
-
* host's `dragover` refuses every drop it does not claim, so this inlay
|
|
13
|
-
* must `preventDefault` its own drags to make the drop land.
|
|
14
|
-
* @module dsh-filemgr/client/drag/DragFileInlay
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { useEffect, useRef, useState, type ReactElement } from 'react'
|
|
18
|
-
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
19
|
-
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
20
|
-
import { FILE_DRAG_MIME, hasFileDrag, isValidFileDragPayload } from './file-drag.ts'
|
|
21
|
-
import { t } from '../locales.ts'
|
|
22
|
-
import dragCss from '../styles/drag.module.css'
|
|
23
|
-
|
|
24
|
-
/** Injected business face of the drag inlay (session-routed). */
|
|
25
|
-
export interface DragFileInjected {
|
|
26
|
-
/** Splice a workspace-relative path into the active session's draft. */
|
|
27
|
-
insertPath: (path: string) => boolean
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Composed props: the dock's runtime share (sessionId) + the injected verb. */
|
|
31
|
-
export type DragFileInlayProps =
|
|
32
|
-
PropsRuntime<'conversation.input.dock'>
|
|
33
|
-
& DragFileInjected
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The composer dock entry: a zero-height anchor that shows a hint strip
|
|
37
|
-
* while a file row is dragged over the page and inserts the path on drop.
|
|
38
|
-
* @param props - the composed dock entry props.
|
|
39
|
-
*/
|
|
40
|
-
export function DragFileInlay(props: DragFileInlayProps): ReactElement {
|
|
41
|
-
const [active, setActive] = useState(false)
|
|
42
|
-
const depth = useRef(0)
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
const reset = (): void => {
|
|
46
|
-
depth.current = 0
|
|
47
|
-
setActive(false)
|
|
48
|
-
}
|
|
49
|
-
const onDragOver = (event: DragEvent): void => {
|
|
50
|
-
if (!hasFileDrag(event.dataTransfer?.types)) return
|
|
51
|
-
event.preventDefault()
|
|
52
|
-
depth.current += 1
|
|
53
|
-
setActive(true)
|
|
54
|
-
}
|
|
55
|
-
const onDragLeave = (event: DragEvent): void => {
|
|
56
|
-
if (!hasFileDrag(event.dataTransfer?.types)) return
|
|
57
|
-
depth.current = Math.max(0, depth.current - 1)
|
|
58
|
-
if (depth.current === 0) setActive(false)
|
|
59
|
-
}
|
|
60
|
-
const onDrop = (event: DragEvent): void => {
|
|
61
|
-
if (!hasFileDrag(event.dataTransfer?.types)) return
|
|
62
|
-
event.preventDefault()
|
|
63
|
-
const path = event.dataTransfer?.getData(FILE_DRAG_MIME) ?? ''
|
|
64
|
-
reset()
|
|
65
|
-
if (isValidFileDragPayload(path)) props.insertPath(path)
|
|
66
|
-
}
|
|
67
|
-
const onDragEnd = (): void => reset()
|
|
68
|
-
document.addEventListener('dragover', onDragOver)
|
|
69
|
-
document.addEventListener('dragleave', onDragLeave)
|
|
70
|
-
document.addEventListener('drop', onDrop)
|
|
71
|
-
window.addEventListener('dragend', onDragEnd)
|
|
72
|
-
return () => {
|
|
73
|
-
document.removeEventListener('dragover', onDragOver)
|
|
74
|
-
document.removeEventListener('dragleave', onDragLeave)
|
|
75
|
-
document.removeEventListener('drop', onDrop)
|
|
76
|
-
window.removeEventListener('dragend', onDragEnd)
|
|
77
|
-
}
|
|
78
|
-
}, [props.insertPath])
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<div
|
|
82
|
-
className={active ? `${dragCss.strip} ${dragCss.stripActive}` : dragCss.strip}
|
|
83
|
-
data-testid="filemgr-drag-inlay"
|
|
84
|
-
aria-live="polite"
|
|
85
|
-
>
|
|
86
|
-
{active ? <span className={dragCss.stripText}>{t('explorer.drag.dropHint')}</span> : null}
|
|
87
|
-
</div>
|
|
88
|
-
)
|
|
89
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Composer dock inlay: the drop target for explorer file drags. It mounts
|
|
3
|
+
* in the official `conversation.input.dock` band (a session-scoped list
|
|
4
|
+
* slot declared by the shipped ui-conversation rc.6 shell), so it stacks
|
|
5
|
+
* with the git-graph chip above the composer card. While a file row is
|
|
6
|
+
* dragged over the page it shows a hint strip; on drop it splices the
|
|
7
|
+
* workspace-relative path into the active session's draft through the
|
|
8
|
+
* conversation input facade.
|
|
9
|
+
*
|
|
10
|
+
* The document-level listeners only claim drags carrying our custom MIME —
|
|
11
|
+
* the composer host's own drop handling (OS image files) is untouched. The
|
|
12
|
+
* host's `dragover` refuses every drop it does not claim, so this inlay
|
|
13
|
+
* must `preventDefault` its own drags to make the drop land.
|
|
14
|
+
* @module dsh-filemgr/client/drag/DragFileInlay
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useEffect, useRef, useState, type ReactElement } from 'react'
|
|
18
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
20
|
+
import { FILE_DRAG_MIME, hasFileDrag, isValidFileDragPayload } from './file-drag.ts'
|
|
21
|
+
import { t } from '../locales.ts'
|
|
22
|
+
import dragCss from '../styles/drag.module.css'
|
|
23
|
+
|
|
24
|
+
/** Injected business face of the drag inlay (session-routed). */
|
|
25
|
+
export interface DragFileInjected {
|
|
26
|
+
/** Splice a workspace-relative path into the active session's draft. */
|
|
27
|
+
insertPath: (path: string) => boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Composed props: the dock's runtime share (sessionId) + the injected verb. */
|
|
31
|
+
export type DragFileInlayProps =
|
|
32
|
+
PropsRuntime<'conversation.input.dock'>
|
|
33
|
+
& DragFileInjected
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The composer dock entry: a zero-height anchor that shows a hint strip
|
|
37
|
+
* while a file row is dragged over the page and inserts the path on drop.
|
|
38
|
+
* @param props - the composed dock entry props.
|
|
39
|
+
*/
|
|
40
|
+
export function DragFileInlay(props: DragFileInlayProps): ReactElement {
|
|
41
|
+
const [active, setActive] = useState(false)
|
|
42
|
+
const depth = useRef(0)
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const reset = (): void => {
|
|
46
|
+
depth.current = 0
|
|
47
|
+
setActive(false)
|
|
48
|
+
}
|
|
49
|
+
const onDragOver = (event: DragEvent): void => {
|
|
50
|
+
if (!hasFileDrag(event.dataTransfer?.types)) return
|
|
51
|
+
event.preventDefault()
|
|
52
|
+
depth.current += 1
|
|
53
|
+
setActive(true)
|
|
54
|
+
}
|
|
55
|
+
const onDragLeave = (event: DragEvent): void => {
|
|
56
|
+
if (!hasFileDrag(event.dataTransfer?.types)) return
|
|
57
|
+
depth.current = Math.max(0, depth.current - 1)
|
|
58
|
+
if (depth.current === 0) setActive(false)
|
|
59
|
+
}
|
|
60
|
+
const onDrop = (event: DragEvent): void => {
|
|
61
|
+
if (!hasFileDrag(event.dataTransfer?.types)) return
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
const path = event.dataTransfer?.getData(FILE_DRAG_MIME) ?? ''
|
|
64
|
+
reset()
|
|
65
|
+
if (isValidFileDragPayload(path)) props.insertPath(path)
|
|
66
|
+
}
|
|
67
|
+
const onDragEnd = (): void => reset()
|
|
68
|
+
document.addEventListener('dragover', onDragOver)
|
|
69
|
+
document.addEventListener('dragleave', onDragLeave)
|
|
70
|
+
document.addEventListener('drop', onDrop)
|
|
71
|
+
window.addEventListener('dragend', onDragEnd)
|
|
72
|
+
return () => {
|
|
73
|
+
document.removeEventListener('dragover', onDragOver)
|
|
74
|
+
document.removeEventListener('dragleave', onDragLeave)
|
|
75
|
+
document.removeEventListener('drop', onDrop)
|
|
76
|
+
window.removeEventListener('dragend', onDragEnd)
|
|
77
|
+
}
|
|
78
|
+
}, [props.insertPath])
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
className={active ? `${dragCss.strip} ${dragCss.stripActive}` : dragCss.strip}
|
|
83
|
+
data-testid="filemgr-drag-inlay"
|
|
84
|
+
aria-live="polite"
|
|
85
|
+
>
|
|
86
|
+
{active ? <span className={dragCss.stripText}>{t('explorer.drag.dropHint')}</span> : null}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|