@lijian-ui/dsh-file-manager 0.1.20

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 (169) hide show
  1. package/LICENSE +38 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +66 -0
  4. package/README.zh.md +66 -0
  5. package/cordis.patch.yml +13 -0
  6. package/lib/assets/mermaid.min.js +3587 -0
  7. package/lib/client.js +7621 -0
  8. package/lib/client.js.map +1 -0
  9. package/lib/index.js +3207 -0
  10. package/lib/tsconfig.client.tsbuildinfo +1 -0
  11. package/lib/tsconfig.host.tsbuildinfo +1 -0
  12. package/lib/types/client/FileManagerSettingsCard.d.ts +56 -0
  13. package/lib/types/client/FileManagerSettingsCard.d.ts.map +1 -0
  14. package/lib/types/client/PluginSettingsCard.d.ts +104 -0
  15. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
  16. package/lib/types/client/api.d.ts +75 -0
  17. package/lib/types/client/api.d.ts.map +1 -0
  18. package/lib/types/client/chat/AttachedFilesDock.d.ts +51 -0
  19. package/lib/types/client/chat/AttachedFilesDock.d.ts.map +1 -0
  20. package/lib/types/client/chat/file-ref.d.ts +35 -0
  21. package/lib/types/client/chat/file-ref.d.ts.map +1 -0
  22. package/lib/types/client/chat/mermaid-chat.d.ts +30 -0
  23. package/lib/types/client/chat/mermaid-chat.d.ts.map +1 -0
  24. package/lib/types/client/chat/placeholder-hint.d.ts +16 -0
  25. package/lib/types/client/chat/placeholder-hint.d.ts.map +1 -0
  26. package/lib/types/client/components/ExplorerPanel.d.ts +26 -0
  27. package/lib/types/client/components/ExplorerPanel.d.ts.map +1 -0
  28. package/lib/types/client/components/FileIcon.d.ts +15 -0
  29. package/lib/types/client/components/FileIcon.d.ts.map +1 -0
  30. package/lib/types/client/components/ScmPanel.d.ts +23 -0
  31. package/lib/types/client/components/ScmPanel.d.ts.map +1 -0
  32. package/lib/types/client/components/a11y.d.ts +12 -0
  33. package/lib/types/client/components/a11y.d.ts.map +1 -0
  34. package/lib/types/client/components/icons.d.ts +41 -0
  35. package/lib/types/client/components/icons.d.ts.map +1 -0
  36. package/lib/types/client/components/overlay.d.ts +45 -0
  37. package/lib/types/client/components/overlay.d.ts.map +1 -0
  38. package/lib/types/client/drag/DragFileInlay.d.ts +31 -0
  39. package/lib/types/client/drag/DragFileInlay.d.ts.map +1 -0
  40. package/lib/types/client/drag/file-drag.d.ts +47 -0
  41. package/lib/types/client/drag/file-drag.d.ts.map +1 -0
  42. package/lib/types/client/drag.d.ts +31 -0
  43. package/lib/types/client/drag.d.ts.map +1 -0
  44. package/lib/types/client/file-source.d.ts +29 -0
  45. package/lib/types/client/file-source.d.ts.map +1 -0
  46. package/lib/types/client/fileType.d.ts +31 -0
  47. package/lib/types/client/fileType.d.ts.map +1 -0
  48. package/lib/types/client/floating.d.ts +27 -0
  49. package/lib/types/client/floating.d.ts.map +1 -0
  50. package/lib/types/client/hooks/useResizableSplit.d.ts +37 -0
  51. package/lib/types/client/hooks/useResizableSplit.d.ts.map +1 -0
  52. package/lib/types/client/hooks/useStore.d.ts +10 -0
  53. package/lib/types/client/hooks/useStore.d.ts.map +1 -0
  54. package/lib/types/client/index.d.ts +57 -0
  55. package/lib/types/client/index.d.ts.map +1 -0
  56. package/lib/types/client/layout.d.ts +104 -0
  57. package/lib/types/client/layout.d.ts.map +1 -0
  58. package/lib/types/client/locales.d.ts +125 -0
  59. package/lib/types/client/locales.d.ts.map +1 -0
  60. package/lib/types/client/maximize.d.ts +24 -0
  61. package/lib/types/client/maximize.d.ts.map +1 -0
  62. package/lib/types/client/mention.d.ts +44 -0
  63. package/lib/types/client/mention.d.ts.map +1 -0
  64. package/lib/types/client/mount.d.ts +22 -0
  65. package/lib/types/client/mount.d.ts.map +1 -0
  66. package/lib/types/client/persist.d.ts +55 -0
  67. package/lib/types/client/persist.d.ts.map +1 -0
  68. package/lib/types/client/picker/FilePickerModal.d.ts +47 -0
  69. package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -0
  70. package/lib/types/client/picker/file-picker.d.ts +62 -0
  71. package/lib/types/client/picker/file-picker.d.ts.map +1 -0
  72. package/lib/types/client/preview/PreviewPanel.d.ts +17 -0
  73. package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -0
  74. package/lib/types/client/preview/PreviewTabs.d.ts +30 -0
  75. package/lib/types/client/preview/PreviewTabs.d.ts.map +1 -0
  76. package/lib/types/client/preview/PreviewToolbar.d.ts +37 -0
  77. package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -0
  78. package/lib/types/client/preview/content.d.ts +43 -0
  79. package/lib/types/client/preview/content.d.ts.map +1 -0
  80. package/lib/types/client/preview/markdown.d.ts +60 -0
  81. package/lib/types/client/preview/markdown.d.ts.map +1 -0
  82. package/lib/types/client/preview/mermaid.d.ts +78 -0
  83. package/lib/types/client/preview/mermaid.d.ts.map +1 -0
  84. package/lib/types/client/preview/selection.d.ts +26 -0
  85. package/lib/types/client/preview/selection.d.ts.map +1 -0
  86. package/lib/types/client/reference.d.ts +75 -0
  87. package/lib/types/client/reference.d.ts.map +1 -0
  88. package/lib/types/client/settings-form.d.ts +206 -0
  89. package/lib/types/client/settings-form.d.ts.map +1 -0
  90. package/lib/types/client/store.d.ts +279 -0
  91. package/lib/types/client/store.d.ts.map +1 -0
  92. package/lib/types/core/types.d.ts +132 -0
  93. package/lib/types/core/types.d.ts.map +1 -0
  94. package/lib/types/host/fs-service.d.ts +96 -0
  95. package/lib/types/host/fs-service.d.ts.map +1 -0
  96. package/lib/types/host/gate.d.ts +45 -0
  97. package/lib/types/host/gate.d.ts.map +1 -0
  98. package/lib/types/host/git-runner.d.ts +80 -0
  99. package/lib/types/host/git-runner.d.ts.map +1 -0
  100. package/lib/types/host/git-service.d.ts +130 -0
  101. package/lib/types/host/git-service.d.ts.map +1 -0
  102. package/lib/types/host/loopback.d.ts +25 -0
  103. package/lib/types/host/loopback.d.ts.map +1 -0
  104. package/lib/types/host/poll-guard.d.ts +55 -0
  105. package/lib/types/host/poll-guard.d.ts.map +1 -0
  106. package/lib/types/host/routes.d.ts +54 -0
  107. package/lib/types/host/routes.d.ts.map +1 -0
  108. package/lib/types/index.d.ts +35 -0
  109. package/lib/types/index.d.ts.map +1 -0
  110. package/lib/types/mount-once.d.ts +25 -0
  111. package/lib/types/mount-once.d.ts.map +1 -0
  112. package/package.json +85 -0
  113. package/src/client/FileManagerSettingsCard.tsx +117 -0
  114. package/src/client/PluginSettingsCard.tsx +337 -0
  115. package/src/client/api.ts +141 -0
  116. package/src/client/chat/file-ref.ts +144 -0
  117. package/src/client/chat/mermaid-chat.tsx +102 -0
  118. package/src/client/chat/placeholder-hint.tsx +46 -0
  119. package/src/client/components/ExplorerPanel.tsx +534 -0
  120. package/src/client/components/FileIcon.tsx +46 -0
  121. package/src/client/components/ScmPanel.tsx +480 -0
  122. package/src/client/components/a11y.ts +20 -0
  123. package/src/client/components/icons.tsx +274 -0
  124. package/src/client/components/overlay.tsx +225 -0
  125. package/src/client/css-modules.d.ts +5 -0
  126. package/src/client/drag/DragFileInlay.tsx +89 -0
  127. package/src/client/drag/file-drag.ts +67 -0
  128. package/src/client/drag.ts +154 -0
  129. package/src/client/file-source.ts +58 -0
  130. package/src/client/fileType.ts +129 -0
  131. package/src/client/floating.ts +63 -0
  132. package/src/client/hooks/useResizableSplit.ts +91 -0
  133. package/src/client/hooks/useStore.ts +15 -0
  134. package/src/client/index.ts +389 -0
  135. package/src/client/layout.ts +546 -0
  136. package/src/client/locales.ts +247 -0
  137. package/src/client/maximize.ts +34 -0
  138. package/src/client/mount.tsx +74 -0
  139. package/src/client/persist.ts +194 -0
  140. package/src/client/picker/FilePickerModal.tsx +506 -0
  141. package/src/client/picker/file-picker.ts +86 -0
  142. package/src/client/preview/PreviewPanel.tsx +233 -0
  143. package/src/client/preview/PreviewTabs.tsx +159 -0
  144. package/src/client/preview/PreviewToolbar.tsx +160 -0
  145. package/src/client/preview/content.tsx +617 -0
  146. package/src/client/preview/markdown.ts +351 -0
  147. package/src/client/preview/mermaid.ts +274 -0
  148. package/src/client/preview/selection.ts +82 -0
  149. package/src/client/reference.ts +161 -0
  150. package/src/client/settings-card.module.css +316 -0
  151. package/src/client/settings-form.ts +451 -0
  152. package/src/client/store.ts +1290 -0
  153. package/src/client/styles/chip.module.css +38 -0
  154. package/src/client/styles/drag.module.css +30 -0
  155. package/src/client/styles/explorer.module.css +371 -0
  156. package/src/client/styles/picker.module.css +344 -0
  157. package/src/client/styles/preview.module.css +904 -0
  158. package/src/client/styles/scm.module.css +380 -0
  159. package/src/client/styles/tokens.module.css +480 -0
  160. package/src/core/types.ts +162 -0
  161. package/src/host/fs-service.ts +513 -0
  162. package/src/host/gate.ts +74 -0
  163. package/src/host/git-runner.ts +116 -0
  164. package/src/host/git-service.ts +395 -0
  165. package/src/host/loopback.ts +63 -0
  166. package/src/host/poll-guard.ts +109 -0
  167. package/src/host/routes.ts +695 -0
  168. package/src/index.ts +97 -0
  169. package/src/mount-once.ts +48 -0
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Inline 16px icon set for the panel surfaces (outline style, currentColor).
3
+ * No icon package is introduced; the set covers the explorer tree, SCM rows,
4
+ * and the preview toolbar/tabs with plain geometric shapes.
5
+ * @module dsh-filemgr/client/components/icons
6
+ */
7
+
8
+ import type { JSX, SVGProps } from 'react'
9
+
10
+ interface IconProps {
11
+ size?: number
12
+ className?: string
13
+ }
14
+
15
+ const base = (size: number): SVGProps<SVGSVGElement> => ({
16
+ width: size,
17
+ height: size,
18
+ viewBox: '0 0 16 16',
19
+ fill: 'none',
20
+ stroke: 'currentColor',
21
+ strokeWidth: 1.3,
22
+ strokeLinecap: 'round',
23
+ strokeLinejoin: 'round',
24
+ 'aria-hidden': true,
25
+ })
26
+
27
+ export function FolderIcon({ size = 16, className }: IconProps): JSX.Element {
28
+ return (
29
+ <svg {...base(size)} className={className}>
30
+ <path d="M2 3.5h4l1.5 2H14a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1Z" />
31
+ </svg>
32
+ )
33
+ }
34
+
35
+ export function FolderOpenIcon({ size = 16, className }: IconProps): JSX.Element {
36
+ return (
37
+ <svg {...base(size)} className={className}>
38
+ <path d="M2 3.5h4l1.5 2H14a1 1 0 0 1 1 1v1H3.5a1 1 0 0 0-.96.72L1 13.5V4.5a1 1 0 0 1 1-1Z" />
39
+ <path d="M2.8 11.5 4 7.5h11l-1.4 4a1 1 0 0 1-.96.72H3.76a1 1 0 0 1-.96-.72Z" />
40
+ </svg>
41
+ )
42
+ }
43
+
44
+ export function FileIcon({ size = 16, className }: IconProps): JSX.Element {
45
+ return (
46
+ <svg {...base(size)} className={className}>
47
+ <path d="M4 2h5l3 3v9H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Z" />
48
+ <path d="M9 2v3h3" />
49
+ </svg>
50
+ )
51
+ }
52
+
53
+ export function FileCodeIcon({ size = 16, className }: IconProps): JSX.Element {
54
+ return (
55
+ <svg {...base(size)} className={className}>
56
+ <path d="M4 2h5l3 3v9H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Z" />
57
+ <path d="M9 2v3h3" />
58
+ <path d="m6.2 8.6-1.4 1.4 1.4 1.4M9.8 8.6l1.4 1.4-1.4 1.4" />
59
+ </svg>
60
+ )
61
+ }
62
+
63
+ export function FileImageIcon({ size = 16, className }: IconProps): JSX.Element {
64
+ return (
65
+ <svg {...base(size)} className={className}>
66
+ <path d="M4 2h5l3 3v9H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Z" />
67
+ <path d="M9 2v3h3" />
68
+ <circle cx="6.2" cy="6.8" r="1" />
69
+ <path d="m5 11 1.8-1.8 1.4 1.4 1.3-1.3L12 12" />
70
+ </svg>
71
+ )
72
+ }
73
+
74
+ export function FileTextIcon({ size = 16, className }: IconProps): JSX.Element {
75
+ return (
76
+ <svg {...base(size)} className={className}>
77
+ <path d="M4 2h5l3 3v9H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Z" />
78
+ <path d="M9 2v3h3" />
79
+ <path d="M6 9h4M6 11.2h4" />
80
+ </svg>
81
+ )
82
+ }
83
+
84
+ export function ChevronRightIcon({ size = 14, className }: IconProps): JSX.Element {
85
+ return (
86
+ <svg {...base(size)} className={className}>
87
+ <path d="m6 3.5 4.5 4.5L6 12.5" />
88
+ </svg>
89
+ )
90
+ }
91
+
92
+ export function ChevronDownIcon({ size = 14, className }: IconProps): JSX.Element {
93
+ return (
94
+ <svg {...base(size)} className={className}>
95
+ <path d="m3.5 6 4.5 4.5L12.5 6" />
96
+ </svg>
97
+ )
98
+ }
99
+
100
+ export function CloseIcon({ size = 12, className }: IconProps): JSX.Element {
101
+ return (
102
+ <svg {...base(size)} className={className}>
103
+ <path d="m3.5 3.5 9 9M12.5 3.5l-9 9" />
104
+ </svg>
105
+ )
106
+ }
107
+
108
+ export function PlusIcon({ size = 14, className }: IconProps): JSX.Element {
109
+ return (
110
+ <svg {...base(size)} className={className}>
111
+ <path d="M8 3v10M3 8h10" />
112
+ </svg>
113
+ )
114
+ }
115
+
116
+ export function MinusIcon({ size = 14, className }: IconProps): JSX.Element {
117
+ return (
118
+ <svg {...base(size)} className={className}>
119
+ <path d="M3 8h10" />
120
+ </svg>
121
+ )
122
+ }
123
+
124
+ export function UndoIcon({ size = 14, className }: IconProps): JSX.Element {
125
+ return (
126
+ <svg {...base(size)} className={className}>
127
+ <path d="M5 3.5 2.5 6 5 8.5" />
128
+ <path d="M2.5 6h8a3.5 3.5 0 0 1 0 7h-3" />
129
+ </svg>
130
+ )
131
+ }
132
+
133
+ export function RefreshIcon({ size = 14, className }: IconProps): JSX.Element {
134
+ return (
135
+ <svg {...base(size)} className={className}>
136
+ <path d="M13 8a5 5 0 1 1-1.6-3.65" />
137
+ <path d="M13.5 2v2.8h-2.8" />
138
+ </svg>
139
+ )
140
+ }
141
+
142
+ export function SplitIcon({ size = 14, className }: IconProps): JSX.Element {
143
+ return (
144
+ <svg {...base(size)} className={className}>
145
+ <rect x="2" y="2.5" width="12" height="11" rx="1" />
146
+ <path d="M8 2.5v11" />
147
+ </svg>
148
+ )
149
+ }
150
+
151
+ export function CodeIcon({ size = 14, className }: IconProps): JSX.Element {
152
+ return (
153
+ <svg {...base(size)} className={className}>
154
+ <path d="m5.5 4.5-4 3.5 4 3.5M10.5 4.5l4 3.5-4 3.5" />
155
+ </svg>
156
+ )
157
+ }
158
+
159
+ export function EyeIcon({ size = 14, className }: IconProps): JSX.Element {
160
+ return (
161
+ <svg {...base(size)} className={className}>
162
+ <path d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8 12 12.5 8 12.5 1.5 8 1.5 8Z" />
163
+ <circle cx="8" cy="8" r="2" />
164
+ </svg>
165
+ )
166
+ }
167
+
168
+ export function SaveIcon({ size = 14, className }: IconProps): JSX.Element {
169
+ return (
170
+ <svg {...base(size)} className={className}>
171
+ <path d="M3 2.5h8l2.5 2.5v8.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1Z" />
172
+ <path d="M5 2.5v3.5h4.5V2.5M5 13.5V9.5h6v4" />
173
+ </svg>
174
+ )
175
+ }
176
+
177
+ export function DownloadIcon({ size = 14, className }: IconProps): JSX.Element {
178
+ return (
179
+ <svg {...base(size)} className={className}>
180
+ <path d="M8 2v7.5M4.5 6.5 8 10l3.5-3.5" />
181
+ <path d="M2.5 13h11" />
182
+ </svg>
183
+ )
184
+ }
185
+
186
+ export function ExternalIcon({ size = 14, className }: IconProps): JSX.Element {
187
+ return (
188
+ <svg {...base(size)} className={className}>
189
+ <path d="M9 2.5h4.5V7" />
190
+ <path d="M13.5 2.5 7.5 8.5" />
191
+ <path d="M11.5 9v3.5a1 1 0 0 1-1 1H3.5a1 1 0 0 1-1-1V5.5a1 1 0 0 1 1-1H7" />
192
+ </svg>
193
+ )
194
+ }
195
+
196
+ export function SearchIcon({ size = 14, className }: IconProps): JSX.Element {
197
+ return (
198
+ <svg {...base(size)} className={className}>
199
+ <circle cx="7" cy="7" r="4.5" />
200
+ <path d="m10.5 10.5 3 3" />
201
+ </svg>
202
+ )
203
+ }
204
+
205
+ export function BranchIcon({ size = 14, className }: IconProps): JSX.Element {
206
+ return (
207
+ <svg {...base(size)} className={className}>
208
+ <circle cx="4" cy="3.5" r="1.5" />
209
+ <circle cx="4" cy="12.5" r="1.5" />
210
+ <circle cx="12" cy="6.5" r="1.5" />
211
+ <path d="M4 5v5M4 10.5c0-2 2-2.5 4-2.5s4-.5 4-1.5" />
212
+ </svg>
213
+ )
214
+ }
215
+
216
+ export function ListIcon({ size = 14, className }: IconProps): JSX.Element {
217
+ return (
218
+ <svg {...base(size)} className={className}>
219
+ <path d="M2.5 4h11M2.5 8h11M2.5 12h11" />
220
+ </svg>
221
+ )
222
+ }
223
+
224
+ export function TreeIcon({ size = 14, className }: IconProps): JSX.Element {
225
+ return (
226
+ <svg {...base(size)} className={className}>
227
+ <path d="M2.5 3.5h6M8.5 8h5M2.5 8h2" />
228
+ <path d="M6 3.5v7" />
229
+ <path d="M11 8v4.5h-2.5" />
230
+ </svg>
231
+ )
232
+ }
233
+
234
+ export function ShrinkIcon({ size = 14, className }: IconProps): JSX.Element {
235
+ return (
236
+ <svg {...base(size)} className={className}>
237
+ <path d="M14 14 10 10M10 14v-4h4" />
238
+ <path d="M2 2l4 4M6 2v4H2" />
239
+ </svg>
240
+ )
241
+ }
242
+
243
+ export function ExpandRightIcon({ size = 16, className }: IconProps): JSX.Element {
244
+ return (
245
+ <svg {...base(size)} className={className}>
246
+ <path d="M6 3.5 11.5 8 6 12.5" />
247
+ </svg>
248
+ )
249
+ }
250
+
251
+ export function EmptyFolderIcon({ size = 16, className }: IconProps): JSX.Element {
252
+ return (
253
+ <svg {...base(size)} className={className}>
254
+ <path d="M2 3.5h4l1.5 2H14a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1Z" opacity="0.55" />
255
+ </svg>
256
+ )
257
+ }
258
+
259
+ export function MaximizeIcon({ size = 14, className }: IconProps): JSX.Element {
260
+ return (
261
+ <svg {...base(size)} className={className}>
262
+ <rect x="3" y="3" width="10" height="10" rx="1" />
263
+ </svg>
264
+ )
265
+ }
266
+
267
+ export function RestoreIcon({ size = 14, className }: IconProps): JSX.Element {
268
+ return (
269
+ <svg {...base(size)} className={className}>
270
+ <rect x="3" y="5" width="8" height="8" rx="1" />
271
+ <path d="M6.5 3.5h5a1 1 0 0 1 1 1V10" />
272
+ </svg>
273
+ )
274
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,5 @@
1
+ /** CSS Modules typing for the tsdown CSS-inline preset (hashed class map). */
2
+ declare module '*.module.css' {
3
+ const classes: Record<string, string>
4
+ export default classes
5
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Pure drag-to-composer helpers shared by the explorer rows (the drag
3
+ * source) and the composer dock inlay (the drop target): the custom MIME
4
+ * type, the drag-state detector, and the draft-splicing rule. Deliberately
5
+ * framework-free so the splicing math is unit-testable in isolation.
6
+ *
7
+ * The composer host only accepts OS image drops (its document-level drop
8
+ * handler checks `dataTransfer.types` for `Files` and routes through the
9
+ * image pipeline), so a workspace file needs its own MIME. A plain relative
10
+ * path is inserted into the draft — the agent reads the file through its
11
+ * existing tools without any prefix grammar.
12
+ * @module dsh-filemgr/client/drag/file-drag
13
+ */
14
+
15
+ /** Custom MIME carrying a workspace-relative file path. */
16
+ export const FILE_DRAG_MIME = 'application/x-dsh-file'
17
+
18
+ /**
19
+ * Whether a drag event carries our file payload.
20
+ * @param types - the live `dataTransfer.types` list (read-only during drag).
21
+ * @returns true when our MIME is present.
22
+ */
23
+ export function hasFileDrag(types: readonly string[] | undefined): boolean {
24
+ return types !== undefined && types.includes(FILE_DRAG_MIME)
25
+ }
26
+
27
+ /**
28
+ * Whether a drop payload is a plausible workspace-relative path. The custom
29
+ * MIME only proves the drag carries *some* string — a foreign page can set it
30
+ * too, so the payload itself is validated before it reaches the draft:
31
+ * relative POSIX shape only; no absolute paths, no '..' segments, no
32
+ * backslashes, no control characters, and a sane length.
33
+ * @param path - the raw payload from dataTransfer.
34
+ * @returns true when the payload is safe to splice into the draft.
35
+ */
36
+ export function isValidFileDragPayload(path: string): boolean {
37
+ if (path === '' || path.length > 512) return false
38
+ if (path.startsWith('/') || path.includes('\\')) return false
39
+ // eslint-disable-next-line no-control-regex
40
+ if (/[\x00-\x1f\x7f]/.test(path)) return false
41
+ if (path.split('/').some(segment => segment === '..')) return false
42
+ return true
43
+ }
44
+
45
+ /**
46
+ * Splice a workspace-relative path into a composer draft at the caret.
47
+ *
48
+ * Separator rule: one space is added before the path unless the caret sits
49
+ * at the start of the draft or right after whitespace; one space is added
50
+ * after the path unless the caret sits at the end of the draft or right
51
+ * before whitespace. Empty path or an out-of-range caret are no-ops.
52
+ *
53
+ * @param draft - the current draft text.
54
+ * @param path - the relative path to insert.
55
+ * @param caret - insertion offset (default: the end of the draft).
56
+ * @returns the next draft; the caller owns writing it through the input
57
+ * facade.
58
+ */
59
+ export function insertPathIntoDraft(draft: string, path: string, caret?: number): string {
60
+ if (path === '') return draft
61
+ const at = caret === undefined ? draft.length : Math.min(Math.max(caret, 0), draft.length)
62
+ const before = draft.slice(0, at)
63
+ const after = draft.slice(at)
64
+ const needBefore = before !== '' && !/\s$/.test(before)
65
+ const needAfter = after !== '' && !/^\s/.test(after)
66
+ return before + (needBefore ? ' ' : '') + path + (needAfter ? ' ' : '') + after
67
+ }