@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.
- package/LICENSE +38 -0
- package/README.i18n.yaml +6 -0
- package/README.md +66 -0
- package/README.zh.md +66 -0
- package/cordis.patch.yml +13 -0
- package/lib/assets/mermaid.min.js +3587 -0
- package/lib/client.js +7621 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +3207 -0
- package/lib/tsconfig.client.tsbuildinfo +1 -0
- package/lib/tsconfig.host.tsbuildinfo +1 -0
- package/lib/types/client/FileManagerSettingsCard.d.ts +56 -0
- package/lib/types/client/FileManagerSettingsCard.d.ts.map +1 -0
- package/lib/types/client/PluginSettingsCard.d.ts +104 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
- package/lib/types/client/api.d.ts +75 -0
- package/lib/types/client/api.d.ts.map +1 -0
- package/lib/types/client/chat/AttachedFilesDock.d.ts +51 -0
- package/lib/types/client/chat/AttachedFilesDock.d.ts.map +1 -0
- package/lib/types/client/chat/file-ref.d.ts +35 -0
- package/lib/types/client/chat/file-ref.d.ts.map +1 -0
- package/lib/types/client/chat/mermaid-chat.d.ts +30 -0
- package/lib/types/client/chat/mermaid-chat.d.ts.map +1 -0
- package/lib/types/client/chat/placeholder-hint.d.ts +16 -0
- package/lib/types/client/chat/placeholder-hint.d.ts.map +1 -0
- package/lib/types/client/components/ExplorerPanel.d.ts +26 -0
- package/lib/types/client/components/ExplorerPanel.d.ts.map +1 -0
- package/lib/types/client/components/FileIcon.d.ts +15 -0
- package/lib/types/client/components/FileIcon.d.ts.map +1 -0
- package/lib/types/client/components/ScmPanel.d.ts +23 -0
- package/lib/types/client/components/ScmPanel.d.ts.map +1 -0
- package/lib/types/client/components/a11y.d.ts +12 -0
- package/lib/types/client/components/a11y.d.ts.map +1 -0
- package/lib/types/client/components/icons.d.ts +41 -0
- package/lib/types/client/components/icons.d.ts.map +1 -0
- package/lib/types/client/components/overlay.d.ts +45 -0
- package/lib/types/client/components/overlay.d.ts.map +1 -0
- package/lib/types/client/drag/DragFileInlay.d.ts +31 -0
- package/lib/types/client/drag/DragFileInlay.d.ts.map +1 -0
- package/lib/types/client/drag/file-drag.d.ts +47 -0
- package/lib/types/client/drag/file-drag.d.ts.map +1 -0
- package/lib/types/client/drag.d.ts +31 -0
- package/lib/types/client/drag.d.ts.map +1 -0
- package/lib/types/client/file-source.d.ts +29 -0
- package/lib/types/client/file-source.d.ts.map +1 -0
- package/lib/types/client/fileType.d.ts +31 -0
- package/lib/types/client/fileType.d.ts.map +1 -0
- package/lib/types/client/floating.d.ts +27 -0
- package/lib/types/client/floating.d.ts.map +1 -0
- package/lib/types/client/hooks/useResizableSplit.d.ts +37 -0
- package/lib/types/client/hooks/useResizableSplit.d.ts.map +1 -0
- package/lib/types/client/hooks/useStore.d.ts +10 -0
- package/lib/types/client/hooks/useStore.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +57 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/layout.d.ts +104 -0
- package/lib/types/client/layout.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +125 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/maximize.d.ts +24 -0
- package/lib/types/client/maximize.d.ts.map +1 -0
- package/lib/types/client/mention.d.ts +44 -0
- package/lib/types/client/mention.d.ts.map +1 -0
- package/lib/types/client/mount.d.ts +22 -0
- package/lib/types/client/mount.d.ts.map +1 -0
- package/lib/types/client/persist.d.ts +55 -0
- package/lib/types/client/persist.d.ts.map +1 -0
- package/lib/types/client/picker/FilePickerModal.d.ts +47 -0
- package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -0
- package/lib/types/client/picker/file-picker.d.ts +62 -0
- package/lib/types/client/picker/file-picker.d.ts.map +1 -0
- package/lib/types/client/preview/PreviewPanel.d.ts +17 -0
- package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -0
- package/lib/types/client/preview/PreviewTabs.d.ts +30 -0
- package/lib/types/client/preview/PreviewTabs.d.ts.map +1 -0
- package/lib/types/client/preview/PreviewToolbar.d.ts +37 -0
- package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -0
- package/lib/types/client/preview/content.d.ts +43 -0
- package/lib/types/client/preview/content.d.ts.map +1 -0
- package/lib/types/client/preview/markdown.d.ts +60 -0
- package/lib/types/client/preview/markdown.d.ts.map +1 -0
- package/lib/types/client/preview/mermaid.d.ts +78 -0
- package/lib/types/client/preview/mermaid.d.ts.map +1 -0
- package/lib/types/client/preview/selection.d.ts +26 -0
- package/lib/types/client/preview/selection.d.ts.map +1 -0
- package/lib/types/client/reference.d.ts +75 -0
- package/lib/types/client/reference.d.ts.map +1 -0
- package/lib/types/client/settings-form.d.ts +206 -0
- package/lib/types/client/settings-form.d.ts.map +1 -0
- package/lib/types/client/store.d.ts +279 -0
- package/lib/types/client/store.d.ts.map +1 -0
- package/lib/types/core/types.d.ts +132 -0
- package/lib/types/core/types.d.ts.map +1 -0
- package/lib/types/host/fs-service.d.ts +96 -0
- package/lib/types/host/fs-service.d.ts.map +1 -0
- package/lib/types/host/gate.d.ts +45 -0
- package/lib/types/host/gate.d.ts.map +1 -0
- package/lib/types/host/git-runner.d.ts +80 -0
- package/lib/types/host/git-runner.d.ts.map +1 -0
- package/lib/types/host/git-service.d.ts +130 -0
- package/lib/types/host/git-service.d.ts.map +1 -0
- package/lib/types/host/loopback.d.ts +25 -0
- package/lib/types/host/loopback.d.ts.map +1 -0
- package/lib/types/host/poll-guard.d.ts +55 -0
- package/lib/types/host/poll-guard.d.ts.map +1 -0
- package/lib/types/host/routes.d.ts +54 -0
- package/lib/types/host/routes.d.ts.map +1 -0
- package/lib/types/index.d.ts +35 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/mount-once.d.ts +25 -0
- package/lib/types/mount-once.d.ts.map +1 -0
- package/package.json +85 -0
- package/src/client/FileManagerSettingsCard.tsx +117 -0
- package/src/client/PluginSettingsCard.tsx +337 -0
- package/src/client/api.ts +141 -0
- package/src/client/chat/file-ref.ts +144 -0
- package/src/client/chat/mermaid-chat.tsx +102 -0
- package/src/client/chat/placeholder-hint.tsx +46 -0
- package/src/client/components/ExplorerPanel.tsx +534 -0
- package/src/client/components/FileIcon.tsx +46 -0
- package/src/client/components/ScmPanel.tsx +480 -0
- package/src/client/components/a11y.ts +20 -0
- package/src/client/components/icons.tsx +274 -0
- package/src/client/components/overlay.tsx +225 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/drag/DragFileInlay.tsx +89 -0
- package/src/client/drag/file-drag.ts +67 -0
- package/src/client/drag.ts +154 -0
- package/src/client/file-source.ts +58 -0
- package/src/client/fileType.ts +129 -0
- package/src/client/floating.ts +63 -0
- package/src/client/hooks/useResizableSplit.ts +91 -0
- package/src/client/hooks/useStore.ts +15 -0
- package/src/client/index.ts +389 -0
- package/src/client/layout.ts +546 -0
- package/src/client/locales.ts +247 -0
- package/src/client/maximize.ts +34 -0
- package/src/client/mount.tsx +74 -0
- package/src/client/persist.ts +194 -0
- package/src/client/picker/FilePickerModal.tsx +506 -0
- package/src/client/picker/file-picker.ts +86 -0
- package/src/client/preview/PreviewPanel.tsx +233 -0
- package/src/client/preview/PreviewTabs.tsx +159 -0
- package/src/client/preview/PreviewToolbar.tsx +160 -0
- package/src/client/preview/content.tsx +617 -0
- package/src/client/preview/markdown.ts +351 -0
- package/src/client/preview/mermaid.ts +274 -0
- package/src/client/preview/selection.ts +82 -0
- package/src/client/reference.ts +161 -0
- package/src/client/settings-card.module.css +316 -0
- package/src/client/settings-form.ts +451 -0
- package/src/client/store.ts +1290 -0
- package/src/client/styles/chip.module.css +38 -0
- package/src/client/styles/drag.module.css +30 -0
- package/src/client/styles/explorer.module.css +371 -0
- package/src/client/styles/picker.module.css +344 -0
- package/src/client/styles/preview.module.css +904 -0
- package/src/client/styles/scm.module.css +380 -0
- package/src/client/styles/tokens.module.css +480 -0
- package/src/core/types.ts +162 -0
- package/src/host/fs-service.ts +513 -0
- package/src/host/gate.ts +74 -0
- package/src/host/git-runner.ts +116 -0
- package/src/host/git-service.ts +395 -0
- package/src/host/loopback.ts +63 -0
- package/src/host/poll-guard.ts +109 -0
- package/src/host/routes.ts +695 -0
- package/src/index.ts +97 -0
- 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,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
|
+
}
|