@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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-free drag machinery shared by the React hook (useResizableSplit)
|
|
3
|
+
* and the DOM layout controller: one pointer-down handler that owns capture,
|
|
4
|
+
* the rAF-flush loop, the e.buttons === 0 fallback, every end path, and the
|
|
5
|
+
* drag styles. Pure DOM — no React, so both callers run the exact same
|
|
6
|
+
* smoothness core (FileManager's useResizableSplit architecture, re-implemented).
|
|
7
|
+
* @module dsh-filemgr/client/drag
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface DragStartOptions {
|
|
11
|
+
/** Drag left = grow (handle on the left edge of its container). */
|
|
12
|
+
reverse: boolean
|
|
13
|
+
/** Width at drag start (read at pointer-down time). */
|
|
14
|
+
getStartWidth: () => number
|
|
15
|
+
/** Clamped width from the raw delta (px mode adds deltaX directly). */
|
|
16
|
+
compute: (startWidth: number, deltaX: number) => number
|
|
17
|
+
/** Per-frame flush (rAF-merged; called at most once per frame). */
|
|
18
|
+
onFrame: (width: number) => void
|
|
19
|
+
/** Final commit (also fired on every end path). */
|
|
20
|
+
onEnd: (width: number) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Whether a pointer event is the primary (left) button or touch. */
|
|
24
|
+
function isPrimaryPointer(event: PointerEvent): boolean {
|
|
25
|
+
return event.pointerType === 'touch' || event.button === 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handle one pointer-down: wire capture + window listeners, run the rAF
|
|
30
|
+
* loop, and end on any of the five termination paths. Call from a
|
|
31
|
+
* onPointerDown handler (React or plain DOM).
|
|
32
|
+
* @param event - the raw pointerdown event.
|
|
33
|
+
* @param el - the handle element (capture target + reverse marker source).
|
|
34
|
+
* @param opts - drag behavior.
|
|
35
|
+
* @returns a disposer (idempotent; also called internally on end).
|
|
36
|
+
*/
|
|
37
|
+
export function handlePointerDragStart(event: PointerEvent, el: HTMLElement, opts: DragStartOptions): () => void {
|
|
38
|
+
if (!isPrimaryPointer(event)) return () => {}
|
|
39
|
+
event.preventDefault()
|
|
40
|
+
|
|
41
|
+
const startX = event.clientX
|
|
42
|
+
const startWidth = opts.getStartWidth()
|
|
43
|
+
const pointerId = event.pointerId
|
|
44
|
+
const reverse = opts.reverse
|
|
45
|
+
|
|
46
|
+
let rafId: number | null = null
|
|
47
|
+
let pendingWidth: number | null = null
|
|
48
|
+
let latestWidth = startWidth
|
|
49
|
+
let isDragging = true
|
|
50
|
+
let cleanup: (() => void) | null = null
|
|
51
|
+
|
|
52
|
+
const flushPending = (): void => {
|
|
53
|
+
if (pendingWidth === null) return
|
|
54
|
+
latestWidth = pendingWidth
|
|
55
|
+
opts.onFrame(pendingWidth)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const addWindowListener = <K extends keyof WindowEventMap>(
|
|
59
|
+
key: K,
|
|
60
|
+
handler: (e: WindowEventMap[K]) => void,
|
|
61
|
+
): (() => void) => {
|
|
62
|
+
window.addEventListener(key, handler)
|
|
63
|
+
return () => window.removeEventListener(key, handler)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const computeWidth = (clientX: number): number => {
|
|
67
|
+
const deltaX = reverse ? startX - clientX : clientX - startX
|
|
68
|
+
return opts.compute(startWidth, deltaX)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const finishDrag = (e?: PointerEvent | MouseEvent | FocusEvent): void => {
|
|
72
|
+
if (!isDragging) return
|
|
73
|
+
isDragging = false
|
|
74
|
+
if (rafId !== null) {
|
|
75
|
+
cancelAnimationFrame(rafId)
|
|
76
|
+
rafId = null
|
|
77
|
+
}
|
|
78
|
+
flushPending()
|
|
79
|
+
let finalWidth = latestWidth
|
|
80
|
+
if (e !== undefined && 'clientX' in e && typeof e.clientX === 'number') {
|
|
81
|
+
finalWidth = computeWidth(e.clientX)
|
|
82
|
+
}
|
|
83
|
+
opts.onEnd(finalWidth)
|
|
84
|
+
cleanup?.()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handlePointerMove = (e: PointerEvent): void => {
|
|
88
|
+
if (!isDragging) return
|
|
89
|
+
if (e.buttons === 0) {
|
|
90
|
+
// Lost pointerup (capture steal / release outside): end now.
|
|
91
|
+
finishDrag(e)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
pendingWidth = computeWidth(e.clientX)
|
|
95
|
+
if (rafId === null) {
|
|
96
|
+
rafId = requestAnimationFrame(() => {
|
|
97
|
+
rafId = null
|
|
98
|
+
flushPending()
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const handlePointerUp = (e: PointerEvent): void => finishDrag(e)
|
|
103
|
+
const handlePointerCancel = (e: PointerEvent): void => finishDrag(e)
|
|
104
|
+
const handleMouseUp = (e: MouseEvent): void => finishDrag(e)
|
|
105
|
+
const handleLostCapture = (): void => finishDrag()
|
|
106
|
+
|
|
107
|
+
// Drag styles: no text selection, col-resize cursor, disable transitions.
|
|
108
|
+
const previousUserSelect = document.body.style.userSelect
|
|
109
|
+
const previousCursor = document.body.style.cursor
|
|
110
|
+
document.body.style.userSelect = 'none'
|
|
111
|
+
document.body.style.cursor = 'col-resize'
|
|
112
|
+
const frame = el.closest('[data-dsh-frame]')
|
|
113
|
+
frame?.setAttribute('data-filemgr-instant', '')
|
|
114
|
+
|
|
115
|
+
const restore = (): void => {
|
|
116
|
+
document.body.style.userSelect = previousUserSelect
|
|
117
|
+
document.body.style.cursor = previousCursor
|
|
118
|
+
frame?.removeAttribute('data-filemgr-instant')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (el.setPointerCapture) {
|
|
122
|
+
try {
|
|
123
|
+
el.setPointerCapture(pointerId)
|
|
124
|
+
el.addEventListener('lostpointercapture', handleLostCapture)
|
|
125
|
+
} catch {
|
|
126
|
+
// capture failed; the window listeners cover the drag
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const releaseCapture = (): void => {
|
|
131
|
+
try {
|
|
132
|
+
if (el.releasePointerCapture && el.hasPointerCapture?.(pointerId)) {
|
|
133
|
+
el.releasePointerCapture(pointerId)
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// already released
|
|
137
|
+
}
|
|
138
|
+
el.removeEventListener('lostpointercapture', handleLostCapture)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const listeners = [
|
|
142
|
+
addWindowListener('pointermove', handlePointerMove),
|
|
143
|
+
addWindowListener('pointerup', handlePointerUp),
|
|
144
|
+
addWindowListener('pointercancel', handlePointerCancel),
|
|
145
|
+
addWindowListener('mouseup', handleMouseUp),
|
|
146
|
+
addWindowListener('blur', () => finishDrag()),
|
|
147
|
+
]
|
|
148
|
+
cleanup = () => {
|
|
149
|
+
restore()
|
|
150
|
+
releaseCapture()
|
|
151
|
+
for (const dispose of listeners) dispose()
|
|
152
|
+
}
|
|
153
|
+
return cleanup
|
|
154
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `file` input-trigger source: registers the reference codec the shell's
|
|
3
|
+
* submit pipeline needs to serialize a file chip (`slash: no serializer for
|
|
4
|
+
* reference source "file"` disappears once this source is in the roster).
|
|
5
|
+
*
|
|
6
|
+
* The source is deliberately INERT as a menu participant: the file picker is
|
|
7
|
+
* our own pi-desktop-style tree modal (see picker/FilePickerModal.tsx), not
|
|
8
|
+
* the shell's flat trigger menu, so `candidates` returns nothing and `onPick`
|
|
9
|
+
* never fires. Its only jobs are (1) to be findable by
|
|
10
|
+
* `inputTriggers.serializeReference('file', ref)` and (2) to own the model
|
|
11
|
+
* projection of a chip — the `@path:lines` plain-text token.
|
|
12
|
+
*
|
|
13
|
+
* Trigger '@' keeps the chip family visually consistent with the other '@'
|
|
14
|
+
* sources; the menu group this source contributes is empty and is covered by
|
|
15
|
+
* the picker modal the moment a bare `@` lands.
|
|
16
|
+
* @module dsh-filemgr/client/file-source
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
20
|
+
import type { InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
|
21
|
+
|
|
22
|
+
/** The roster name the chip occurrences address (see reference.ts). */
|
|
23
|
+
export const FILE_SOURCE_NAME = 'file'
|
|
24
|
+
|
|
25
|
+
/** Required services: the trigger roster plus the session scope carrier. */
|
|
26
|
+
export const inject = ['inputTriggers']
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register the inert `file` reference source (codec only). Safe to call
|
|
30
|
+
* multiple times (each call returns a disposer; the roster rejects a
|
|
31
|
+
* duplicate (trigger, name) seat, so guard with a module flag).
|
|
32
|
+
*/
|
|
33
|
+
export function registerFileReferenceSource(ctx: ClientContext): () => void {
|
|
34
|
+
// ctx.get — not property access: the caller (index.ts apply) does not
|
|
35
|
+
// declare 'inputTriggers' in its inject list, and property access would
|
|
36
|
+
// throw "cannot get property without inject". get() needs no inject.
|
|
37
|
+
const inputTriggers = (ctx.get as (name: string) => unknown)('inputTriggers') as
|
|
38
|
+
| { registerSource(src: InputTriggerSource): () => void }
|
|
39
|
+
| undefined
|
|
40
|
+
if (inputTriggers === undefined) return () => {}
|
|
41
|
+
const source: InputTriggerSource = {
|
|
42
|
+
trigger: '@',
|
|
43
|
+
name: FILE_SOURCE_NAME,
|
|
44
|
+
candidates: async () => [],
|
|
45
|
+
onPick: () => undefined,
|
|
46
|
+
codec: {
|
|
47
|
+
clipboardText: (ref) => ref,
|
|
48
|
+
serialize: (ref, _signal) => Promise.resolve(ref),
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return inputTriggers.registerSource(source)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Duplicate registration (HMR remount) is not a failure.
|
|
55
|
+
console.warn('[dsh-filemgr] file reference source already registered:', error)
|
|
56
|
+
return () => {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview content-type detection from a file name — the router's single
|
|
3
|
+
* source of truth for what a file becomes when opened (mirrors FileManager's
|
|
4
|
+
* getFileTypeInfo table, re-derived for the panel's format set).
|
|
5
|
+
* @module dsh-filemgr/client/fileType
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PreviewContentType } from '../core/types.ts'
|
|
9
|
+
|
|
10
|
+
/** Markdown extensions. */
|
|
11
|
+
const MARKDOWN_EXT = new Set(['md', 'markdown', 'mdx'])
|
|
12
|
+
/** HTML extensions. */
|
|
13
|
+
const HTML_EXT = new Set(['html', 'htm', 'xhtml'])
|
|
14
|
+
/** Diff extensions. */
|
|
15
|
+
const DIFF_EXT = new Set(['diff', 'patch'])
|
|
16
|
+
/** CSV. */
|
|
17
|
+
const CSV_EXT = new Set(['csv'])
|
|
18
|
+
/** PDF. */
|
|
19
|
+
const PDF_EXT = new Set(['pdf'])
|
|
20
|
+
/** Office documents. */
|
|
21
|
+
const WORD_EXT = new Set(['doc', 'docx', 'odt', 'rtf'])
|
|
22
|
+
const EXCEL_EXT = new Set(['xls', 'xlsx', 'ods'])
|
|
23
|
+
const PPT_EXT = new Set(['ppt', 'pptx', 'odp'])
|
|
24
|
+
/** Images. */
|
|
25
|
+
const IMAGE_EXT = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico', 'bmp', 'avif'])
|
|
26
|
+
/** Extensions treated as editable code/text. */
|
|
27
|
+
const CODE_EXT = new Set([
|
|
28
|
+
'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'json', 'jsonc', 'css', 'scss', 'less',
|
|
29
|
+
'yml', 'yaml', 'toml', 'xml', 'sh', 'bash', 'zsh', 'fish', 'rs', 'py', 'go',
|
|
30
|
+
'java', 'c', 'h', 'cpp', 'hpp', 'cc', 'cs', 'sql', 'php', 'rb', 'swift', 'kt',
|
|
31
|
+
'vue', 'svelte', 'astro', 'txt', 'log', 'ini', 'env', 'conf', 'cfg', 'gitignore',
|
|
32
|
+
'dockerfile', 'makefile', 'graphql', 'proto', 'prisma', 'zig', 'lua', 'r', 'dart',
|
|
33
|
+
'ex', 'exs', 'erl', 'hs', 'clj', 'scala', 'groovy', 'vb', 'ps1', 'bat', 'cmd',
|
|
34
|
+
'pl', 'pm', 'tcl', 'asm', 's', 'f', 'f90', 'jl', 'nim', 'ml', 'elm', 'purs',
|
|
35
|
+
'solidity', 'sol', 'tf', 'hcl', 'dockerignore', 'editorconfig', 'prettierrc',
|
|
36
|
+
'eslintrc', 'babelrc', 'npmrc', 'nix', 'lock', 'map',
|
|
37
|
+
])
|
|
38
|
+
/** No-extension names that are plain text. */
|
|
39
|
+
const TEXT_NAMES = new Set([
|
|
40
|
+
'license', 'licence', 'readme', 'changelog', 'contributing', 'authors', 'notice',
|
|
41
|
+
'makefile', 'dockerfile', 'justfile', 'gemfile', 'rakefile', 'procfile',
|
|
42
|
+
])
|
|
43
|
+
/**
|
|
44
|
+
* Leading-dot config dotfiles whose full (dotted) basename is plain text. The
|
|
45
|
+
* de-dot rule below maps most single-dot files (`.gitignore` -> ext `gitignore`)
|
|
46
|
+
* into CODE_EXT; these multi-suffix / uncommon ones have no useful extension
|
|
47
|
+
* (`.env.local` -> `local`), so we match them by their whole dotted name.
|
|
48
|
+
*/
|
|
49
|
+
const DOTFILE_TEXT_NAMES = new Set([
|
|
50
|
+
'.gitignore', '.gitattributes', '.gitmodules', '.env', '.env.local',
|
|
51
|
+
'.env.production', '.env.development', '.env.test', '.npmrc', '.npmrc.template',
|
|
52
|
+
'.prettierrc', '.prettierrc.json', '.prettierrc.yaml', '.babelrc', '.babelrc.json',
|
|
53
|
+
'.eslintrc', '.eslintrc.json', '.eslintrc.js', '.editorconfig', '.dockerignore',
|
|
54
|
+
'.eslintignore', '.prettierignore', '.gitignore.local', '.hgignore',
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
/** Detect the preview content type of a file by name (lowercased). */
|
|
58
|
+
export function detectContentType(name: string): PreviewContentType {
|
|
59
|
+
const base = name.split('/').pop() ?? name
|
|
60
|
+
const lower = base.toLowerCase()
|
|
61
|
+
const dot = lower.lastIndexOf('.')
|
|
62
|
+
// Leading-dot files: the first dot is the hidden-file marker, not a
|
|
63
|
+
// separator — take the text after it (`.gitignore` -> `gitignore`).
|
|
64
|
+
const ext = lower[0] === '.'
|
|
65
|
+
? (dot > 0 ? lower.slice(dot + 1) : lower.slice(1))
|
|
66
|
+
: (dot > 0 ? lower.slice(dot + 1) : '')
|
|
67
|
+
const stem = dot > 0 ? lower.slice(0, dot) : lower
|
|
68
|
+
if (lower[0] === '.' && DOTFILE_TEXT_NAMES.has(lower)) return 'text'
|
|
69
|
+
if (ext === '' && TEXT_NAMES.has(stem)) return 'text'
|
|
70
|
+
if (ext === '') return 'unsupported'
|
|
71
|
+
if (MARKDOWN_EXT.has(ext)) return 'markdown'
|
|
72
|
+
if (HTML_EXT.has(ext)) return 'html'
|
|
73
|
+
if (DIFF_EXT.has(ext)) return 'diff'
|
|
74
|
+
if (CSV_EXT.has(ext)) return 'csv'
|
|
75
|
+
if (PDF_EXT.has(ext)) return 'pdf'
|
|
76
|
+
if (WORD_EXT.has(ext)) return 'word'
|
|
77
|
+
if (EXCEL_EXT.has(ext)) return 'excel'
|
|
78
|
+
if (PPT_EXT.has(ext)) return 'ppt'
|
|
79
|
+
if (IMAGE_EXT.has(ext)) return 'image'
|
|
80
|
+
if (CODE_EXT.has(ext)) return 'code'
|
|
81
|
+
return 'unsupported'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Whether the type can be edited and saved back. */
|
|
85
|
+
export function isEditableType(type: PreviewContentType): boolean {
|
|
86
|
+
return type === 'markdown' || type === 'html' || type === 'code' || type === 'csv' || type === 'text'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Whether the type reads its content as text (vs image data URL). */
|
|
90
|
+
export function isTextType(type: PreviewContentType): boolean {
|
|
91
|
+
return type !== 'image' && type !== 'pdf' && type !== 'word' && type !== 'excel'
|
|
92
|
+
&& type !== 'ppt' && type !== 'unsupported' && type !== 'url'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A stable tab id from the file identity (root + path + type). */
|
|
96
|
+
export function tabIdOf(root: string, path: string, type: PreviewContentType): string {
|
|
97
|
+
return `${root}\u0000${path}\u0000${type}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The language hint for code tabs (extension without the dot). */
|
|
101
|
+
export function languageOf(name: string): string {
|
|
102
|
+
const base = name.split('/').pop() ?? name
|
|
103
|
+
const dot = base.lastIndexOf('.')
|
|
104
|
+
return dot > 0 ? base.slice(dot + 1) : ''
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The title for a tab: the basename. */
|
|
108
|
+
export function basenameOf(path: string): string {
|
|
109
|
+
const parts = path.split('/')
|
|
110
|
+
return parts[parts.length - 1] ?? path
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The parent relative path of a path ('' for a root-level item). */
|
|
114
|
+
export function parentRel(path: string): string {
|
|
115
|
+
const idx = path.lastIndexOf('/')
|
|
116
|
+
return idx > 0 ? path.slice(0, idx) : ''
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The streaming URL a pdf tab renders: the host raw route serves the bytes
|
|
121
|
+
* with mime application/pdf, so the preview iframe loads them directly — no
|
|
122
|
+
* base64 round-trip and no read-size cap. The nonce defeats browser caching
|
|
123
|
+
* when the tab is refreshed after the file changed on disk.
|
|
124
|
+
*
|
|
125
|
+
* Contributed by EricWang1358 (#239).
|
|
126
|
+
*/
|
|
127
|
+
export function pdfPreviewUrl(root: string, path: string, nonce: number): string {
|
|
128
|
+
return `/filemgr/raw?root=${encodeURIComponent(root)}&path=${encodeURIComponent(path)}&v=${nonce}`
|
|
129
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating expand button geometry (issues #374 / #292): the button is a
|
|
3
|
+
* fixed chrome element docked at the viewport's top-right corner, just
|
|
4
|
+
* below the shell conversation header — the horizontal divider under the
|
|
5
|
+
* "Session log" row — so the re-expand control sits below the header
|
|
6
|
+
* chrome instead of overlapping it. Its top stays below the Window
|
|
7
|
+
* Controls Overlay titlebar strip when dsh-desktop reports one (issue
|
|
8
|
+
* #292). Every computed position is clamped into the usable range.
|
|
9
|
+
* @module dsh-filemgr/client/floating
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Breathing room above/below the button (px). */
|
|
13
|
+
export const FLOATING_MARGIN_PX = 6
|
|
14
|
+
/** Gap between the shell header's bottom divider and the button top (px). */
|
|
15
|
+
export const FLOATING_HEADER_GAP_PX = 8
|
|
16
|
+
/** The button's rendered size (kept in sync with tokens.module.css). */
|
|
17
|
+
export const FLOATING_BUTTON_HEIGHT_PX = 24
|
|
18
|
+
/** Top offset of the explorer's collapse chevron inside its tab bar (px).
|
|
19
|
+
* Used only as a fallback when the shell conversation header is not found
|
|
20
|
+
* (kept in sync with the chevron rule in tokens.module.css). */
|
|
21
|
+
export const COLLAPSE_CHEVRON_TOP_PX = 6
|
|
22
|
+
|
|
23
|
+
/** Minimal Window Controls Overlay surface (untyped in older TS DOM libs). */
|
|
24
|
+
interface WindowControlsOverlayLike {
|
|
25
|
+
visible: boolean
|
|
26
|
+
getTitlebarAreaRect?: () => { height?: number }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The WCO titlebar height when visible; 0 in a plain browser tab. */
|
|
30
|
+
export function titlebarAreaHeight(): number {
|
|
31
|
+
const wco = (navigator as Navigator & { windowControlsOverlay?: WindowControlsOverlayLike }).windowControlsOverlay
|
|
32
|
+
if (wco === undefined || !wco.visible || wco.getTitlebarAreaRect === undefined) return 0
|
|
33
|
+
try {
|
|
34
|
+
const rect = wco.getTitlebarAreaRect()
|
|
35
|
+
const height = rect?.height ?? 0
|
|
36
|
+
return height > 0 ? Math.round(height) : 0
|
|
37
|
+
} catch {
|
|
38
|
+
return 0
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Clamp a requested top px into the usable vertical range. */
|
|
43
|
+
export function clampFloatingTop(
|
|
44
|
+
top: number,
|
|
45
|
+
viewportHeight: number,
|
|
46
|
+
buttonHeight: number,
|
|
47
|
+
titlebar: number,
|
|
48
|
+
): number {
|
|
49
|
+
const min = titlebar + FLOATING_MARGIN_PX
|
|
50
|
+
const max = Math.max(min, viewportHeight - buttonHeight - FLOATING_MARGIN_PX)
|
|
51
|
+
if (!Number.isFinite(top)) return min
|
|
52
|
+
return Math.min(max, Math.max(min, top))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The default top: aligned with the collapse chevron at the top-right. */
|
|
56
|
+
export function topAlignedFloatingTop(viewportHeight: number, buttonHeight: number, titlebar: number): number {
|
|
57
|
+
return clampFloatingTop(
|
|
58
|
+
titlebar + COLLAPSE_CHEVRON_TOP_PX,
|
|
59
|
+
viewportHeight,
|
|
60
|
+
buttonHeight,
|
|
61
|
+
titlebar,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The panel system's single drag engine hook — a thin React wrapper over the
|
|
3
|
+
* framework-free machinery in drag.ts (FileManager's useResizableSplit
|
|
4
|
+
* architecture, re-implemented): px or ratio units, range-validated
|
|
5
|
+
* localStorage persistence, double-click reset to the default width.
|
|
6
|
+
* @module dsh-filemgr/client/hooks/useResizableSplit
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
10
|
+
import type { PointerEvent as ReactPointerEvent } from 'react'
|
|
11
|
+
import { handlePointerDragStart } from '../drag.ts'
|
|
12
|
+
import { readStoredNumber, writeStoredNumber } from '../persist.ts'
|
|
13
|
+
|
|
14
|
+
export interface UseResizableSplitOptions {
|
|
15
|
+
/** Default width (px or percent). */
|
|
16
|
+
defaultWidth?: number
|
|
17
|
+
/** Minimum (same unit). */
|
|
18
|
+
minWidth?: number
|
|
19
|
+
/** Maximum (same unit). */
|
|
20
|
+
maxWidth?: number
|
|
21
|
+
/** localStorage key (preference persistence). */
|
|
22
|
+
storageKey?: string
|
|
23
|
+
/** 'px' for fixed pixel widths, 'ratio' for percents (default 'ratio'). */
|
|
24
|
+
unit?: 'px' | 'ratio'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DragHandleProps {
|
|
28
|
+
onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void
|
|
29
|
+
onDoubleClick: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resizable-split engine.
|
|
34
|
+
* @param options - width contract + persistence key.
|
|
35
|
+
* @returns current width, the committed setter, handle props, and the clamp.
|
|
36
|
+
*/
|
|
37
|
+
export function useResizableSplit(options: UseResizableSplitOptions = {}) {
|
|
38
|
+
const {
|
|
39
|
+
defaultWidth = 50,
|
|
40
|
+
minWidth = 20,
|
|
41
|
+
maxWidth = 80,
|
|
42
|
+
storageKey,
|
|
43
|
+
unit = 'ratio',
|
|
44
|
+
} = options
|
|
45
|
+
const isPx = unit === 'px'
|
|
46
|
+
|
|
47
|
+
const [width, setWidthState] = useState(() =>
|
|
48
|
+
storageKey === undefined
|
|
49
|
+
? defaultWidth
|
|
50
|
+
: readStoredNumber(storageKey, minWidth, maxWidth, defaultWidth))
|
|
51
|
+
|
|
52
|
+
// The pointer-down closure reads the width at drag START without rebinding.
|
|
53
|
+
const widthRef = useRef(width)
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
widthRef.current = width
|
|
56
|
+
}, [width])
|
|
57
|
+
|
|
58
|
+
/** The committed setter: state + storage (validated) + resize event. */
|
|
59
|
+
const setWidth = useCallback((value: number) => {
|
|
60
|
+
setWidthState(value)
|
|
61
|
+
if (storageKey !== undefined) writeStoredNumber(storageKey, value)
|
|
62
|
+
try {
|
|
63
|
+
window.dispatchEvent(new CustomEvent('preview-panel-resize', { detail: { width: value } }))
|
|
64
|
+
} catch {
|
|
65
|
+
// event dispatch is best-effort
|
|
66
|
+
}
|
|
67
|
+
}, [storageKey])
|
|
68
|
+
|
|
69
|
+
const clamp = useCallback((value: number): number => {
|
|
70
|
+
return Math.min(maxWidth, Math.max(minWidth, value))
|
|
71
|
+
}, [minWidth, maxWidth])
|
|
72
|
+
|
|
73
|
+
const handlePointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
|
74
|
+
const el = event.currentTarget as HTMLDivElement
|
|
75
|
+
handlePointerDragStart(event.nativeEvent, el, {
|
|
76
|
+
reverse: el.dataset.reverse === 'true',
|
|
77
|
+
getStartWidth: () => widthRef.current,
|
|
78
|
+
compute: (startWidth, deltaX) => clamp(startWidth + deltaX),
|
|
79
|
+
onFrame: (value) => setWidthState(value),
|
|
80
|
+
onEnd: (value) => setWidth(value),
|
|
81
|
+
})
|
|
82
|
+
}, [clamp, setWidth])
|
|
83
|
+
|
|
84
|
+
const handleDoubleClick = useCallback(() => {
|
|
85
|
+
setWidth(defaultWidth)
|
|
86
|
+
}, [defaultWidth, setWidth])
|
|
87
|
+
|
|
88
|
+
const handleProps: DragHandleProps = { onPointerDown: handlePointerDown, onDoubleClick: handleDoubleClick }
|
|
89
|
+
|
|
90
|
+
return { width, setWidth, handleProps, clamp, isPx }
|
|
91
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React bindings for the framework-free stores: useSyncExternalStore with a
|
|
3
|
+
* stable snapshot (the stores return immutable snapshots, so selector-free
|
|
4
|
+
* subscription is safe), plus a stable-callback helper for event handlers.
|
|
5
|
+
* @module dsh-filemgr/client/hooks/useStore
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useSyncExternalStore } from 'react'
|
|
9
|
+
import type { StateHandle } from '../store.ts'
|
|
10
|
+
|
|
11
|
+
/** Subscribe a component to one store (full snapshot). */
|
|
12
|
+
export function useStore<S>(store: StateHandle<S>): S {
|
|
13
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)
|
|
14
|
+
}
|
|
15
|
+
|