@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,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
+