@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,247 @@
1
+ /**
2
+ * Locale strings for the panel surfaces (zh/en). The client registers the
3
+ * dictionary through the locale service like the sibling plugins; copy is
4
+ * deliberately short and technical.
5
+ * @module dsh-filemgr/client/locales
6
+ */
7
+
8
+ const zh = {
9
+ 'explorer.tabs.files': '文件',
10
+ 'explorer.tabs.changes': '变更',
11
+ 'explorer.search.placeholder': '按文件名搜索',
12
+ 'explorer.search.searching': '搜索中…',
13
+ 'explorer.search.empty': '没有匹配的文件',
14
+ 'explorer.search.error': '搜索失败',
15
+ 'explorer.search.truncated': '结果过多,仅显示前 {count} 条',
16
+ 'explorer.tree.empty': '项目为空',
17
+ 'explorer.collapse': '收起面板',
18
+ 'explorer.expand': '展开面板',
19
+ 'explorer.maximize': '最大化文件面板',
20
+ 'explorer.restore': '还原面板',
21
+ 'explorer.openPreview': '打开预览',
22
+ 'explorer.drag.dropHint': '松手插入文件路径',
23
+ 'scm.repositories': '存储库',
24
+ 'scm.changes': '变更',
25
+ 'scm.staged': '已暂存',
26
+ 'scm.unstaged': '变更',
27
+ 'scm.untracked': '未跟踪',
28
+ 'scm.conflicted': '冲突',
29
+ 'scm.stage': '暂存',
30
+ 'scm.unstage': '取消暂存',
31
+ 'scm.discard': '放弃更改',
32
+ 'scm.stageAll': '全部暂存',
33
+ 'scm.discardAll': '全部放弃',
34
+ 'scm.empty': '没有更改',
35
+ 'scm.notRepo': '当前目录不是 git 仓库',
36
+ 'scm.gitMissing': '未检测到 git,请先安装 git 后重试',
37
+ 'scm.loading': '读取状态中…',
38
+ 'scm.failed': '操作失败',
39
+ 'scm.viewList': '列表视图',
40
+ 'scm.viewTree': '树视图',
41
+ 'scm.discardConfirmTracked': '放弃对 {count} 个文件的更改?此操作不可恢复。',
42
+ 'scm.discardConfirmUntracked': '删除 {count} 个未跟踪文件?此操作不可恢复。',
43
+ 'preview.noTabs': '没有打开的预览',
44
+ 'preview.newUrlTab': '新建 URL 预览',
45
+ 'preview.collapsePanel': '收起预览面板',
46
+ 'preview.maximize': '最大化预览面板',
47
+ 'preview.restore': '还原面板',
48
+ 'preview.source': '源码',
49
+ 'preview.preview': '预览',
50
+ 'preview.editor': '编辑器',
51
+ 'preview.split': '分屏',
52
+ 'preview.refresh': '刷新',
53
+ 'preview.refresh.updated': '文件已在磁盘更新',
54
+ 'preview.save': '保存',
55
+ 'preview.download': '下载',
56
+ 'preview.copyCode': '复制代码',
57
+ 'preview.copyCodeDone': '已复制',
58
+ 'preview.openExternal': '在系统应用中打开',
59
+ 'preview.dirty': '未保存的更改',
60
+ 'preview.closeLeft': '关闭左侧',
61
+ 'preview.closeRight': '关闭右侧',
62
+ 'preview.closeOthers': '关闭其他',
63
+ 'preview.closeAll': '关闭全部',
64
+ 'preview.closeConfirmTitle': '关闭未保存的标签页',
65
+ 'preview.closeConfirmBody': '{count} 个标签页有未保存的更改,关闭将丢失这些更改。',
66
+ 'preview.saved': '已保存',
67
+ 'preview.saveConflict': '文件已在磁盘上被修改,保存冲突:请刷新后重试',
68
+ 'preview.errorOversized': '文件过大,仅加载前 80,000 字符',
69
+ 'preview.unsupported': '此格式暂不支持预览',
70
+ 'preview.downloadHint': '可在系统应用中打开或下载查看',
71
+ 'preview.url.placeholder': '输入网址,回车打开',
72
+ 'preview.url.hint': '按 Esc 还原',
73
+ 'common.cancel': '取消',
74
+ 'common.confirm': '确定',
75
+ 'common.close': '关闭',
76
+ 'common.delete': '删除',
77
+ 'common.copyPath': '复制路径',
78
+ 'common.copied': '已复制',
79
+ 'explorer.menu.copyPath': '复制路径',
80
+ 'explorer.menu.copyName': '复制名称',
81
+ 'explorer.menu.reveal': '在文件管理器中显示',
82
+ 'explorer.menu.openWithDefault': '用默认应用打开',
83
+ 'explorer.menu.rename': '重命名',
84
+ 'explorer.menu.newFile': '新建文件',
85
+ 'explorer.menu.newFolder': '新建文件夹',
86
+ 'explorer.menu.delete': '删除',
87
+ 'explorer.rename.title': '重命名',
88
+ 'explorer.newFile.title': '新建文件',
89
+ 'explorer.newFolder.title': '新建文件夹',
90
+ 'explorer.deleteConfirmTitle': '删除确认',
91
+ 'explorer.deleteConfirmBody': '确定要删除「{name}」吗?此操作不可恢复。',
92
+ 'explorer.opFailed': '操作失败',
93
+ 'explorer.refToInput': '引用到输入框',
94
+ 'settings.title': '右侧面板',
95
+ 'settings.description': 'Explorer / Preview 与 SCM 变更面板的总开关。',
96
+ 'settings.enabled': '启用右侧面板',
97
+ 'settings.enabledHint': '关闭后不再挂载右侧文件树与预览列、不显示浮动展开按钮、不注册 /filemgr 路由,也不做工作区监视与 git 轮询;聚合包其余插件不受影响。',
98
+ 'settings.overridden': '已覆盖',
99
+ 'settings.reset': '恢复默认',
100
+ 'settings.notExposed': '当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置,或为 dsh-host-apiproxy 的 WEB_SETTINGS_NAMESPACES 白名单补充本命名空间后重启。',
101
+ 'settings.readOnly': '当前部署的设置只读。',
102
+ 'settings.inherit': '继承',
103
+ 'settings.on': '开',
104
+ 'settings.off': '关',
105
+ 'settings.expand': '展开设置',
106
+ 'settings.collapse': '收起设置',
107
+ 'settings.save': '保存',
108
+ 'settings.saving': '保存中…',
109
+ 'settings.discard': '放弃',
110
+ 'settings.unsaved': '未保存',
111
+ 'settings.saveFailed': '部署未接受这些值,已保留供你修改。',
112
+ 'settings.invalidNumber': '请输入数字,留空则使用默认值。',
113
+ } as const
114
+
115
+ const en: Record<keyof typeof zh, string> = {
116
+ 'explorer.tabs.files': 'Files',
117
+ 'explorer.tabs.changes': 'Changes',
118
+ 'explorer.search.placeholder': 'Search file names',
119
+ 'explorer.search.searching': 'Searching…',
120
+ 'explorer.search.empty': 'No matching files',
121
+ 'explorer.search.error': 'Search failed',
122
+ 'explorer.search.truncated': 'Too many results, showing first {count}',
123
+ 'explorer.tree.empty': 'The project is empty',
124
+ 'explorer.collapse': 'Collapse panel',
125
+ 'explorer.expand': 'Expand panel',
126
+ 'explorer.maximize': 'Maximize files panel',
127
+ 'explorer.restore': 'Restore panel',
128
+ 'explorer.openPreview': 'Open preview',
129
+ 'explorer.drag.dropHint': 'Release to insert the file path',
130
+ 'scm.repositories': 'Repositories',
131
+ 'scm.changes': 'Changes',
132
+ 'scm.staged': 'Staged',
133
+ 'scm.unstaged': 'Changes',
134
+ 'scm.untracked': 'Untracked',
135
+ 'scm.conflicted': 'Conflict',
136
+ 'scm.stage': 'Stage',
137
+ 'scm.unstage': 'Unstage',
138
+ 'scm.discard': 'Discard',
139
+ 'scm.stageAll': 'Stage all',
140
+ 'scm.discardAll': 'Discard all',
141
+ 'scm.empty': 'No changes',
142
+ 'scm.notRepo': 'Not a git repository',
143
+ 'scm.gitMissing': 'Git is not installed. Install git and reload to use the changes panel',
144
+ 'scm.loading': 'Loading status…',
145
+ 'scm.failed': 'Operation failed',
146
+ 'scm.viewList': 'List view',
147
+ 'scm.viewTree': 'Tree view',
148
+ 'scm.discardConfirmTracked': 'Discard changes in {count} files? This cannot be undone.',
149
+ 'scm.discardConfirmUntracked': 'Delete {count} untracked files? This cannot be undone.',
150
+ 'preview.noTabs': 'No open previews',
151
+ 'preview.newUrlTab': 'New URL preview',
152
+ 'preview.collapsePanel': 'Collapse preview panel',
153
+ 'preview.maximize': 'Maximize preview panel',
154
+ 'preview.restore': 'Restore panel',
155
+ 'preview.source': 'Source',
156
+ 'preview.preview': 'Preview',
157
+ 'preview.editor': 'Editor',
158
+ 'preview.split': 'Split',
159
+ 'preview.refresh': 'Refresh',
160
+ 'preview.refresh.updated': 'File updated on disk',
161
+ 'preview.save': 'Save',
162
+ 'preview.download': 'Download',
163
+ 'preview.copyCode': 'Copy code',
164
+ 'preview.copyCodeDone': 'Copied',
165
+ 'preview.openExternal': 'Open in system app',
166
+ 'preview.dirty': 'Unsaved changes',
167
+ 'preview.closeLeft': 'Close left',
168
+ 'preview.closeRight': 'Close right',
169
+ 'preview.closeOthers': 'Close others',
170
+ 'preview.closeAll': 'Close all',
171
+ 'preview.closeConfirmTitle': 'Close unsaved tabs',
172
+ 'preview.closeConfirmBody': '{count} tabs have unsaved changes. Closing will lose them.',
173
+ 'preview.saved': 'Saved',
174
+ 'preview.saveConflict': 'File changed on disk. Save conflict: refresh and retry',
175
+ 'preview.errorOversized': 'File too large, only the first 80,000 characters loaded',
176
+ 'preview.unsupported': 'Preview not supported for this format',
177
+ 'preview.downloadHint': 'Open in a system app or download to view',
178
+ 'preview.url.placeholder': 'Enter a URL and press Enter',
179
+ 'preview.url.hint': 'Press Esc to revert',
180
+ 'common.cancel': 'Cancel',
181
+ 'common.confirm': 'OK',
182
+ 'common.close': 'Close',
183
+ 'common.delete': 'Delete',
184
+ 'common.copyPath': 'Copy path',
185
+ 'common.copied': 'Copied',
186
+ 'explorer.menu.copyPath': 'Copy path',
187
+ 'explorer.menu.copyName': 'Copy name',
188
+ 'explorer.menu.reveal': 'Reveal in file manager',
189
+ 'explorer.menu.openWithDefault': 'Open with default app',
190
+ 'explorer.menu.rename': 'Rename',
191
+ 'explorer.menu.newFile': 'New file',
192
+ 'explorer.menu.newFolder': 'New folder',
193
+ 'explorer.menu.delete': 'Delete',
194
+ 'explorer.rename.title': 'Rename',
195
+ 'explorer.newFile.title': 'New file',
196
+ 'explorer.newFolder.title': 'New folder',
197
+ 'explorer.deleteConfirmTitle': 'Confirm delete',
198
+ 'explorer.deleteConfirmBody': 'Delete "{name}"? This cannot be undone.',
199
+ 'explorer.opFailed': 'Operation failed',
200
+ 'explorer.refToInput': 'Reference to input',
201
+ 'settings.title': 'Right panel',
202
+ 'settings.description': 'Master switch for the Explorer / Preview and SCM changes panels.',
203
+ 'settings.enabled': 'Enable the right panel',
204
+ 'settings.enabledHint': 'When off, the Explorer and Preview columns are not mounted, the floating expand button disappears, the /filemgr routes are not registered, and workspace watching and git polling stop. The rest of the family bundle is unaffected.',
205
+ 'settings.overridden': 'Overridden',
206
+ 'settings.reset': 'Reset',
207
+ 'settings.notExposed': 'This DSH version does not expose this plugin\'s settings namespace; the form is unavailable. Edit ~/.dsh/settings.yaml directly, or add the namespace to the WEB_SETTINGS_NAMESPACES allowlist of dsh-host-apiproxy and restart.',
208
+ 'settings.readOnly': 'Settings are read-only in this deployment.',
209
+ 'settings.inherit': 'Inherit',
210
+ 'settings.on': 'On',
211
+ 'settings.off': 'Off',
212
+ 'settings.expand': 'Expand settings',
213
+ 'settings.collapse': 'Collapse settings',
214
+ 'settings.save': 'Save',
215
+ 'settings.saving': 'Saving…',
216
+ 'settings.discard': 'Discard',
217
+ 'settings.unsaved': 'Unsaved',
218
+ 'settings.saveFailed': 'The deployment rejected these values; your edits are kept.',
219
+ 'settings.invalidNumber': 'Enter a number, or leave it empty to use the default.',
220
+ }
221
+
222
+ export type FileManagerPanelKey = keyof typeof zh
223
+
224
+ /** The dictionary namespace this plugin owns. */
225
+ export const NS = 'filemgr'
226
+
227
+ /** Format one copy string with {name} placeholders. */
228
+ export function format(template: string, params: Record<string, string | number>): string {
229
+ return template.replace(/\{(\w+)\}/g, (_, key: string) => String(params[key] ?? `{${key}}`))
230
+ }
231
+
232
+ /** Simple dictionary access (zh/en by a global flag the client sets). */
233
+ export const dictionaries: Record<'zh' | 'en', Record<FileManagerPanelKey, string>> = { zh, en }
234
+
235
+ let currentLanguage: 'zh' | 'en' = 'zh'
236
+
237
+ /** Set the active language (the client mirrors the locale service). */
238
+ export function setLanguage(language: string): void {
239
+ currentLanguage = language === 'en' ? 'en' : 'zh'
240
+ }
241
+
242
+ /** Translate one key with optional params. */
243
+ export function t(key: FileManagerPanelKey, params?: Record<string, string | number>): string {
244
+ const table = dictionaries[currentLanguage] ?? zh
245
+ const template = table[key] ?? zh[key]
246
+ return params === undefined ? template : format(template, params)
247
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Maximize-mode geometry (issue #315): when a panel is maximized the layout
3
+ * controller rewrites the frame grid so the target column takes the whole
4
+ * row (sidebar, details, chat and the other panel collapse to 0px tracks —
5
+ * everything stays mounted, nothing unmounts). On narrow viewports the
6
+ * maximized column instead leaves the grid and renders as a fixed
7
+ * full-screen overlay (mobile-friendly full-screen mode), so the shell's
8
+ * own narrow layout stays untouched behind it.
9
+ * @module dsh-filemgr/client/maximize
10
+ */
11
+
12
+ import type { MaximizeTarget } from './store.ts'
13
+
14
+ /** Below this available row width the maximized panel renders as a fixed
15
+ * full-screen overlay instead of a grid takeover. */
16
+ export const MAXIMIZE_OVERLAY_BREAKPOINT_PX = 640
17
+
18
+ /**
19
+ * The five grid tracks while one panel is maximized: shell sidebar, chat,
20
+ * shell details and the other panel all collapse to 0px; the target column
21
+ * takes the whole measured frame width. Tracks are the same string shape the
22
+ * shell's own inline style uses (px + fr), so nothing else needs to change.
23
+ */
24
+ export function maximizedGridTracks(target: MaximizeTarget, frameWidth: number): string {
25
+ const wide = `${Math.max(0, Math.round(frameWidth))}px`
26
+ return target === 'explorer'
27
+ ? `0px 0px 0px 0px ${wide}`
28
+ : `0px 0px 0px ${wide} 0px`
29
+ }
30
+
31
+ /** Whether the maximized panel should render as a fixed full-screen overlay. */
32
+ export function maximizedOverlay(availableWidth: number): boolean {
33
+ return availableWidth > 0 && availableWidth < MAXIMIZE_OVERLAY_BREAKPOINT_PX
34
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * DOM mounting: two React roots rendered into the panel columns the layout
3
+ * controller appends to the frame grid. The roots wait for their columns
4
+ * (the shell mounts asynchronously), and everything is wrapped so a DOM
5
+ * failure degrades the panels, never the GUI boot.
6
+ * @module dsh-filemgr/client/mount
7
+ */
8
+
9
+ import { createRoot, type Root } from 'react-dom/client'
10
+ import type { PanelStores } from './store.ts'
11
+ import { ExplorerPanel } from './components/ExplorerPanel.tsx'
12
+ import { PreviewPanel } from './preview/PreviewPanel.tsx'
13
+ import type { FileReferenceSelection } from './preview/selection.ts'
14
+
15
+ const EXPLORER_COL_SELECTOR = '[data-filemgr-explorer-col]'
16
+ const PREVIEW_COL_SELECTOR = '[data-filemgr-preview-col]'
17
+
18
+ /** Wait for one selector (the shell/frame mounts after boot settlement). */
19
+ function waitForElement(selector: string, onFound: (el: HTMLElement) => void): () => void {
20
+ let disposed = false
21
+ let observer: MutationObserver | undefined
22
+ const tryFind = (): void => {
23
+ if (disposed) return
24
+ const el = document.querySelector<HTMLElement>(selector)
25
+ if (el !== null) {
26
+ observer?.disconnect()
27
+ onFound(el)
28
+ }
29
+ }
30
+ observer = new MutationObserver(() => { tryFind() })
31
+ observer.observe(document.body, { childList: true, subtree: true })
32
+ tryFind()
33
+ return () => {
34
+ disposed = true
35
+ observer?.disconnect()
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Mount both panel roots.
41
+ * @param stores - the panel store bundle.
42
+ * @param onToggleExplorer - collapse toggle (owned by the layout controller).
43
+ * @param onReference - insert a file-reference chip into the draft for a file
44
+ * (the Explorer row @-reference button); folders should call a different
45
+ * helper because the chip has no line concept for them.
46
+ * @param onAddFileReference - insert a file-reference chip into the draft for
47
+ * a preview selection (path + start/end line).
48
+ * @returns a disposer unmounting both trees.
49
+ */
50
+ export function mountPanels(
51
+ stores: PanelStores,
52
+ onToggleExplorer: () => void,
53
+ onReference: (path: string, isDir: boolean) => void,
54
+ onAddFileReference: (selection: FileReferenceSelection) => boolean,
55
+ ): () => void {
56
+ let explorerRoot: Root | undefined
57
+ let previewRoot: Root | undefined
58
+ const disposers: Array<() => void> = []
59
+
60
+ disposers.push(waitForElement(EXPLORER_COL_SELECTOR, (el) => {
61
+ explorerRoot = createRoot(el)
62
+ explorerRoot.render(<ExplorerPanel stores={stores} onToggleCollapse={onToggleExplorer} onReference={onReference} />)
63
+ }))
64
+ disposers.push(waitForElement(PREVIEW_COL_SELECTOR, (el) => {
65
+ previewRoot = createRoot(el)
66
+ previewRoot.render(<PreviewPanel stores={stores} onAddFileReference={onAddFileReference} />)
67
+ }))
68
+
69
+ return () => {
70
+ for (const dispose of disposers) dispose()
71
+ explorerRoot?.unmount()
72
+ previewRoot?.unmount()
73
+ }
74
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Persistence helpers for panel preferences: range-validated reads (invalid
3
+ * stored values fall back to defaults — a broken or hand-edited value must
4
+ * never produce a 0px or NaN panel), debounced writes, and the LRU registry
5
+ * for preview scopes (at most 12 scopes; the oldest savedAt evicts).
6
+ *
7
+ * Keys follow the FileManager contract verbatim:
8
+ * chat-workspace-width-px, chat-preview-width-px, preview-panel-split-ratio,
9
+ * project-panel-collapse:<root>, explorer-ui:<root>, scm-ui:<root>,
10
+ * preview-ui:<root>.
11
+ * @module dsh-filemgr/client/persist
12
+ */
13
+
14
+ /** Read a stored number, validating it against [min, max]; fallback otherwise. */
15
+ export function readStoredNumber(key: string, min: number, max: number, fallback: number): number {
16
+ try {
17
+ const raw = localStorage.getItem(key)
18
+ if (raw === null) return fallback
19
+ const value = Number(raw)
20
+ if (!Number.isFinite(value)) return fallback
21
+ if (value < min || value > max) return fallback
22
+ return value
23
+ } catch {
24
+ return fallback
25
+ }
26
+ }
27
+
28
+ /** Write a number if it differs from the stored value (avoids churn). */
29
+ export function writeStoredNumber(key: string, value: number): void {
30
+ try {
31
+ const raw = String(Math.round(value))
32
+ if (localStorage.getItem(key) === raw) return
33
+ localStorage.setItem(key, raw)
34
+ } catch {
35
+ // persistence is best-effort; the panel still works
36
+ }
37
+ }
38
+
39
+ /**
40
+ * A single debounce pipeline used by the stores for its search and persist
41
+ * timers: coalesces rapid schedules into one trailing run (the latest fn wins).
42
+ * `flush` runs the pending fn immediately (pagehide/beforeunload), `dispose`
43
+ * cancels a pending schedule. Behavior is equivalent to the stores' previous
44
+ * hand-rolled setTimeout + clearTimeout pairs, just centralized.
45
+ */
46
+ export interface Debounced {
47
+ /** Queue a fn; repeated calls before the delay replaces the pending fn. */
48
+ schedule: (fn: () => void) => void
49
+ /** Run the pending fn now and clear the timer. */
50
+ flush: () => void
51
+ /** Cancel the pending fn and timer. */
52
+ dispose: () => void
53
+ }
54
+
55
+ /** Create one debounced scheduler (default 150ms). */
56
+ export function createDebounced(delayMs = 150): Debounced {
57
+ let timer: ReturnType<typeof setTimeout> | undefined
58
+ let pending: (() => void) | null = null
59
+ const flush = (): void => {
60
+ if (timer !== undefined) clearTimeout(timer)
61
+ timer = undefined
62
+ const fn = pending
63
+ pending = null
64
+ if (fn !== null) fn()
65
+ }
66
+ return {
67
+ schedule(fn: () => void) {
68
+ pending = fn
69
+ // Reset on every schedule so the run trails the LAST change (the same
70
+ // trailing-edge semantics the stores' former clear+setTimeout had).
71
+ if (timer !== undefined) clearTimeout(timer)
72
+ timer = setTimeout(flush, delayMs)
73
+ },
74
+ flush,
75
+ dispose() {
76
+ if (timer !== undefined) clearTimeout(timer)
77
+ timer = undefined
78
+ pending = null
79
+ },
80
+ }
81
+ }
82
+
83
+ /** The preview-ui scope registry: keys, savedAt values, eviction. */
84
+ export const PREVIEW_SCOPE_PREFIX = 'preview-ui:'
85
+ /** LRU cap on distinct preview scopes. */
86
+ export const PREVIEW_SCOPE_CAP = 12
87
+
88
+ /**
89
+ * Collect every stored key under a prefix. localStorage has no prefix index,
90
+ * so the whole store is swept once, then filtered to the package's own keys
91
+ * — enumeration is never interleaved with removal (removals would shift the
92
+ * indices mid-loop and skip entries).
93
+ */
94
+ function listStoredKeysByPrefix(prefix: string): string[] {
95
+ const keys: string[] = []
96
+ try {
97
+ for (let i = 0; i < localStorage.length; i += 1) {
98
+ const key = localStorage.key(i)
99
+ if (key !== null && key.startsWith(prefix)) keys.push(key)
100
+ }
101
+ } catch {
102
+ return []
103
+ }
104
+ return keys
105
+ }
106
+
107
+ /**
108
+ * Precisely delete every stored key under a prefix. Only this package's own
109
+ * prefixed keys are removed — foreign-application keys are never touched,
110
+ * replacing the former all-at-once `localStorage.clear()` sweep.
111
+ */
112
+ export function removeStoredByPrefix(prefix: string): number {
113
+ const keys = listStoredKeysByPrefix(prefix)
114
+ let removed = 0
115
+ for (const key of keys) {
116
+ try {
117
+ localStorage.removeItem(key)
118
+ removed += 1
119
+ } catch {
120
+ // best-effort; a storage failure does not abort the rest
121
+ }
122
+ }
123
+ return removed
124
+ }
125
+
126
+ /** All stored preview scopes with their savedAt timestamps, oldest first. */
127
+ export function listPreviewScopes(): Array<{ root: string; savedAt: number }> {
128
+ const out: Array<{ root: string; savedAt: number }> = []
129
+ for (const key of listStoredKeysByPrefix(PREVIEW_SCOPE_PREFIX)) {
130
+ const root = key.slice(PREVIEW_SCOPE_PREFIX.length)
131
+ let savedAt = 0
132
+ try {
133
+ const raw = localStorage.getItem(key)
134
+ if (raw !== null) {
135
+ const parsed = JSON.parse(raw) as { savedAt?: unknown }
136
+ if (typeof parsed.savedAt === 'number') savedAt = parsed.savedAt
137
+ }
138
+ } catch {
139
+ savedAt = 0
140
+ }
141
+ out.push({ root, savedAt })
142
+ }
143
+ out.sort((a, b) => a.savedAt - b.savedAt)
144
+ return out
145
+ }
146
+
147
+ /** Evict the oldest scopes beyond the cap. */
148
+ export function evictPreviewScopes(keep: string): void {
149
+ // Cheap pre-check first: enumerate keys only (no JSON.parse, no sort).
150
+ // Writes far below the cap — the common case — return here instead of
151
+ // parsing and sorting every stored preview scope on each persist.
152
+ if (listStoredKeysByPrefix(PREVIEW_SCOPE_PREFIX).length <= PREVIEW_SCOPE_CAP) return
153
+ const scopes = listPreviewScopes().filter((scope) => scope.root !== keep)
154
+ let excess = scopes.length - (PREVIEW_SCOPE_CAP - 1)
155
+ for (const scope of scopes) {
156
+ if (excess <= 0) break
157
+ try {
158
+ localStorage.removeItem(`${PREVIEW_SCOPE_PREFIX}${scope.root}`)
159
+ } catch {
160
+ // best-effort
161
+ }
162
+ excess -= 1
163
+ }
164
+ }
165
+
166
+ /** Serialize a JSON value with a size guard (quota failures degrade silently). */
167
+ export function writeJson(key: string, value: unknown): boolean {
168
+ try {
169
+ localStorage.setItem(key, JSON.stringify(value))
170
+ return true
171
+ } catch {
172
+ try {
173
+ localStorage.removeItem(key)
174
+ } catch {
175
+ // storage unavailable entirely
176
+ }
177
+ return false
178
+ }
179
+ }
180
+
181
+ /** Parse a stored JSON value; fallback on any failure. */
182
+ export function readJson<T>(key: string, fallback: T): T {
183
+ try {
184
+ const raw = localStorage.getItem(key)
185
+ if (raw === null) return fallback
186
+ const parsed = JSON.parse(raw) as unknown
187
+ // JSON.parse('null') yields null without throwing; descending callers
188
+ // expect an object and would throw on null/primitive, killing their effect.
189
+ if (parsed === null || typeof parsed !== 'object') return fallback
190
+ return parsed as T
191
+ } catch {
192
+ return fallback
193
+ }
194
+ }