dsh-plugin-core 0.20.4 → 0.20.6

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 (142) hide show
  1. package/dist/design/tokens-css.generated.d.ts +1 -1
  2. package/dist/design/tokens-css.generated.d.ts.map +1 -1
  3. package/dist/design/tokens-css.generated.js +1 -1
  4. package/dist/design/tokens-css.generated.js.map +1 -1
  5. package/dist/explorer/FileBrowser.d.ts +13 -6
  6. package/dist/explorer/FileBrowser.d.ts.map +1 -1
  7. package/dist/explorer/FileBrowser.js +190 -305
  8. package/dist/explorer/FileBrowser.js.map +1 -1
  9. package/dist/explorer/crumb-nav.d.ts +45 -0
  10. package/dist/explorer/crumb-nav.d.ts.map +1 -0
  11. package/dist/explorer/crumb-nav.js +75 -0
  12. package/dist/explorer/crumb-nav.js.map +1 -0
  13. package/dist/explorer/crumbs-menu.d.ts +37 -0
  14. package/dist/explorer/crumbs-menu.d.ts.map +1 -0
  15. package/dist/explorer/crumbs-menu.js +102 -0
  16. package/dist/explorer/crumbs-menu.js.map +1 -0
  17. package/dist/explorer/explorer-css.generated.d.ts +1 -1
  18. package/dist/explorer/explorer-css.generated.d.ts.map +1 -1
  19. package/dist/explorer/explorer-css.generated.js +1 -1
  20. package/dist/explorer/explorer-css.generated.js.map +1 -1
  21. package/dist/explorer/fs-face.d.ts +36 -0
  22. package/dist/explorer/fs-face.d.ts.map +1 -0
  23. package/dist/explorer/fs-face.js +74 -0
  24. package/dist/explorer/fs-face.js.map +1 -0
  25. package/dist/explorer/index.d.ts +2 -1
  26. package/dist/explorer/index.d.ts.map +1 -1
  27. package/dist/explorer/index.js +2 -1
  28. package/dist/explorer/index.js.map +1 -1
  29. package/dist/explorer/landing.d.ts +26 -0
  30. package/dist/explorer/landing.d.ts.map +1 -0
  31. package/dist/explorer/landing.js +43 -0
  32. package/dist/explorer/landing.js.map +1 -0
  33. package/dist/explorer/open.d.ts +35 -38
  34. package/dist/explorer/open.d.ts.map +1 -1
  35. package/dist/explorer/open.js +124 -156
  36. package/dist/explorer/open.js.map +1 -1
  37. package/dist/explorer/preview/BytesPreview.d.ts +2 -3
  38. package/dist/explorer/preview/BytesPreview.d.ts.map +1 -1
  39. package/dist/explorer/preview/BytesPreview.js +3 -3
  40. package/dist/explorer/preview/BytesPreview.js.map +1 -1
  41. package/dist/explorer/preview/HtmlPreview.d.ts +2 -3
  42. package/dist/explorer/preview/HtmlPreview.d.ts.map +1 -1
  43. package/dist/explorer/preview/HtmlPreview.js +4 -5
  44. package/dist/explorer/preview/HtmlPreview.js.map +1 -1
  45. package/dist/explorer/preview/OfficePreview.d.ts +2 -1
  46. package/dist/explorer/preview/OfficePreview.d.ts.map +1 -1
  47. package/dist/explorer/preview/OfficePreview.js +9 -2
  48. package/dist/explorer/preview/OfficePreview.js.map +1 -1
  49. package/dist/explorer/preview/TextPreview.d.ts +2 -3
  50. package/dist/explorer/preview/TextPreview.d.ts.map +1 -1
  51. package/dist/explorer/preview/TextPreview.js +3 -3
  52. package/dist/explorer/preview/TextPreview.js.map +1 -1
  53. package/dist/explorer/preview/index.d.ts +6 -3
  54. package/dist/explorer/preview/index.d.ts.map +1 -1
  55. package/dist/explorer/preview/index.js +3 -6
  56. package/dist/explorer/preview/index.js.map +1 -1
  57. package/dist/explorer/preview-title-bar.d.ts +26 -0
  58. package/dist/explorer/preview-title-bar.d.ts.map +1 -0
  59. package/dist/explorer/preview-title-bar.js +48 -0
  60. package/dist/explorer/preview-title-bar.js.map +1 -0
  61. package/dist/explorer/types.d.ts +25 -23
  62. package/dist/explorer/types.d.ts.map +1 -1
  63. package/dist/explorer/types.js +61 -120
  64. package/dist/explorer/types.js.map +1 -1
  65. package/dist/host/fs-route.d.ts +25 -0
  66. package/dist/host/fs-route.d.ts.map +1 -0
  67. package/dist/host/fs-route.js +243 -0
  68. package/dist/host/fs-route.js.map +1 -0
  69. package/dist/host/index.d.ts +2 -0
  70. package/dist/host/index.d.ts.map +1 -1
  71. package/dist/host/index.js +1 -0
  72. package/dist/host/index.js.map +1 -1
  73. package/dist/session/session-css.generated.d.ts +1 -1
  74. package/dist/session/session-css.generated.d.ts.map +1 -1
  75. package/dist/session/session-css.generated.js +1 -1
  76. package/dist/session/session-css.generated.js.map +1 -1
  77. package/dist/ui/DateTimePicker.d.ts.map +1 -1
  78. package/dist/ui/DateTimePicker.js +5 -1
  79. package/dist/ui/DateTimePicker.js.map +1 -1
  80. package/dist/ui/Modal.d.ts.map +1 -1
  81. package/dist/ui/Modal.js +7 -2
  82. package/dist/ui/Modal.js.map +1 -1
  83. package/dist/ui/Picker.d.ts.map +1 -1
  84. package/dist/ui/Picker.js +6 -3
  85. package/dist/ui/Picker.js.map +1 -1
  86. package/dist/ui/controls/datetime-css.generated.d.ts +1 -1
  87. package/dist/ui/controls/datetime-css.generated.d.ts.map +1 -1
  88. package/dist/ui/controls/datetime-css.generated.js +1 -1
  89. package/dist/ui/controls/datetime-css.generated.js.map +1 -1
  90. package/dist/ui/controls/field-css.generated.d.ts +1 -1
  91. package/dist/ui/controls/field-css.generated.d.ts.map +1 -1
  92. package/dist/ui/controls/field-css.generated.js +1 -1
  93. package/dist/ui/controls/field-css.generated.js.map +1 -1
  94. package/dist/ui/controls/loading-css.generated.d.ts +1 -1
  95. package/dist/ui/controls/loading-css.generated.d.ts.map +1 -1
  96. package/dist/ui/controls/loading-css.generated.js +1 -1
  97. package/dist/ui/controls/loading-css.generated.js.map +1 -1
  98. package/dist/ui/feedback-css.generated.d.ts +1 -1
  99. package/dist/ui/feedback-css.generated.d.ts.map +1 -1
  100. package/dist/ui/feedback-css.generated.js +1 -1
  101. package/dist/ui/feedback-css.generated.js.map +1 -1
  102. package/dist/ui/marquee-css.generated.d.ts +1 -1
  103. package/dist/ui/marquee-css.generated.d.ts.map +1 -1
  104. package/dist/ui/marquee-css.generated.js +1 -1
  105. package/dist/ui/marquee-css.generated.js.map +1 -1
  106. package/dist/ui/overlay-css.generated.d.ts +1 -1
  107. package/dist/ui/overlay-css.generated.d.ts.map +1 -1
  108. package/dist/ui/overlay-css.generated.js +1 -1
  109. package/dist/ui/overlay-css.generated.js.map +1 -1
  110. package/dist/ui/overlay.d.ts +8 -2
  111. package/dist/ui/overlay.d.ts.map +1 -1
  112. package/dist/ui/overlay.js +7 -6
  113. package/dist/ui/overlay.js.map +1 -1
  114. package/dist/ui/panel-css.d.ts +2 -0
  115. package/dist/ui/panel-css.d.ts.map +1 -0
  116. package/dist/ui/panel-css.generated.d.ts +2 -0
  117. package/dist/ui/panel-css.generated.d.ts.map +1 -0
  118. package/dist/ui/panel-css.generated.js +4 -0
  119. package/dist/ui/panel-css.generated.js.map +1 -0
  120. package/dist/ui/panel-css.js +2 -0
  121. package/dist/ui/panel-css.js.map +1 -0
  122. package/dist/ui/panel.d.ts +41 -0
  123. package/dist/ui/panel.d.ts.map +1 -0
  124. package/dist/ui/panel.js +37 -0
  125. package/dist/ui/panel.js.map +1 -0
  126. package/dist/ui/picker-css.generated.d.ts +1 -1
  127. package/dist/ui/picker-css.generated.d.ts.map +1 -1
  128. package/dist/ui/picker-css.generated.js +1 -1
  129. package/dist/ui/picker-css.generated.js.map +1 -1
  130. package/dist/utils/i18n.d.ts +1 -1
  131. package/dist/utils/i18n.d.ts.map +1 -1
  132. package/dist/utils/i18n.js +6 -0
  133. package/dist/utils/i18n.js.map +1 -1
  134. package/docs/design/architecture.md +9 -4
  135. package/docs/design/external/dsh-host.md +60 -0
  136. package/docs/design/style.md +26 -1
  137. package/docs/help/explorer.doc.ts +35 -30
  138. package/docs/help/explorer.md +23 -19
  139. package/docs/help/integration.doc.ts +15 -10
  140. package/docs/help/integration.md +15 -10
  141. package/docs/requirements.md +2 -1
  142. package/package.json +1 -1
@@ -172,7 +172,8 @@ border-right: 1px solid transparent;
172
172
  缝 + 圆角)当场被否:用户要的就是「让出 1 像素」,不是换一套滚动条外观。所以口径只有一条 ——
173
173
  让位量固定 1px,颜色 / 粗细 / 圆角全部归浏览器。
174
174
 
175
- - 已有实色边框的容器(如 Picker 菜单的 `border:1px solid`)滚动条贴的就是那条线,算已让位,**不要叠加**。
175
+ - 已有实色边框的容器(如 Picker 菜单的 `border:1px solid`,或挂 `.dshpc-panel` 后由面板提供的边框)
176
+ 滚动条贴的就是那条线,算已让位,**不要叠加**。
176
177
  - 消费方自己的滚动容器(主区 / 侧栏)不在本仓代码里,配方相同:自己加那条 `border-right`。
177
178
  2026-11-02 起 `--dshpc-dock-gap` 已删,dock 不再替主区让 margin,改由主区容器自己让。
178
179
  - 自检:`test/quality.test.mjs` 扫 `src/**/*.css`,滚动规则缺边框即红。
@@ -185,6 +186,30 @@ a `::-webkit-scrollbar` thumb (3px gap + radius) was tried on 2026-11-02 and rej
185
186
  Containers that already have a solid border count as done. Consumer-owned scroll containers use the same
186
187
  one-liner; `--dshpc-dock-gap` was removed on 2026-11-02. A test enforces the border.
187
188
 
189
+ ## 浮层三档:模态 / 锚点面板 / 菜单
190
+
191
+ > EN: Three tiers for floating surfaces — modal / popover / menu
192
+
193
+ 所有「浮起来的框」的**表面**(底色 · 描边宽 · 圆角 · 阴影)只在 `src/ui/panel.css` 定义,档位只有三个
194
+ (2026-11-03 用户定:样式统一,交互各家自己管):
195
+
196
+ | 档位 | 用在 | 底色 | 描边 | 圆角 | 阴影 |
197
+ |---|---|---|---|---|---|
198
+ | `--modal` | `Modal` / `ConfirmModal` / `AlertModal` | `surface-1` | 3px | `radius-xl` | `shadow-2` |
199
+ | `--popover` | 选文件弹窗(`pickWorkspaceFile`) | `surface-1` | 1px | `radius-lg` | `shadow-2` |
200
+ | `--menu` | ▾ 选层、`Picker` 下拉 | `surface-menu` | 1px | `radius-sm` | `shadow-1` |
201
+
202
+ - 组件里**不许**再写这四个值;要改外观只改 `panel.css`。
203
+ - **定位不归档位管**:Modal 居中、选文件弹窗贴锚点并上下翻转、菜单挂触发器下方,各自实现。
204
+ - 关闭钮只有一枚 `.dshpc-panel__close`;旧类名 `.dshpc-overlay-head__close` 保留作别名(消费方覆盖不失效)。
205
+ - 层序只许用 `--dshpc-z-panel`(面板本体)/ `--dshpc-z-popmenu`(叠在面板上的菜单与其透明遮罩),不许写字面量。
206
+ - **不并入三档**:`PanelDock` 与 `SideSheet` 属贴边家族(`side-panel.css` 的 `.dshpc-side-surface`,
207
+ 2026-11-02 拍板两者皮肤一模一样),一个是布局成员、一个是贴边抽屉,都不是浮层。
208
+ - 组件**内部**的微层序(缩略按钮 5、拖拽条 7、dock 关闭钮 8、Segmented 的 0/1)不走 token ——
209
+ 它们只在各自 stacking context 里排序,跟页面浮层层级无关。
210
+ - 自检:`test/css-discipline.test.mjs` 盯「色值只进 `tokens.css`」「`--dshpc-panel-*` 只在 `panel.css`」;
211
+ `test/css-contract.test.mjs` 盯「一个类名一套皮肤」。
212
+
188
213
  ## 三条硬规矩
189
214
 
190
215
  > EN: Three hard rules
@@ -1,20 +1,25 @@
1
1
  // explorer 子包的使用帮助 —— 真源(TS 数据模块)。内容全在此一处,demo 与 md 都从这份派生。
2
2
  import type { DocTopic } from '../design/doc-format.js'
3
3
 
4
- const explorerUsage = `import { openFilePreview, pickWorkspaceFile } from 'dsh-plugin-core/explorer'
4
+ const explorerUsage = `import { connectFs, openFilePreview, pickWorkspaceFile } from 'dsh-plugin-core/explorer'
5
+ import { serveFsRoute } from 'dsh-plugin-core/host'
6
+ import { FS_PATH } from './paths' // 你自己的常量:宿主与 client 必须用同一个
5
7
 
6
- // 只在 client apply 里接线一次(接工作区快照 + 读文件服务),之后不用再碰服务
8
+ // ── 宿主半边(apply):挂一条只读文件路由(不限工作区;读文件/列目录都在它上面) ──
9
+ serveFsRoute(ctx, { path: FS_PATH })
10
+
11
+ // ── client 半边(apply):接一次(connectHost 照旧负责工作区快照) ──
7
12
  connectHost(ctx)
13
+ connectFs({ path: FS_PATH })
8
14
 
9
15
  // 右侧分栏预览(占宽度、挤窄主区):一个容器 + 一个路径就够
10
- openFilePreview({ mount, path: '/abs/ledger' }) // 文件夹 ⇒ 目录树;文件 ⇒ 直接预览
11
- openFilePreview({ mount, path: 'docs/README.md' }) // 工作区相对路径也行
12
-
13
- // 覆盖项:指定工作区快照 / 指定会话 / Office 转 PDF / 与别处共用的根缓存
14
- openFilePreview({ mount, path, workspaceAnchors, sessionId, officeToPdf, rootCache })
16
+ openFilePreview({ mount, path: '/abs/ledger' }) // 文件夹 ⇒ 目录树;文件 ⇒ 直接预览
17
+ openFilePreview({ mount, path: 'docs/README.md' }) // 工作区相对路径也行(相对谁见下)
18
+ openFilePreview({ mount }) // 不给路径 ⇒ 根节点(工作区清单 + 盘符)
15
19
 
16
- // 锚定弹窗选文件(Promise 返回路径,不自己挂 dock)
17
- const r = await pickWorkspaceFile({ anchor, filter: { exts: ['md'] } })`
20
+ // 锚定弹窗选文件(Promise 返回**绝对路径**,不自己挂 dock)
21
+ const r = await pickWorkspaceFile({ anchor, filter: { exts: ['md'] } })
22
+ const dir = await pickWorkspaceFile({ anchor, filter: { dirs: true } }) // 只选文件夹`
18
23
 
19
24
  const doc: DocTopic = {
20
25
  title: { zh: 'explorer —— 工作区浏览', en: 'explorer — Workspace browsing' },
@@ -25,20 +30,20 @@ const doc: DocTopic = {
25
30
  heading: { zh: '概览', en: 'Overview' },
26
31
  prose: [
27
32
  {
28
- zh: '提供工作区文件浏览与选择,数据来自宿主提供的工作区文件快照。',
29
- en: 'Workspace file browsing and picking, driven by the host-provided workspace file snapshot.',
33
+ zh: '提供文件浏览与选择;数据走宿主侧只读 FS 路由,**不限工作区**(工作区之外也能浏览)。界面与原版一致。',
34
+ en: 'File browsing and picking **not limited to workspaces**: one root tree (workspace nodes + the real root), served by a host-side read-only FS route.',
30
35
  },
31
36
  {
32
- zh: '**接线归核心**:client `apply` 里调一次 `connectHost(ctx)`,读文件服务、会话 id、工作区快照都由核心自己解,调用时只给容器和路径。',
33
- en: '**Wiring belongs to core**: call `connectHost(ctx)` once in the client apply; the file service, session id and workspace snapshot are resolved by core — you only pass a container and a path.',
37
+ zh: '**接线归核心,但要两条**:宿主 `apply` 调 `serveFsRoute({ path })`,client `apply` 用同一个常量调 `connectFs({ path })`(`connectHost(ctx)` 照旧负责工作区快照)。浏览**不需要会话**。',
38
+ en: '**Core does the wiring, but with two calls**: `serveFsRoute({ path })` on the host side and `connectFs({ path })` with the same constant on the client side (`connectHost(ctx)` still covers the workspace snapshot). Browsing needs **no session**.',
34
39
  },
35
40
  {
36
41
  zh: '落点只给一个元素:页面里**包住主区**的那个(核心会补成 flex 行、让主区伸展,关闭时还原),不要传 `document.body` 再自己套 fixed——那是浮层,不是分栏。',
37
42
  en: 'Pass one element: the one wrapping the main area. Core makes it a flex row and lets the main area stretch, restoring it on close. Never pass `document.body` with your own fixed positioning — that is an overlay, not a split.',
38
43
  },
39
44
  {
40
- zh: '绝对路径按最长前缀匹配所属工作区;落在任何工作区外时,文件照样能预览(官方 read 允许工作区外文件),只有目录树退化成只读路径栏。',
41
- en: 'Absolute paths are matched to their workspace by longest prefix. Outside every workspace the file still previews (official read allows it); only the directory tree degrades to a read-only path bar.',
45
+ zh: '面包屑口径不变:路径所属工作区名作根名,其下按相对层级展开。▾ 下拉**根层置顶**(其余仍列各工作区):POSIX 是一条「根目录」,**Windows 没有根目录这东西,列的是盘符**(`C:/`、`D:/`…,由宿主 `op:roots` 探出来)。点其他工作区 = 直接列该区根目录(不需要会话)。',
46
+ en: 'Inside a workspace the breadcrumb starts at its name and the ▾ menu nests it there; outside it starts at the real root (`/` or a drive). A workspace root still appears in the real path, tagged “Workspace”.',
42
47
  },
43
48
  ],
44
49
  },
@@ -84,29 +89,23 @@ const doc: DocTopic = {
84
89
  en: 'The element wrapping the main area; no flex needed (core turns it into a flex row, stretches the main area, and restores it on close). Never `document.body`',
85
90
  },
86
91
  },
87
- {
88
- name: 'opts.workspaceFiles',
89
- type: 'WorkspaceFilesFace',
90
- def: '—',
91
- desc: {
92
- zh: '读文件服务。**一般不用传**:`connectHost(ctx)` 接线后核心自己持有;传了以传入为准。传 `content` 时可省',
93
- en: 'File service. **Usually unnecessary**: core holds it after `connectHost(ctx)`; when passed it wins. Optional when passing `content`',
94
- },
95
- },
96
92
  {
97
93
  name: 'opts.sessionId',
98
94
  type: 'string',
99
95
  def: '—',
100
96
  desc: {
101
- zh: '该工作区的真实会话 id。**一般不用传**:按路径匹配所属工作区,匹配不出就取最近有会话的工作区;传 `content` 时可省',
102
- en: 'A real session id. **Usually unnecessary**: resolved from the path’s workspace, else the most recently updated workspace with a session; optional when passing `content`',
97
+ zh: '**只在两种情况下需要**:① 相对路径的基准工作区;② Office 预览(转 PDF 要一条有效会话,缺省由核心取最近有会话的工作区)',
98
+ en: 'Needed in exactly two cases: (1) the base workspace for a relative path; (2) Office preview (conversion needs one valid session; core falls back to the most recent workspace with one)',
103
99
  },
104
100
  },
105
101
  {
106
102
  name: 'opts.path',
107
103
  type: 'string',
108
104
  def: '—',
109
- desc: { zh: '工作区相对路径;绝对路径会自动按最长前缀匹配所属工作区。与 `content` 二选一', en: 'Workspace-relative path; absolute paths are matched to their workspace by longest prefix. Mutually exclusive with `content`' },
105
+ desc: {
106
+ zh: '绝对路径或工作区相对路径(相对路径要有可解析的基准,否则返回 `null` 并 warn);不传 ⇒ 根节点。与 `content` 二选一',
107
+ en: 'Absolute or workspace-relative (a relative one needs a resolvable base, else `null` + a warning); omitted ⇒ the root node. Mutually exclusive with `content`',
108
+ },
110
109
  },
111
110
  {
112
111
  name: 'opts.content',
@@ -128,9 +127,12 @@ const doc: DocTopic = {
128
127
  },
129
128
  {
130
129
  name: 'pickWorkspaceFile(opts)',
131
- type: 'PickWorkspaceFileOptions => Promise<PickedFile>',
130
+ type: 'PickWorkspaceFileOptions => Promise<PickedPath | null>',
132
131
  def: '—',
133
- desc: { zh: '锚定按钮弹出模态选文件,解析出 `{ path, isDir }`', en: 'Anchored modal picker that resolves `{ path, isDir }`' },
132
+ desc: {
133
+ zh: '锚定按钮弹出模态选文件,解析出 `{ path, isDir, workspaceRel?, workspace? }`;`path` 是**绝对路径**',
134
+ en: 'Anchored modal picker resolving `{ path, isDir, workspaceRel?, workspace? }`; `path` is **absolute**',
135
+ },
134
136
  },
135
137
  {
136
138
  name: 'opts.anchor',
@@ -143,7 +145,10 @@ const doc: DocTopic = {
143
145
  name: 'opts.filter',
144
146
  type: 'PickFilter',
145
147
  def: '—',
146
- desc: { zh: '按扩展名过滤 `{ exts: [\'md\'] }`;`dirs: true` 则只允许选文件夹', en: 'Filter by extension `{ exts: [\'md\'] }`; `dirs: true` restricts picking to folders' },
148
+ desc: {
149
+ zh: '`exts` 只要这些扩展名(目录仍可展开导航);`dirs: true` 只允许选文件夹(文件不渲染)',
150
+ en: '`exts` limits files (folders still expand); `dirs: true` picks folders only (files hidden, plus a “Select this folder” action in the crumb bar)',
151
+ },
147
152
  },
148
153
  ],
149
154
  },
@@ -10,21 +10,21 @@
10
10
 
11
11
  > EN: Overview
12
12
 
13
- 提供工作区文件浏览与选择,数据来自宿主提供的工作区文件快照。
13
+ 提供文件浏览与选择;数据走宿主侧只读 FS 路由,**不限工作区**(工作区之外也能浏览)。界面与原版一致。
14
14
 
15
- > EN: Workspace file browsing and picking, driven by the host-provided workspace file snapshot.
15
+ > EN: File browsing and picking **not limited to workspaces**: one root tree (workspace nodes + the real root), served by a host-side read-only FS route.
16
16
 
17
- **接线归核心**:client `apply` 里调一次 `connectHost(ctx)`,读文件服务、会话 id、工作区快照都由核心自己解,调用时只给容器和路径。
17
+ **接线归核心,但要两条**:宿主 `apply` 调 `serveFsRoute({ path })`,client `apply` 用同一个常量调 `connectFs({ path })`(`connectHost(ctx)` 照旧负责工作区快照)。浏览**不需要会话**。
18
18
 
19
- > EN: **Wiring belongs to core**: call `connectHost(ctx)` once in the client apply; the file service, session id and workspace snapshot are resolved by core — you only pass a container and a path.
19
+ > EN: **Core does the wiring, but with two calls**: `serveFsRoute({ path })` on the host side and `connectFs({ path })` with the same constant on the client side (`connectHost(ctx)` still covers the workspace snapshot). Browsing needs **no session**.
20
20
 
21
21
  落点只给一个元素:页面里**包住主区**的那个(核心会补成 flex 行、让主区伸展,关闭时还原),不要传 `document.body` 再自己套 fixed——那是浮层,不是分栏。
22
22
 
23
23
  > EN: Pass one element: the one wrapping the main area. Core makes it a flex row and lets the main area stretch, restoring it on close. Never pass `document.body` with your own fixed positioning — that is an overlay, not a split.
24
24
 
25
- 绝对路径按最长前缀匹配所属工作区;落在任何工作区外时,文件照样能预览(官方 read 允许工作区外文件),只有目录树退化成只读路径栏。
25
+ 面包屑口径不变:路径所属工作区名作根名,其下按相对层级展开。▾ 下拉**根层置顶**(其余仍列各工作区):POSIX 是一条「根目录」,**Windows 没有根目录这东西,列的是盘符**(`C:/`、`D:/`…,由宿主 `op:roots` 探出来)。点其他工作区 = 直接列该区根目录(不需要会话)。
26
26
 
27
- > EN: Absolute paths are matched to their workspace by longest prefix. Outside every workspace the file still previews (official read allows it); only the directory tree degrades to a read-only path bar.
27
+ > EN: Inside a workspace the breadcrumb starts at its name and the ▾ menu nests it there; outside it starts at the real root (`/` or a drive). A workspace root still appears in the real path, tagged “Workspace”.
28
28
 
29
29
  ## 0. 装包
30
30
 
@@ -43,20 +43,25 @@ pnpm add dsh-plugin-core
43
43
  > EN: `connectHost(ctx)` takes just that one slice; the whole ctx never enters core.
44
44
 
45
45
  ```tsx id="explorer-usage-code"
46
- import { openFilePreview, pickWorkspaceFile } from 'dsh-plugin-core/explorer'
46
+ import { connectFs, openFilePreview, pickWorkspaceFile } from 'dsh-plugin-core/explorer'
47
+ import { serveFsRoute } from 'dsh-plugin-core/host'
48
+ import { FS_PATH } from './paths' // 你自己的常量:宿主与 client 必须用同一个
47
49
 
48
- // 只在 client apply 里接线一次(接工作区快照 + 读文件服务),之后不用再碰服务
50
+ // ── 宿主半边(apply):挂一条只读文件路由(不限工作区;读文件/列目录都在它上面) ──
51
+ serveFsRoute(ctx, { path: FS_PATH })
52
+
53
+ // ── client 半边(apply):接一次(connectHost 照旧负责工作区快照) ──
49
54
  connectHost(ctx)
55
+ connectFs({ path: FS_PATH })
50
56
 
51
57
  // 右侧分栏预览(占宽度、挤窄主区):一个容器 + 一个路径就够
52
- openFilePreview({ mount, path: '/abs/ledger' }) // 文件夹 ⇒ 目录树;文件 ⇒ 直接预览
53
- openFilePreview({ mount, path: 'docs/README.md' }) // 工作区相对路径也行
54
-
55
- // 覆盖项:指定工作区快照 / 指定会话 / Office 转 PDF / 与别处共用的根缓存
56
- openFilePreview({ mount, path, workspaceAnchors, sessionId, officeToPdf, rootCache })
58
+ openFilePreview({ mount, path: '/abs/ledger' }) // 文件夹 ⇒ 目录树;文件 ⇒ 直接预览
59
+ openFilePreview({ mount, path: 'docs/README.md' }) // 工作区相对路径也行(相对谁见下)
60
+ openFilePreview({ mount }) // 不给路径 ⇒ 根节点(工作区清单 + 盘符)
57
61
 
58
- // 锚定弹窗选文件(Promise 返回路径,不自己挂 dock)
62
+ // 锚定弹窗选文件(Promise 返回**绝对路径**,不自己挂 dock)
59
63
  const r = await pickWorkspaceFile({ anchor, filter: { exts: ['md'] } })
64
+ const dir = await pickWorkspaceFile({ anchor, filter: { dirs: true } }) // 只选文件夹
60
65
  ```
61
66
 
62
67
  ## API
@@ -67,12 +72,11 @@ const r = await pickWorkspaceFile({ anchor, filter: { exts: ['md'] } })
67
72
  |---|---|---|---|---|---|
68
73
  | `openFilePreview(opts)` | `OpenFilePreviewOptions => FilePreviewHandle` | — | — | 在右侧分栏打开文件 / 文件夹预览,返回可关闭它的句柄。预览内容二选一:`path`(核心解析并渲染)或 `content`(由调用方给内容) | Opens a file/folder preview in the right-side dock and returns a handle to close it. Body is one of two: `path` (resolved and rendered by core) or `content` (rendered from your own node) |
69
74
  | `opts.mount` | `HTMLElement` | 是 | — | 页面里**包住主区**的那个元素;不必自己写 flex(核心补成 flex 行并让主区伸展,关闭时还原)。不要传 `document.body` | The element wrapping the main area; no flex needed (core turns it into a flex row, stretches the main area, and restores it on close). Never `document.body` |
70
- | `opts.workspaceFiles` | `WorkspaceFilesFace` | — | — | 读文件服务。**一般不用传**:`connectHost(ctx)` 接线后核心自己持有;传了以传入为准。传 `content` 时可省 | File service. **Usually unnecessary**: core holds it after `connectHost(ctx)`; when passed it wins. Optional when passing `content` |
71
- | `opts.sessionId` | `string` | — | — | 该工作区的真实会话 id。**一般不用传**:按路径匹配所属工作区,匹配不出就取最近有会话的工作区;传 `content` 时可省 | A real session id. **Usually unnecessary**: resolved from the path’s workspace, else the most recently updated workspace with a session; optional when passing `content` |
72
- | `opts.path` | `string` | — | — | 工作区相对路径;绝对路径会自动按最长前缀匹配所属工作区。与 `content` 二选一 | Workspace-relative path; absolute paths are matched to their workspace by longest prefix. Mutually exclusive with `content` |
75
+ | `opts.sessionId` | `string` | — | — | **只在两种情况下需要**:① 相对路径的基准工作区;② Office 预览(转 PDF 要一条有效会话,缺省由核心取最近有会话的工作区) | Needed in exactly two cases: (1) the base workspace for a relative path; (2) Office preview (conversion needs one valid session; core falls back to the most recent workspace with one) |
76
+ | `opts.path` | `string` | — | — | 绝对路径或工作区相对路径(相对路径要有可解析的基准,否则返回 `null` 并 warn);不传 ⇒ 根节点。与 `content` 二选一 | Absolute or workspace-relative (a relative one needs a resolvable base, else `null` + a warning); omitted ⇒ the root node. Mutually exclusive with `content` |
73
77
  | `opts.content` | `() => ReactNode` | — | — | 自定义预览内容,塞进同一个右侧 dock;传它时不需要 `path` | Custom preview body rendered in the same dock; no `path` needed when set |
74
78
  | `opts.officeToPdf` | `OfficeToPdfFace` | — | — | Office 转 PDF 能力;缺失则该格式走「不支持」而不是报错 | Office→PDF capability; without it that format shows "unsupported" instead of erroring |
75
79
  | `opts.rootName` | `string` | — | — | 兜底显示名(路径匹配不出工作区时用) | Fallback display name, used when no workspace matches the path |
76
- | `pickWorkspaceFile(opts)` | `PickWorkspaceFileOptions => Promise<PickedFile>` | — | — | 锚定按钮弹出模态选文件,解析出 `{ path, isDir }` | Anchored modal picker that resolves `{ path, isDir }` |
80
+ | `pickWorkspaceFile(opts)` | `PickWorkspaceFileOptions => Promise<PickedPath \| null>` | — | — | 锚定按钮弹出模态选文件,解析出 `{ path, isDir, workspaceRel?, workspace? }`;`path` 是**绝对路径** | Anchored modal picker resolving `{ path, isDir, workspaceRel?, workspace? }`; `path` is **absolute** |
77
81
  | `opts.anchor` | `HTMLElement` | 是 | — | 模态贴着它弹 | The modal anchors next to it |
78
- | `opts.filter` | `PickFilter` | — | — | 按扩展名过滤 `{ exts: ['md'] }`;`dirs: true` 则只允许选文件夹 | Filter by extension `{ exts: ['md'] }`; `dirs: true` restricts picking to folders |
82
+ | `opts.filter` | `PickFilter` | — | — | `exts` 只要这些扩展名(目录仍可展开导航);`dirs: true` 只允许选文件夹(文件不渲染) | `exts` limits files (folders still expand); `dirs: true` picks folders only (files hidden, plus a “Select this folder” action in the crumb bar) |
@@ -26,16 +26,21 @@ const useUtils = `import { formatDateTime, relativeFuture, formatBytes } from 'd
26
26
  formatDateTime(task.runAt, { seconds: true }) // 2026-10-18 09:30:05
27
27
  relativeFuture(task.runAt, Date.now()) // 3 分钟后(未来时间用这个)
28
28
  formatBytes(file.size) // 12 KB`
29
- const useExplorer = `import { openFilePreview, pickWorkspaceFile } from 'dsh-plugin-core/explorer'
29
+ const useExplorer = `import { connectFs, openFilePreview, pickWorkspaceFile } from 'dsh-plugin-core/explorer'
30
+ import { serveFsRoute } from 'dsh-plugin-core/host'
31
+ import { FS_PATH } from './paths' // 自己的常量,宿主与 client 共用
30
32
 
31
- // 从 ctx 取出这一小块(不要传整个 ctx)
32
- const workspaceFiles = ctx.remote.workspaceFiles
33
+ // 宿主 apply:挂只读文件路由(列目录 / 读文件都在它上面,不限工作区)
34
+ serveFsRoute(ctx, { path: FS_PATH })
35
+ // client apply:接同一个常量(connectHost(ctx) 照旧负责工作区快照)
36
+ connectFs({ path: FS_PATH })
33
37
 
34
- // 预览某文件:右侧分栏(挤窄 row 左边的主区,不是浮在上面)
35
- openFilePreview({ mount: pageRowEl, workspaceFiles, sessionId, path: 'docs/requirements.md' })
38
+ // 预览某文件:右侧分栏(挤窄 row 左边的主区,不是浮在上面);绝对 / 相对都收
39
+ openFilePreview({ mount: pageRowEl, path: '/abs/ws/docs/requirements.md' })
36
40
 
37
- // 让用户选文件:锚定按钮旁弹模态,Promise 返回 { path, isDir }(工作区相对路径)
38
- const picked = await pickWorkspaceFile({ workspaceFiles, sessionId, anchor: btn, filter: { exts: ['ts'] } })`
41
+ // 让用户选:锚定按钮旁弹模态,Promise 返回 { path: 绝对路径, isDir, workspaceRel?, workspace? }
42
+ const picked = await pickWorkspaceFile({ anchor: btn, filter: { exts: ['ts'] } })
43
+ const folder = await pickWorkspaceFile({ anchor: btn, filter: { dirs: true } }) // 只选文件夹`
39
44
  const useSession = `import { openSessionView, SessionViewModal } from 'dsh-plugin-core/session'
40
45
 
41
46
  // sessions / uiConversation 同样从 ctx 取出(懒得取就用官方 client 服务)
@@ -45,7 +50,7 @@ if (view !== null) {
45
50
  sessionId, view,
46
51
  // 关闭必须释放 retain 引用
47
52
  onClose: () => { view.dispose(); setOpen(false) },
48
- onOpenFile: (path) => openFilePreview({ mount: pageRowEl, workspaceFiles, sessionId, path }),
53
+ onOpenFile: (path) => openFilePreview({ mount: pageRowEl, path, sessionId }),
49
54
  })
50
55
  }`
51
56
  const cleanup = `kit.dispose() // 撤掉 <style> 标签并清空登记域(插件卸载 / HMR)`
@@ -69,8 +74,8 @@ const doc: DocTopic = {
69
74
  { zh: '§2 用一个控件', en: '§2 Use a control' },
70
75
  { zh: '§3 用通用方法', en: '§3 Use helpers' },
71
76
  {
72
- zh: '§4 接工作区浏览:`mount` 必填(预览面板是布局成员,占宽挤窄主区),传页面那层 flex 行。`sessionId` 必须属于该工作区。',
73
- en: '§4 Wire workspace browsing: `mount` is required (the preview panel is a layout member that takes width); pass the page’s flex row. `sessionId` must belong to that workspace.',
77
+ zh: '§4 接文件浏览:`mount` 必填(预览面板是布局成员,占宽挤窄主区),传页面那层 flex 行;数据面要在宿主 `serveFsRoute` + client `connectFs` 用同一个 path 常量接上,浏览本身不需要会话。',
78
+ en: '§4 Wire file browsing: `mount` is required (the preview panel is a layout member that takes width); pass the page’s flex row. Wire the data face with the same path constant on both sides (`serveFsRoute` / `connectFs`) — browsing itself needs no session.',
74
79
  },
75
80
  {
76
81
  zh: '§5 接会话预览:`openSessionView` 返回 `null` 表示该会话不可解析;拿到 view 后交给 `SessionViewModal`,关闭时记得 `dispose()`。',
@@ -28,9 +28,9 @@
28
28
 
29
29
  > EN: §3 Use helpers
30
30
 
31
- §4 接工作区浏览:`mount` 必填(预览面板是布局成员,占宽挤窄主区),传页面那层 flex 行。`sessionId` 必须属于该工作区。
31
+ §4 接文件浏览:`mount` 必填(预览面板是布局成员,占宽挤窄主区),传页面那层 flex 行;数据面要在宿主 `serveFsRoute` + client `connectFs` 用同一个 path 常量接上,浏览本身不需要会话。
32
32
 
33
- > EN: §4 Wire workspace browsing: `mount` is required (the preview panel is a layout member that takes width); pass the page’s flex row. `sessionId` must belong to that workspace.
33
+ > EN: §4 Wire file browsing: `mount` is required (the preview panel is a layout member that takes width); pass the page’s flex row. Wire the data face with the same path constant on both sides (`serveFsRoute` / `connectFs`) — browsing itself needs no session.
34
34
 
35
35
  §5 接会话预览:`openSessionView` 返回 `null` 表示该会话不可解析;拿到 view 后交给 `SessionViewModal`,关闭时记得 `dispose()`。
36
36
 
@@ -78,16 +78,21 @@ formatBytes(file.size) // 12 KB
78
78
  ```
79
79
 
80
80
  ```tsx id="integration-use-explorer"
81
- import { openFilePreview, pickWorkspaceFile } from 'dsh-plugin-core/explorer'
81
+ import { connectFs, openFilePreview, pickWorkspaceFile } from 'dsh-plugin-core/explorer'
82
+ import { serveFsRoute } from 'dsh-plugin-core/host'
83
+ import { FS_PATH } from './paths' // 自己的常量,宿主与 client 共用
82
84
 
83
- // 从 ctx 取出这一小块(不要传整个 ctx)
84
- const workspaceFiles = ctx.remote.workspaceFiles
85
+ // 宿主 apply:挂只读文件路由(列目录 / 读文件都在它上面,不限工作区)
86
+ serveFsRoute(ctx, { path: FS_PATH })
87
+ // client apply:接同一个常量(connectHost(ctx) 照旧负责工作区快照)
88
+ connectFs({ path: FS_PATH })
85
89
 
86
- // 预览某文件:右侧分栏(挤窄 row 左边的主区,不是浮在上面)
87
- openFilePreview({ mount: pageRowEl, workspaceFiles, sessionId, path: 'docs/requirements.md' })
90
+ // 预览某文件:右侧分栏(挤窄 row 左边的主区,不是浮在上面);绝对 / 相对都收
91
+ openFilePreview({ mount: pageRowEl, path: '/abs/ws/docs/requirements.md' })
88
92
 
89
- // 让用户选文件:锚定按钮旁弹模态,Promise 返回 { path, isDir }(工作区相对路径)
90
- const picked = await pickWorkspaceFile({ workspaceFiles, sessionId, anchor: btn, filter: { exts: ['ts'] } })
93
+ // 让用户选:锚定按钮旁弹模态,Promise 返回 { path: 绝对路径, isDir, workspaceRel?, workspace? }
94
+ const picked = await pickWorkspaceFile({ anchor: btn, filter: { exts: ['ts'] } })
95
+ const folder = await pickWorkspaceFile({ anchor: btn, filter: { dirs: true } }) // 只选文件夹
91
96
  ```
92
97
 
93
98
  ```tsx id="integration-use-session"
@@ -100,7 +105,7 @@ if (view !== null) {
100
105
  sessionId, view,
101
106
  // 关闭必须释放 retain 引用
102
107
  onClose: () => { view.dispose(); setOpen(false) },
103
- onOpenFile: (path) => openFilePreview({ mount: pageRowEl, workspaceFiles, sessionId, path }),
108
+ onOpenFile: (path) => openFilePreview({ mount: pageRowEl, path, sessionId }),
104
109
  })
105
110
  }
106
111
  ```
@@ -247,7 +247,8 @@ dsh-plugin-core/
247
247
  | 文档 | README 接入指南 | ✅ 已完成 |
248
248
 
249
249
  **未做**(按重要性):消费者插件接入 / 会话 UI 镜像 / 类名隔离 /
250
- 设置页骨架(等第二个消费者)/ npm 发布流程。详见 README「还没做的事」。
250
+ 设置页骨架(等第二个消费者)/ npm 发布流程。落地进度一律以任务台账(task-ledger)为准,
251
+ 本文只记需求与已拍板的结论,不跟进度。
251
252
 
252
253
  > 说明:`dsh-plugin-core-demo` 作为真实插件已在宿主内跑通配置面板、侧栏入口、整页预览,
253
254
  > 已证明 core 在宿主里可用;上面「消费者插件接入」专指把 `dsh-task-dispatch-table` 等迁到 core、
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-core",
3
- "version": "0.20.4",
3
+ "version": "0.20.6",
4
4
  "packageManager": "pnpm@12.3.4",
5
5
  "description": "Shared base for DSH plugins: design tokens, UI controls, utils, workspace explorer, session preview",
6
6
  "keywords": [