@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.
Files changed (101) hide show
  1. package/LICENSE +38 -38
  2. package/README.i18n.yaml +6 -6
  3. package/README.md +66 -66
  4. package/README.zh.md +66 -66
  5. package/cordis.patch.yml +13 -13
  6. package/lib/client.js +27354 -355
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +474 -42
  9. package/lib/tsconfig.client.tsbuildinfo +1 -1
  10. package/lib/tsconfig.host.tsbuildinfo +1 -1
  11. package/lib/types/client/file-source.d.ts +10 -0
  12. package/lib/types/client/file-source.d.ts.map +1 -1
  13. package/lib/types/client/fileType.d.ts +19 -4
  14. package/lib/types/client/fileType.d.ts.map +1 -1
  15. package/lib/types/client/index.d.ts +0 -1
  16. package/lib/types/client/index.d.ts.map +1 -1
  17. package/lib/types/client/locales.d.ts +3 -0
  18. package/lib/types/client/locales.d.ts.map +1 -1
  19. package/lib/types/client/mount.d.ts +1 -1
  20. package/lib/types/client/mount.d.ts.map +1 -1
  21. package/lib/types/client/picker/FilePickerModal.d.ts +8 -6
  22. package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -1
  23. package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
  24. package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
  25. package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
  26. package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
  27. package/lib/types/client/preview/content.d.ts +3 -1
  28. package/lib/types/client/preview/content.d.ts.map +1 -1
  29. package/lib/types/client/preview/office.d.ts +23 -0
  30. package/lib/types/client/preview/office.d.ts.map +1 -0
  31. package/lib/types/client/reference.d.ts +23 -10
  32. package/lib/types/client/reference.d.ts.map +1 -1
  33. package/lib/types/client/store.d.ts.map +1 -1
  34. package/lib/types/host/fs-service.d.ts.map +1 -1
  35. package/lib/types/host/office-preview.d.ts +13 -0
  36. package/lib/types/host/office-preview.d.ts.map +1 -0
  37. package/lib/types/host/routes.d.ts.map +1 -1
  38. package/lib/types/index.d.ts.map +1 -1
  39. package/package.json +19 -4
  40. package/src/client/DockItem.tsx +1 -1
  41. package/src/client/FileManagerSettingsCard.tsx +117 -117
  42. package/src/client/PluginSettingsCard.tsx +337 -337
  43. package/src/client/chat/file-ref.ts +144 -144
  44. package/src/client/chat/mermaid-chat.tsx +102 -102
  45. package/src/client/chat/placeholder-hint.tsx +46 -46
  46. package/src/client/components/ExplorerPanel.tsx +534 -534
  47. package/src/client/components/FileIcon.tsx +46 -46
  48. package/src/client/components/ScmPanel.tsx +480 -480
  49. package/src/client/components/a11y.ts +20 -20
  50. package/src/client/components/icons.tsx +274 -274
  51. package/src/client/components/overlay.tsx +225 -225
  52. package/src/client/drag/DragFileInlay.tsx +89 -89
  53. package/src/client/drag/file-drag.ts +67 -67
  54. package/src/client/drag.ts +154 -154
  55. package/src/client/file-source.ts +34 -0
  56. package/src/client/fileType.ts +32 -5
  57. package/src/client/hooks/useResizableSplit.ts +91 -91
  58. package/src/client/hooks/useStore.ts +15 -15
  59. package/src/client/index.ts +45 -17
  60. package/src/client/layout.ts +523 -523
  61. package/src/client/locales.ts +6 -0
  62. package/src/client/maximize.ts +34 -34
  63. package/src/client/mount.tsx +2 -1
  64. package/src/client/persist.ts +194 -194
  65. package/src/client/picker/FilePickerModal.tsx +526 -507
  66. package/src/client/picker/file-picker.ts +86 -86
  67. package/src/client/preview/PreviewPanel.tsx +8 -1
  68. package/src/client/preview/PreviewTabs.tsx +159 -159
  69. package/src/client/preview/PreviewToolbar.tsx +3 -2
  70. package/src/client/preview/content.tsx +7 -2
  71. package/src/client/preview/markdown.ts +351 -351
  72. package/src/client/preview/mermaid.ts +274 -274
  73. package/src/client/preview/office.tsx +321 -0
  74. package/src/client/reference.ts +62 -8
  75. package/src/client/settings-card.module.css +316 -316
  76. package/src/client/settings-form.ts +451 -451
  77. package/src/client/store.ts +17 -12
  78. package/src/client/styles/drag.module.css +30 -30
  79. package/src/client/styles/explorer.module.css +371 -371
  80. package/src/client/styles/picker.module.css +344 -344
  81. package/src/client/styles/preview.module.css +18 -11
  82. package/src/client/styles/scm.module.css +380 -380
  83. package/src/client/styles/tokens.module.css +376 -367
  84. package/src/core/types.ts +162 -162
  85. package/src/host/fs-service.ts +7 -1
  86. package/src/host/gate.ts +73 -73
  87. package/src/host/git-runner.ts +116 -116
  88. package/src/host/git-service.ts +395 -395
  89. package/src/host/loopback.ts +63 -63
  90. package/src/host/office-preview.ts +399 -0
  91. package/src/host/poll-guard.ts +109 -109
  92. package/src/host/routes.ts +72 -1
  93. package/src/index.ts +3 -1
  94. package/src/mount-once.ts +48 -48
  95. package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
  96. package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
  97. package/lib/types/client/floating.d.ts +0 -27
  98. package/lib/types/client/floating.d.ts.map +0 -1
  99. package/lib/types/client/mention.d.ts +0 -44
  100. package/lib/types/client/mention.d.ts.map +0 -1
  101. 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
+ }