dsh-plugin-core 0.20.3 → 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.
- package/dist/design/tokens-css.generated.d.ts +1 -1
- package/dist/design/tokens-css.generated.d.ts.map +1 -1
- package/dist/design/tokens-css.generated.js +1 -1
- package/dist/design/tokens-css.generated.js.map +1 -1
- package/dist/explorer/FileBrowser.d.ts +13 -6
- package/dist/explorer/FileBrowser.d.ts.map +1 -1
- package/dist/explorer/FileBrowser.js +190 -305
- package/dist/explorer/FileBrowser.js.map +1 -1
- package/dist/explorer/crumb-nav.d.ts +45 -0
- package/dist/explorer/crumb-nav.d.ts.map +1 -0
- package/dist/explorer/crumb-nav.js +75 -0
- package/dist/explorer/crumb-nav.js.map +1 -0
- package/dist/explorer/crumbs-menu.d.ts +37 -0
- package/dist/explorer/crumbs-menu.d.ts.map +1 -0
- package/dist/explorer/crumbs-menu.js +102 -0
- package/dist/explorer/crumbs-menu.js.map +1 -0
- package/dist/explorer/explorer-css.generated.d.ts +1 -1
- package/dist/explorer/explorer-css.generated.d.ts.map +1 -1
- package/dist/explorer/explorer-css.generated.js +1 -1
- package/dist/explorer/explorer-css.generated.js.map +1 -1
- package/dist/explorer/fs-face.d.ts +36 -0
- package/dist/explorer/fs-face.d.ts.map +1 -0
- package/dist/explorer/fs-face.js +74 -0
- package/dist/explorer/fs-face.js.map +1 -0
- package/dist/explorer/index.d.ts +2 -1
- package/dist/explorer/index.d.ts.map +1 -1
- package/dist/explorer/index.js +2 -1
- package/dist/explorer/index.js.map +1 -1
- package/dist/explorer/landing.d.ts +26 -0
- package/dist/explorer/landing.d.ts.map +1 -0
- package/dist/explorer/landing.js +43 -0
- package/dist/explorer/landing.js.map +1 -0
- package/dist/explorer/open.d.ts +35 -38
- package/dist/explorer/open.d.ts.map +1 -1
- package/dist/explorer/open.js +124 -156
- package/dist/explorer/open.js.map +1 -1
- package/dist/explorer/preview/BytesPreview.d.ts +2 -3
- package/dist/explorer/preview/BytesPreview.d.ts.map +1 -1
- package/dist/explorer/preview/BytesPreview.js +3 -3
- package/dist/explorer/preview/BytesPreview.js.map +1 -1
- package/dist/explorer/preview/HtmlPreview.d.ts +2 -3
- package/dist/explorer/preview/HtmlPreview.d.ts.map +1 -1
- package/dist/explorer/preview/HtmlPreview.js +4 -5
- package/dist/explorer/preview/HtmlPreview.js.map +1 -1
- package/dist/explorer/preview/OfficePreview.d.ts +2 -1
- package/dist/explorer/preview/OfficePreview.d.ts.map +1 -1
- package/dist/explorer/preview/OfficePreview.js +9 -2
- package/dist/explorer/preview/OfficePreview.js.map +1 -1
- package/dist/explorer/preview/TextPreview.d.ts +2 -3
- package/dist/explorer/preview/TextPreview.d.ts.map +1 -1
- package/dist/explorer/preview/TextPreview.js +3 -3
- package/dist/explorer/preview/TextPreview.js.map +1 -1
- package/dist/explorer/preview/index.d.ts +6 -3
- package/dist/explorer/preview/index.d.ts.map +1 -1
- package/dist/explorer/preview/index.js +3 -6
- package/dist/explorer/preview/index.js.map +1 -1
- package/dist/explorer/preview-title-bar.d.ts +26 -0
- package/dist/explorer/preview-title-bar.d.ts.map +1 -0
- package/dist/explorer/preview-title-bar.js +48 -0
- package/dist/explorer/preview-title-bar.js.map +1 -0
- package/dist/explorer/types.d.ts +25 -23
- package/dist/explorer/types.d.ts.map +1 -1
- package/dist/explorer/types.js +61 -120
- package/dist/explorer/types.js.map +1 -1
- package/dist/host/fs-route.d.ts +25 -0
- package/dist/host/fs-route.d.ts.map +1 -0
- package/dist/host/fs-route.js +243 -0
- package/dist/host/fs-route.js.map +1 -0
- package/dist/host/index.d.ts +2 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +1 -0
- package/dist/host/index.js.map +1 -1
- package/dist/session/session-css.generated.d.ts +1 -1
- package/dist/session/session-css.generated.d.ts.map +1 -1
- package/dist/session/session-css.generated.js +1 -1
- package/dist/session/session-css.generated.js.map +1 -1
- package/dist/ui/DateTimePicker.d.ts.map +1 -1
- package/dist/ui/DateTimePicker.js +5 -1
- package/dist/ui/DateTimePicker.js.map +1 -1
- package/dist/ui/Modal.d.ts.map +1 -1
- package/dist/ui/Modal.js +7 -2
- package/dist/ui/Modal.js.map +1 -1
- package/dist/ui/PanelDock.d.ts.map +1 -1
- package/dist/ui/PanelDock.js +4 -8
- package/dist/ui/PanelDock.js.map +1 -1
- package/dist/ui/Picker.d.ts.map +1 -1
- package/dist/ui/Picker.js +6 -3
- package/dist/ui/Picker.js.map +1 -1
- package/dist/ui/code-viewer-css.generated.d.ts +1 -1
- package/dist/ui/code-viewer-css.generated.d.ts.map +1 -1
- package/dist/ui/code-viewer-css.generated.js +1 -1
- package/dist/ui/code-viewer-css.generated.js.map +1 -1
- package/dist/ui/controls/datetime-css.generated.d.ts +1 -1
- package/dist/ui/controls/datetime-css.generated.d.ts.map +1 -1
- package/dist/ui/controls/datetime-css.generated.js +1 -1
- package/dist/ui/controls/datetime-css.generated.js.map +1 -1
- package/dist/ui/controls/field-css.generated.d.ts +1 -1
- package/dist/ui/controls/field-css.generated.d.ts.map +1 -1
- package/dist/ui/controls/field-css.generated.js +1 -1
- package/dist/ui/controls/field-css.generated.js.map +1 -1
- package/dist/ui/controls/loading-css.generated.d.ts +1 -1
- package/dist/ui/controls/loading-css.generated.d.ts.map +1 -1
- package/dist/ui/controls/loading-css.generated.js +1 -1
- package/dist/ui/controls/loading-css.generated.js.map +1 -1
- package/dist/ui/feedback-css.generated.d.ts +1 -1
- package/dist/ui/feedback-css.generated.d.ts.map +1 -1
- package/dist/ui/feedback-css.generated.js +1 -1
- package/dist/ui/feedback-css.generated.js.map +1 -1
- package/dist/ui/marquee-css.generated.d.ts +1 -1
- package/dist/ui/marquee-css.generated.d.ts.map +1 -1
- package/dist/ui/marquee-css.generated.js +1 -1
- package/dist/ui/marquee-css.generated.js.map +1 -1
- package/dist/ui/overlay-css.generated.d.ts +1 -1
- package/dist/ui/overlay-css.generated.d.ts.map +1 -1
- package/dist/ui/overlay-css.generated.js +1 -1
- package/dist/ui/overlay-css.generated.js.map +1 -1
- package/dist/ui/overlay.d.ts +8 -2
- package/dist/ui/overlay.d.ts.map +1 -1
- package/dist/ui/overlay.js +7 -6
- package/dist/ui/overlay.js.map +1 -1
- package/dist/ui/panel-css.d.ts +2 -0
- package/dist/ui/panel-css.d.ts.map +1 -0
- package/dist/ui/panel-css.generated.d.ts +2 -0
- package/dist/ui/panel-css.generated.d.ts.map +1 -0
- package/dist/ui/panel-css.generated.js +4 -0
- package/dist/ui/panel-css.generated.js.map +1 -0
- package/dist/ui/panel-css.js +2 -0
- package/dist/ui/panel-css.js.map +1 -0
- package/dist/ui/panel-dock-css.generated.d.ts +1 -1
- package/dist/ui/panel-dock-css.generated.d.ts.map +1 -1
- package/dist/ui/panel-dock-css.generated.js +1 -1
- package/dist/ui/panel-dock-css.generated.js.map +1 -1
- package/dist/ui/panel.d.ts +41 -0
- package/dist/ui/panel.d.ts.map +1 -0
- package/dist/ui/panel.js +37 -0
- package/dist/ui/panel.js.map +1 -0
- package/dist/ui/picker-css.generated.d.ts +1 -1
- package/dist/ui/picker-css.generated.d.ts.map +1 -1
- package/dist/ui/picker-css.generated.js +1 -1
- package/dist/ui/picker-css.generated.js.map +1 -1
- package/dist/ui/side-panel-css.generated.d.ts +1 -1
- package/dist/ui/side-panel-css.generated.d.ts.map +1 -1
- package/dist/ui/side-panel-css.generated.js +1 -1
- package/dist/ui/side-panel-css.generated.js.map +1 -1
- package/dist/utils/i18n.d.ts +1 -1
- package/dist/utils/i18n.d.ts.map +1 -1
- package/dist/utils/i18n.js +6 -0
- package/dist/utils/i18n.js.map +1 -1
- package/docs/design/architecture.md +9 -4
- package/docs/design/external/dsh-host.md +60 -0
- package/docs/design/style.md +54 -0
- package/docs/help/explorer.doc.ts +35 -30
- package/docs/help/explorer.md +23 -19
- package/docs/help/integration.doc.ts +15 -10
- package/docs/help/integration.md +15 -10
- package/docs/help/ui.doc.ts +2 -2
- package/docs/help/ui.md +2 -2
- package/docs/requirements.md +2 -1
- package/package.json +1 -1
|
@@ -8,104 +8,37 @@
|
|
|
8
8
|
// 两个消费场景共用本组件(用户 2026-10-20 拍板:同一套内容,两种展现形式):
|
|
9
9
|
// · 浏览(dock):由 **核心 PanelDock** 承载(openFilePreview),拖宽与关闭都由它负责,
|
|
10
10
|
// 本组件不再自绘拖拽条 / 侧边栏外壳;
|
|
11
|
-
// · 选择(picker):`picker` + `onPick(
|
|
11
|
+
// · 选择(picker):`picker` + `onPick(picked)`(`path` 是**绝对路径**,另带工作区归属),点文件即回调。
|
|
12
12
|
//
|
|
13
13
|
// 控件纪律:能用核心控件的一律核心(ghost 面包屑 / IconButton / Segmented / MarqueeText);
|
|
14
14
|
// 核心没有的那一项 = ▾ 选层浮层,按兄弟项目 dsh-task-dispatch-table 的 `.dsh-tdt-sv-crumbs-*`
|
|
15
15
|
// 逐像素照抄(间距 / 圆角 / 最大高度 / 空态),不自创度量。
|
|
16
16
|
import { createElement as h, Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
|
|
17
|
-
import {
|
|
17
|
+
import { IconChevronRightOutlineRegular, writeClipboard, FileTypeIcon, } from '../ui/official.js';
|
|
18
18
|
import { Button, IconButton } from '../ui/Button.js';
|
|
19
|
-
import { MarqueeText } from '../ui/MarqueeText.js';
|
|
20
|
-
import { Segmented } from '../ui/Segmented.js';
|
|
21
19
|
import { workspaceList } from '../ui/host-data.js';
|
|
22
20
|
import { t } from '../utils/i18n.js';
|
|
23
21
|
import { FilePreview } from './preview/index.js';
|
|
24
|
-
import {
|
|
22
|
+
import { bareCode, baseNameOfPath, crumbsOfDir, createRequestGate, dirnameOf, errView, isAborted, isAbsoluteish, isFailed, joinPath, listingOf, matchesFilter, relativizeToDir, sortEntries, normalizePath, workspaceOfPath, } from './types.js';
|
|
25
23
|
import { ensureComponentStyles } from '../ui/css.js';
|
|
24
|
+
import { PANEL_CSS } from '../ui/panel-css.js';
|
|
26
25
|
import { EXPLORER_CSS } from './explorer-css.js';
|
|
26
|
+
import { crumbsMenuEntries } from './crumbs-menu.js';
|
|
27
|
+
import { CrumbNav, PlainNav } from './crumb-nav.js';
|
|
28
|
+
import { LandingBody } from './landing.js';
|
|
29
|
+
import { PreviewTitleBar } from './preview-title-bar.js';
|
|
27
30
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* - 当前工作区排第一(打开文件夹图标 + 加粗),**其下路径紧跟**(每层一个右箭头位缩进),
|
|
31
|
-
* 再列其他工作区(关合图标 + 灰)——「打开哪个就跟在哪个下面」;点当前工作区 = 回到其根目录。
|
|
32
|
-
* - 当前工作区未知(rootName 空或不在清单)时路径段置顶、全部工作区按「其他」罗列;
|
|
33
|
-
* - workspaces 不传 = 不显示工作区段(根无箭头、其下逐层缩进)。
|
|
34
|
-
*
|
|
35
|
-
* 条目是**自写浮层**(核心没有「带缩进的选择层菜单」这类控件 ⇒ 按兄弟项目
|
|
36
|
-
* `dsh-task-dispatch-table` 的 `.dsh-tdt-sv-crumbs-menu*` 逐条照抄间距):
|
|
37
|
-
* backdrop + 菜单本体 + `-item`(4px 8px / 最大宽 280 / 超出滚动 / 空态)。
|
|
31
|
+
* 「根」在 POSIX 上的落点 `/`。Windows 没有这一个根 —— 根层是盘符清单,由宿主 `op:'roots'` 给出
|
|
32
|
+
* (见下面 `roots` 状态),本常量只在 POSIX / 探不到盘符时用。
|
|
38
33
|
*/
|
|
39
|
-
|
|
40
|
-
const { workspaces, rootName, rootFallbackLabel, crumbs, onPickWorkspace, onLoadDir } = params;
|
|
41
|
-
const out = [];
|
|
42
|
-
// 内联样式照抄兄弟项目(wsEntry 的 gap 6px 覆盖类的 2px:图标与文字的间距以它为准)。
|
|
43
|
-
const wsEntry = (ws, current) => h('button', {
|
|
44
|
-
key: `ws:${ws}`,
|
|
45
|
-
type: 'button',
|
|
46
|
-
role: 'menuitem',
|
|
47
|
-
className: 'dshpc-fb__menu-item',
|
|
48
|
-
style: { paddingLeft: 8, display: 'flex', alignItems: 'center', gap: '6px' },
|
|
49
|
-
title: ws,
|
|
50
|
-
// 点当前工作区 = 回到该工作区根目录(浏览深处时唯一的「回根」入口,兄弟 2026-10-10);
|
|
51
|
-
// 点其他工作区才走 onPickWorkspace(换会话)。
|
|
52
|
-
onClick: () => {
|
|
53
|
-
if (current)
|
|
54
|
-
onLoadDir?.('');
|
|
55
|
-
else
|
|
56
|
-
onPickWorkspace?.(ws);
|
|
57
|
-
},
|
|
58
|
-
}, h('span', { className: 'dshpc-fb__menu-icon', style: { color: current ? 'var(--dshpc-fg)' : 'var(--dshpc-fg-2)' } }, h(current ? IconFolderOpenOutlineRegular : IconFolderCloseRegular, { size: 13 })), h('span', { className: 'dshpc-fb__menu-label', style: { fontWeight: current ? 600 : 400 } }, ws));
|
|
59
|
-
const pathEntry = (crumb, depth, withArrow = true) => h('button', {
|
|
60
|
-
key: `dir:${crumb.path}`,
|
|
61
|
-
type: 'button',
|
|
62
|
-
role: 'menuitem',
|
|
63
|
-
className: 'dshpc-fb__menu-item',
|
|
64
|
-
style: { paddingLeft: 8 },
|
|
65
|
-
title: crumb.path,
|
|
66
|
-
onClick: () => {
|
|
67
|
-
onLoadDir?.(crumb.path);
|
|
68
|
-
},
|
|
69
|
-
}, withArrow
|
|
70
|
-
? h(Fragment, null, Array.from({ length: depth }, (_, s) => h('span', { key: `s${s}`, className: 'dshpc-fb__menu-chev-slot', 'aria-hidden': true })), h('span', { className: 'dshpc-fb__menu-chev' }, h(IconChevronRightOutlineRegular, { size: 11 })))
|
|
71
|
-
: null, h('span', { className: 'dshpc-fb__menu-label' }, crumb.label));
|
|
72
|
-
if (workspaces === undefined) {
|
|
73
|
-
if (crumbs.length === 0)
|
|
74
|
-
return [h('div', { key: 'empty', className: 'dshpc-fb__menu-empty' }, rootFallbackLabel)];
|
|
75
|
-
return crumbs.map((crumb, index) => pathEntry(crumb, index === 0 ? 0 : index - 1, index !== 0));
|
|
76
|
-
}
|
|
77
|
-
// 路径段:根 crumb(path='')已由工作区段表达时剔除,深度从根下一层起算。
|
|
78
|
-
const segments = rootName !== '' && crumbs.length > 0 && crumbs[0]?.path === '' ? crumbs.slice(1) : crumbs;
|
|
79
|
-
if (rootName !== '' && workspaces.includes(rootName)) {
|
|
80
|
-
// 当前工作区排第一,其下路径紧跟,然后是其他工作区(保持原相对顺序)。
|
|
81
|
-
out.push(wsEntry(rootName, true));
|
|
82
|
-
segments.forEach((crumb, i) => {
|
|
83
|
-
out.push(pathEntry(crumb, i));
|
|
84
|
-
});
|
|
85
|
-
for (const ws of workspaces) {
|
|
86
|
-
if (ws !== rootName)
|
|
87
|
-
out.push(wsEntry(ws, false));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
segments.forEach((crumb, i) => {
|
|
92
|
-
out.push(pathEntry(crumb, i));
|
|
93
|
-
});
|
|
94
|
-
for (const ws of workspaces)
|
|
95
|
-
out.push(wsEntry(ws, false));
|
|
96
|
-
}
|
|
97
|
-
return out;
|
|
98
|
-
}
|
|
34
|
+
const FS_ROOT = '/';
|
|
99
35
|
/** 目录浏览器面板。详见 {@link FileBrowserProps}。 */
|
|
100
36
|
export function FileBrowser(props) {
|
|
101
37
|
ensureComponentStyles('explorer', EXPLORER_CSS);
|
|
38
|
+
// ▾ 选层用浮层 menu 档(panel.css):跟弹窗同一套底色/描边/圆角/阴影。
|
|
39
|
+
ensureComponentStyles('panel', PANEL_CSS);
|
|
102
40
|
const tt = props.translate ?? t;
|
|
103
41
|
const picker = props.picker === true;
|
|
104
|
-
// 根缓存:调用方可注入(多实例/清理策略归调用方);不传则内部自建、随组件废弃。
|
|
105
|
-
const cacheRef = useRef(null);
|
|
106
|
-
if (cacheRef.current === null)
|
|
107
|
-
cacheRef.current = props.rootCache ?? createRootCache();
|
|
108
|
-
const rootCache = props.rootCache ?? cacheRef.current;
|
|
109
42
|
// mode:加载/目录树/文件预览/列举错误。viewing 非空 ⇒ 在 dir 树内预览文件。
|
|
110
43
|
const [mode, setMode] = useState('loading');
|
|
111
44
|
const [dir, setDir] = useState('');
|
|
@@ -125,11 +58,15 @@ export function FileBrowser(props) {
|
|
|
125
58
|
// 内联展开:openDirs = 已展开目录路径集合;childCache = 各展开目录拉到的子项。
|
|
126
59
|
const [openDirs, setOpenDirs] = useState(new Set());
|
|
127
60
|
const [childCache, setChildCache] = useState({});
|
|
128
|
-
// 工作区根补学完成信号(缓存非响应式,学成后 bump 触发面包屑重算)。
|
|
129
|
-
const [rootNonce, setRootNonce] = useState(0);
|
|
130
61
|
// 当前目录在会话工作区之外(官方 list 限工作区内,越界即报 outside-workspace)。
|
|
131
62
|
// 依据 = list 的**真实错误码**,不靠缓存猜(根学不出时会误判成"在区内")。
|
|
132
63
|
const [outside, setOutside] = useState(false);
|
|
64
|
+
// picker 且路径/会话都没给 ⇒ 落地页:主窗体先列「根目录 + 各工作区」让用户自己选(用户 2026-11-03 定)。
|
|
65
|
+
const [landing, setLanding] = useState(props.picker === true && props.path === '' && props.sessionId === undefined);
|
|
66
|
+
const landingRef = useRef(landing);
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
landingRef.current = landing;
|
|
69
|
+
}, [landing]);
|
|
133
70
|
const revision = props.revision ?? 0;
|
|
134
71
|
// 请求闸门(latest-wins):顶层列举 / 补学根 各一口;内联展开每个目录一口。
|
|
135
72
|
// begin() 先 abort 上一次,回调用 token.current() 判新旧 ⇒ 连续切换只认最后一次,
|
|
@@ -138,10 +75,6 @@ export function FileBrowser(props) {
|
|
|
138
75
|
if (listGateRef.current === null)
|
|
139
76
|
listGateRef.current = createRequestGate();
|
|
140
77
|
const listGate = listGateRef.current;
|
|
141
|
-
const rootGateRef = useRef(null);
|
|
142
|
-
if (rootGateRef.current === null)
|
|
143
|
-
rootGateRef.current = createRequestGate();
|
|
144
|
-
const rootGate = rootGateRef.current;
|
|
145
78
|
const childAbortsRef = useRef(new Map());
|
|
146
79
|
const abortChildren = useCallback(() => {
|
|
147
80
|
for (const controller of childAbortsRef.current.values())
|
|
@@ -155,18 +88,56 @@ export function FileBrowser(props) {
|
|
|
155
88
|
dirRef.current = dir;
|
|
156
89
|
listingRef.current = listing;
|
|
157
90
|
}, [dir, listing]);
|
|
158
|
-
// 路径 → 工作区匹配:props.path
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// 匹配生效但 rootCache 仍按 props.sessionId 学根 ⇒ 相对化失败 ⇒ 显示 `C.x > workspace > …`)。
|
|
162
|
-
// 匹配不到 ⇒ 整体回落 props.sessionId(旧行为)。
|
|
91
|
+
// 路径 → 工作区匹配:props.path 是绝对路径时按最长前缀找出所属工作区,面包屑根名与
|
|
92
|
+
// 相对化就用它(路径本来就在 FS 面里,不需要再「反推工作区根」)。
|
|
93
|
+
// 匹配不到 ⇒ 回落 props.rootName(调用方给的显示名)。
|
|
163
94
|
// 工作区清单来源(优先级从高到低):① props.workspaceAnchors(调用方显式给,可覆盖);
|
|
164
95
|
// ② 宿主 workspaces 快照(connectHost 注入的 workspaceList,官方 WorkspaceView 镜像)。
|
|
165
|
-
// 快照异步到达时会重算 + 重跑初始 effect(
|
|
96
|
+
// 快照异步到达时会重算 + 重跑初始 effect(rootBase 在其依赖里)⇒ 自愈,无感。
|
|
166
97
|
const hostWorkspaces = useSyncExternalStore(workspaceList.subscribe, workspaceList.getSnapshot);
|
|
167
98
|
const anchors = props.workspaceAnchors ?? hostWorkspaces;
|
|
168
|
-
|
|
169
|
-
|
|
99
|
+
// 根层清单:问宿主要(`op:'roots'`)。POSIX 回 `['/']`;Windows 回盘符 —— **Windows 没有「根目录」
|
|
100
|
+
// 这一个东西,根就是盘符清单**(用户 2026-11-03)。拿不到(文件面没这能力)⇒ 按 POSIX 单根。
|
|
101
|
+
const [roots, setRoots] = useState([FS_ROOT]);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const ask = props.files.roots;
|
|
104
|
+
if (ask === undefined)
|
|
105
|
+
return;
|
|
106
|
+
let alive = true;
|
|
107
|
+
void Promise.resolve()
|
|
108
|
+
.then(() => ask.call(props.files))
|
|
109
|
+
.then((list) => {
|
|
110
|
+
if (!alive || list.length === 0)
|
|
111
|
+
return;
|
|
112
|
+
// 内容相同就不换引用:调用方每次渲染都给新的 files 对象时,否则会「effect → setState → 渲染 → effect」空转。
|
|
113
|
+
setRoots((prev) => (prev.length === list.length && prev.every((item, index) => item === list[index]) ? prev : list));
|
|
114
|
+
})
|
|
115
|
+
.catch(() => { });
|
|
116
|
+
return () => { alive = false; };
|
|
117
|
+
}, [props.files]);
|
|
118
|
+
const anchorMatch = useMemo(() => workspaceOfPath(props.path, anchors), [props.path, anchors]);
|
|
119
|
+
/** 相对路径的基准目录(工作区根):匹配出的工作区 > 调用方给的 > 无(只认绝对路径)。 */
|
|
120
|
+
const sessionAnchor = props.sessionId === undefined
|
|
121
|
+
? null
|
|
122
|
+
: anchors.find((item) => item.sessionIds?.includes(props.sessionId) === true) ?? null;
|
|
123
|
+
const rootBase = anchorMatch?.path ?? props.rootPath ?? sessionAnchor?.path ?? anchors.find((item) => item.path !== undefined)?.path;
|
|
124
|
+
/** 任意入参 → 绝对路径(FS 面只认绝对路径)。 */
|
|
125
|
+
const absOf = useCallback((p) => {
|
|
126
|
+
if (isAbsoluteish(p))
|
|
127
|
+
return normalizePath(p);
|
|
128
|
+
// '' = 「根」:有工作区基准就是该工作区根,没有就是文件系统根(`/`;Windows 上是当前盘根)。
|
|
129
|
+
if (rootBase === undefined || rootBase === '')
|
|
130
|
+
return p === '' ? FS_ROOT : p;
|
|
131
|
+
return p === '' ? normalizePath(rootBase) : normalizePath(`${rootBase}/${p.replace(/^\/+/, '')}`);
|
|
132
|
+
}, [rootBase]);
|
|
133
|
+
/** 面板回传(绝对路径 + 工作区归属)。 */
|
|
134
|
+
const toPicked = useCallback((p, isDir) => {
|
|
135
|
+
const abs = absOf(p);
|
|
136
|
+
const owner = workspaceOfPath(abs, anchors);
|
|
137
|
+
return owner === null
|
|
138
|
+
? { path: abs, isDir }
|
|
139
|
+
: { path: abs, isDir, workspaceRel: relativizeToDir(abs, owner.path ?? null), workspace: owner.title };
|
|
140
|
+
}, [absOf, anchors]);
|
|
170
141
|
/** 记一次 list 结果的"是否越界"(outside-workspace / not-found 均视为不可浏览)。 */
|
|
171
142
|
const noteOutside = useCallback((parsed) => {
|
|
172
143
|
if (parsed === null || parsed === undefined)
|
|
@@ -183,7 +154,8 @@ export function FileBrowser(props) {
|
|
|
183
154
|
const fetchDir = useCallback((targetDir) => {
|
|
184
155
|
const token = listGate.begin();
|
|
185
156
|
abortChildren();
|
|
186
|
-
|
|
157
|
+
const absTarget = absOf(targetDir);
|
|
158
|
+
setDir(absTarget);
|
|
187
159
|
setViewing(null);
|
|
188
160
|
setListErr(null);
|
|
189
161
|
setMenuOpen(false);
|
|
@@ -191,8 +163,8 @@ export function FileBrowser(props) {
|
|
|
191
163
|
// 切换顶层目录:清空内联展开态(展开树只属于当前这一层)。
|
|
192
164
|
setOpenDirs(new Set());
|
|
193
165
|
setChildCache({});
|
|
194
|
-
props.
|
|
195
|
-
.list(
|
|
166
|
+
props.files
|
|
167
|
+
.list(absTarget, token.signal)
|
|
196
168
|
.then((result) => {
|
|
197
169
|
if (!token.current())
|
|
198
170
|
return;
|
|
@@ -218,21 +190,26 @@ export function FileBrowser(props) {
|
|
|
218
190
|
setListErr(errView(error));
|
|
219
191
|
setMode('error');
|
|
220
192
|
});
|
|
221
|
-
}, [abortChildren,
|
|
193
|
+
}, [abortChildren, absOf, listGate, noteOutside, props.files]);
|
|
222
194
|
/** 进入某目录;点到当前目录本身 = 重列本层(刷新)。 */
|
|
223
195
|
const loadDir = useCallback((targetDir) => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
196
|
+
// 落地页上点 ▾ 选根目录 / 工作区:必须先退出落地页,否则主体还是落地页那张表,
|
|
197
|
+
// 点了跟没点一样(用户 2026-11-03 真机:"选里面的任何一个都没有任何效果")。
|
|
198
|
+
if (landingRef.current)
|
|
199
|
+
setLanding(false);
|
|
228
200
|
fetchDir(targetDir);
|
|
229
|
-
}, [
|
|
201
|
+
}, [fetchDir]);
|
|
230
202
|
// 初次进入(path 可能是文件或目录):
|
|
231
203
|
// list(path) 成功 ⇒ 目录树(dir 尽量取宿主绝对路径 ⇒ 面包屑从工作区根往下列);
|
|
232
204
|
// 报错 ⇒ 文件预览态(dir = 父目录,面包屑保留可返回)。
|
|
233
205
|
// ⚠️ 切勿改用服务端 list 返回的 path 当面包屑:那是 workspacePathOf 的工作区**相对**形式,
|
|
234
206
|
// 会丢掉根以下的前导段,面包屑只剩最近一层(源实现 2026-09-29 踩过 ⇒ 已回退为入参路径)。
|
|
235
207
|
useEffect(() => {
|
|
208
|
+
// 落地页(还没选根目录/工作区):什么都不列,等用户点。
|
|
209
|
+
// 用 ref 读、**不进依赖**:否则从落地页点进目录那一刻 landing 变 false 会重跑本 effect,
|
|
210
|
+
// 又把目录拉回 absOf('') = 第一个工作区(真机 2026-11-03)。
|
|
211
|
+
if (landingRef.current)
|
|
212
|
+
return;
|
|
236
213
|
// 同目录换文件(侧栏连点 / 重开同一文件):只换 viewing——目录树、展开态、滚动位置保留,
|
|
237
214
|
// 也不再额外触发一次 list。旧目录列举已由上一轮 effect 的清理函数 abort。
|
|
238
215
|
// 判据用 listingRef(不是 props.path 的类型):只有「当前已列出的这一层里确实有这个文件」
|
|
@@ -241,7 +218,7 @@ export function FileBrowser(props) {
|
|
|
241
218
|
const name = baseNameOfPath(props.path);
|
|
242
219
|
const knownFile = listingRef.current?.some((entry) => entry.name === name && entry.type === 'file') === true;
|
|
243
220
|
if (props.path !== '' && dirRef.current !== '' && parent === dirRef.current && knownFile) {
|
|
244
|
-
setViewing(props.path);
|
|
221
|
+
setViewing(absOf(props.path));
|
|
245
222
|
setMode('file');
|
|
246
223
|
setSourceView(false);
|
|
247
224
|
// 同路径再点一次也要重读内容(对齐旧版「每次打开都重读」;不重挂只换 key)。
|
|
@@ -251,7 +228,6 @@ export function FileBrowser(props) {
|
|
|
251
228
|
return;
|
|
252
229
|
}
|
|
253
230
|
const token = listGate.begin();
|
|
254
|
-
const rootToken = rootGate.begin();
|
|
255
231
|
abortChildren();
|
|
256
232
|
setMode('loading');
|
|
257
233
|
setListErr(null);
|
|
@@ -261,94 +237,28 @@ export function FileBrowser(props) {
|
|
|
261
237
|
setMenuOpen(false);
|
|
262
238
|
setOpenDirs(new Set());
|
|
263
239
|
setChildCache({});
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
void (async () => {
|
|
269
|
-
try {
|
|
270
|
-
const parsed = listingOf(await props.workspaceFiles.list(listSessionId, listPathOf(currentDir), rootToken.signal));
|
|
271
|
-
if (!rootToken.current() || parsed === null || isFailed(parsed))
|
|
272
|
-
return;
|
|
273
|
-
const stat = props.workspaceFiles.stat;
|
|
274
|
-
if (stat === undefined)
|
|
275
|
-
return;
|
|
276
|
-
const rel = parsed.path;
|
|
277
|
-
const file = parsed.entries.find((entry) => entry.type === 'file');
|
|
278
|
-
if (file !== undefined) {
|
|
279
|
-
const sub = rel === '' ? file.name : `${rel}/${file.name}`;
|
|
280
|
-
const abs = (await stat(listSessionId, sub, rootToken.signal).catch(() => null));
|
|
281
|
-
if (!rootToken.current())
|
|
282
|
-
return;
|
|
283
|
-
const absPath = typeof abs?.absolutePath === 'string' ? abs.absolutePath : null;
|
|
284
|
-
if (absPath !== null && learnRoot(rootCache, listSessionId, sub, absPath) !== null) {
|
|
285
|
-
setRootNonce((n) => n + 1);
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
const found = await findAnyFileAbs(props.workspaceFiles, listSessionId, rel, rootToken.signal);
|
|
290
|
-
if (!rootToken.current())
|
|
291
|
-
return;
|
|
292
|
-
if (found !== null && learnRoot(rootCache, listSessionId, found.relativePath, found.absolutePath) !== null) {
|
|
293
|
-
setRootNonce((n) => n + 1);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
/* 学不出就维持退化显示 */
|
|
298
|
-
}
|
|
299
|
-
})();
|
|
300
|
-
};
|
|
301
|
-
props.workspaceFiles
|
|
302
|
-
.list(listSessionId, listPathOf(props.path), token.signal)
|
|
303
|
-
.then(async (result) => {
|
|
240
|
+
const entry = absOf(props.path);
|
|
241
|
+
props.files
|
|
242
|
+
.list(entry, token.signal)
|
|
243
|
+
.then((result) => {
|
|
304
244
|
if (!token.current())
|
|
305
245
|
return;
|
|
306
246
|
const parsed = listingOf(result);
|
|
307
247
|
if (!isFailed(parsed) && parsed !== null) {
|
|
308
248
|
setOutside(false);
|
|
309
|
-
|
|
310
|
-
// 解析不出(空目录/只有子目录/stat 缺失)就退回入参,不阻塞浏览。
|
|
311
|
-
const absDir = await absolutizeDir(rootCache, props.workspaceFiles, listSessionId, props.path, parsed.path, parsed.entries, token.signal);
|
|
312
|
-
if (!token.current())
|
|
313
|
-
return;
|
|
314
|
-
setDir(absDir);
|
|
249
|
+
setDir(entry);
|
|
315
250
|
setListing(parsed.entries);
|
|
316
251
|
setTruncated(parsed.truncated);
|
|
317
252
|
setMode('dir');
|
|
318
|
-
ensureRootLearned(absDir);
|
|
319
253
|
return;
|
|
320
254
|
}
|
|
321
255
|
// 不是目录 ⇒ 当作文件预览,dir 取父目录,尽量把父树也列出来。
|
|
322
|
-
|
|
323
|
-
if (!isAbsoluteish(props.path)) {
|
|
324
|
-
const stat = props.workspaceFiles.stat;
|
|
325
|
-
if (stat !== undefined) {
|
|
326
|
-
try {
|
|
327
|
-
const res = await stat(listSessionId, props.path, token.signal);
|
|
328
|
-
if (!token.current())
|
|
329
|
-
return;
|
|
330
|
-
const abs = typeof res.absolutePath === 'string'
|
|
331
|
-
? res.absolutePath
|
|
332
|
-
: null;
|
|
333
|
-
if (abs !== null) {
|
|
334
|
-
parentDir = dirnameOf(abs);
|
|
335
|
-
// 记住工作区根 ⇒ 之后同会话里相对名也能解析
|
|
336
|
-
learnRoot(rootCache, listSessionId, props.path, abs);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
340
|
-
/* 解析不出就用入参父目录 */
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (!token.current())
|
|
345
|
-
return;
|
|
256
|
+
const parentDir = dirnameOf(entry);
|
|
346
257
|
setDir(parentDir);
|
|
347
|
-
setViewing(
|
|
258
|
+
setViewing(entry);
|
|
348
259
|
setMode('file');
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
.list(listSessionId, listPathOf(parentDir), token.signal)
|
|
260
|
+
props.files
|
|
261
|
+
.list(parentDir, token.signal)
|
|
352
262
|
.then((pres) => {
|
|
353
263
|
if (!token.current())
|
|
354
264
|
return;
|
|
@@ -367,21 +277,28 @@ export function FileBrowser(props) {
|
|
|
367
277
|
if (!token.current() || isAborted(token.signal, error))
|
|
368
278
|
return;
|
|
369
279
|
// 传输层失败:仍尝试按文件预览(预览体内部会给出错误态)。
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
setViewing(props.path);
|
|
280
|
+
setDir(dirnameOf(entry));
|
|
281
|
+
setViewing(entry);
|
|
373
282
|
setMode('file');
|
|
374
|
-
ensureRootLearned(parentDir);
|
|
375
283
|
});
|
|
376
284
|
return () => {
|
|
377
285
|
listGate.abort();
|
|
378
|
-
rootGate.abort();
|
|
379
286
|
};
|
|
380
287
|
// ⚠️ 这里**不** abort 内联展开的子请求:同目录换文件时它们仍然有效(树也保留着)。
|
|
381
288
|
// 子请求只在「换顶层目录」(fetchDir)与卸载时取消。
|
|
382
|
-
}, [abortChildren,
|
|
289
|
+
}, [abortChildren, absOf, listGate, noteOutside, props.files, props.path, revision]);
|
|
383
290
|
// 卸载时把内联展开的在途请求一起取消(依赖为空 ⇒ 只有卸载才跑清理)。
|
|
384
291
|
useEffect(() => () => abortChildren(), [abortChildren]);
|
|
292
|
+
/** ▾ 里点别的工作区:调用方没接管就直接列该工作区根(不需要会话)。 */
|
|
293
|
+
const switchWorkspace = useCallback((name) => {
|
|
294
|
+
if (props.onSelectWorkspace !== undefined) {
|
|
295
|
+
props.onSelectWorkspace(name);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const target = anchors.find((item) => item.title === name);
|
|
299
|
+
if (target?.path !== undefined)
|
|
300
|
+
loadDir(target.path);
|
|
301
|
+
}, [anchors, loadDir, props.onSelectWorkspace]);
|
|
385
302
|
// 面包屑溢出测量:隐藏测量条渲染与可见态完全相同的 crumb 按钮(含 padding/max-width),
|
|
386
303
|
// 使溢出判定与真实排版一致——不超长时必定还原完整路径。ResizeObserver 同时覆盖
|
|
387
304
|
// dock 宽度变化 / 初次布局。
|
|
@@ -400,7 +317,7 @@ export function FileBrowser(props) {
|
|
|
400
317
|
// 依赖要带上「根反推完成」「越界态变化」:这两样会让面包屑从超长绝对路径相对化成
|
|
401
318
|
// 短路径,但若不重算,crumbsOverflow 会卡在首帧的 true(只显示末段),直到下次导航才复原
|
|
402
319
|
// (2026-10-28 真机:打开工作区内的绝对路径,面包屑只剩末段,点一下目录才恢复)。
|
|
403
|
-
}, [dir, viewing, mode, listing,
|
|
320
|
+
}, [dir, viewing, mode, listing, outside]);
|
|
404
321
|
const reload = useCallback(() => {
|
|
405
322
|
if (viewing !== null) {
|
|
406
323
|
setReloadNonce((n) => n + 1);
|
|
@@ -413,7 +330,7 @@ export function FileBrowser(props) {
|
|
|
413
330
|
const target = viewing ?? dir;
|
|
414
331
|
if (target === '')
|
|
415
332
|
return;
|
|
416
|
-
const rel =
|
|
333
|
+
const rel = relativizeToDir(target, rootBase ?? null);
|
|
417
334
|
writeClipboard(rel)
|
|
418
335
|
.then((ok) => {
|
|
419
336
|
if (ok) {
|
|
@@ -424,7 +341,7 @@ export function FileBrowser(props) {
|
|
|
424
341
|
.catch(() => {
|
|
425
342
|
/* 剪贴板不可用(非安全上下文等):静默,不弹错 */
|
|
426
343
|
});
|
|
427
|
-
}, [viewing, dir,
|
|
344
|
+
}, [viewing, dir, rootBase]);
|
|
428
345
|
/** 内联展开/收起某目录(点 ▸):只切展开态,不导航;首次展开才拉子项。 */
|
|
429
346
|
const toggleDir = useCallback((path) => {
|
|
430
347
|
setOpenDirs((prev) => {
|
|
@@ -440,8 +357,8 @@ export function FileBrowser(props) {
|
|
|
440
357
|
const controller = new AbortController();
|
|
441
358
|
childAbortsRef.current.set(path, controller);
|
|
442
359
|
setChildCache((prev) => ({ ...prev, [path]: { status: 'loading' } }));
|
|
443
|
-
props.
|
|
444
|
-
.list(
|
|
360
|
+
props.files
|
|
361
|
+
.list(absOf(path), controller.signal)
|
|
445
362
|
.then((result) => {
|
|
446
363
|
if (controller.signal.aborted)
|
|
447
364
|
return;
|
|
@@ -463,7 +380,7 @@ export function FileBrowser(props) {
|
|
|
463
380
|
childAbortsRef.current.delete(path);
|
|
464
381
|
});
|
|
465
382
|
}
|
|
466
|
-
}, [childCache, props.
|
|
383
|
+
}, [absOf, childCache, props.files]);
|
|
467
384
|
// 过滤点名了扩展名 ⇒ 要的是**文件**:目录只作导航(点行就地展开),不给「选择」、也回传不了 isDir。
|
|
468
385
|
// (旧实现「目录永远保留且永远带选择钮」,于是「仅 JS/MD」下出现「文件夹旁边有选择、
|
|
469
386
|
// MD 文件旁边反而没有」,选中还回传一个文件夹——与过滤语义直接打架。)
|
|
@@ -480,8 +397,8 @@ export function FileBrowser(props) {
|
|
|
480
397
|
if (!isDir) {
|
|
481
398
|
const pick = () => {
|
|
482
399
|
if (picker && props.onPick !== undefined) {
|
|
483
|
-
//
|
|
484
|
-
props.onPick(
|
|
400
|
+
// 回传绝对路径 + 工作区归属(用户 2026-11-03 定 `PickedPath`)。
|
|
401
|
+
props.onPick(toPicked(childPath, false));
|
|
485
402
|
return;
|
|
486
403
|
}
|
|
487
404
|
// 逐层展开后选中文件:面包屑要跟随到该文件所在目录。
|
|
@@ -532,7 +449,7 @@ export function FileBrowser(props) {
|
|
|
532
449
|
if (picker) {
|
|
533
450
|
// 扩展名过滤下这里不该被触发(目录行根本不渲染这枚钮);兜住是为了语义不被绕过。
|
|
534
451
|
if (!extsOnly && props.onPick !== undefined)
|
|
535
|
-
props.onPick(
|
|
452
|
+
props.onPick(toPicked(childPath, true));
|
|
536
453
|
return;
|
|
537
454
|
}
|
|
538
455
|
loadDir(childPath);
|
|
@@ -588,132 +505,100 @@ export function FileBrowser(props) {
|
|
|
588
505
|
? h('div', { className: 'dshpc-fb__row-err' }, tt(cached.error.key, cached.error.params))
|
|
589
506
|
: h(Fragment, null, renderTree(cached.entries, childPath), cached.truncated ? h('div', { className: 'dshpc-fb__row-note' }, tt('explorerTruncated')) : null))
|
|
590
507
|
: null);
|
|
591
|
-
}), [childCache, openDirs, picker, dirPickable, extsOnly, props.filter, props.onPick,
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
508
|
+
}), [childCache, openDirs, picker, dirPickable, extsOnly, props.filter, props.onPick, toPicked, dir, loadDir, tt]);
|
|
509
|
+
// 根层条目:有盘符(Windows)就一条一档;否则单条「根目录」(POSIX)。
|
|
510
|
+
// 两者都是**可选目标**(用户 2026-11-03:Linux 的 `/` 也可以选;Windows 至少能选盘)。
|
|
511
|
+
const drives = roots.filter((root) => root !== FS_ROOT);
|
|
512
|
+
const rootRows = drives.length > 0
|
|
513
|
+
? drives.map((path) => ({ label: path.replace(/\/+$/, ''), path }))
|
|
514
|
+
: [{ label: tt('explorerRootDir'), path: FS_ROOT }];
|
|
515
|
+
// 面包屑(用户 2026-11-03 定):**按当前目录**去比所有工作区。
|
|
516
|
+
// 命中 ⇒ 第一段是工作区名,其下按相对层级;没命中 ⇒ 第一段是根(「根目录」或盘符),其下逐层。
|
|
517
|
+
// 判据只看 `dir`(不比 props.path):浏览途中切进别的工作区也要立刻跟着切根。
|
|
518
|
+
const owner = workspaceOfPath(dir, anchors);
|
|
519
|
+
const rootLabel = owner?.title ?? '';
|
|
520
|
+
const crumbs = owner === null
|
|
521
|
+
? (/^[a-zA-Z]:/.test(normalizePath(dir))
|
|
522
|
+
// 盘符路径:根段由 crumbsOfDir 自己出(`C:`),再补一段「根目录」就重复了。
|
|
523
|
+
? crumbsOfDir(dir)
|
|
524
|
+
: [{ label: rootRows[0]?.label ?? tt('explorerRootDir'), path: FS_ROOT }, ...crumbsOfDir(dir)])
|
|
525
|
+
: [{ label: rootLabel, path: '' }, ...crumbsOfDir(relativizeToDir(dir, owner.path ?? null))];
|
|
606
526
|
// md / html 都出「渲染 ⇄ 源码」切换(默认渲染)。
|
|
607
527
|
const viewingIsMd = viewing !== null && /\.(md|markdown)$/i.test(viewing);
|
|
608
528
|
const viewingIsHtml = viewing !== null && /\.(html|htm)$/i.test(viewing);
|
|
609
529
|
const switchable = viewingIsMd || viewingIsHtml;
|
|
530
|
+
// 工作区清单:调用方没传就用宿主快照里的(picker 与 dock 同一条逻辑 —— 用户 2026-11-03:
|
|
531
|
+
// 两处显示必须一致,不能 dock 有清单、picker 只有一个根目录)。
|
|
610
532
|
const menuItems = crumbsMenuEntries({
|
|
611
|
-
workspaces: props.workspaces,
|
|
533
|
+
workspaces: props.workspaces ?? anchors.map((item) => item.title),
|
|
534
|
+
roots: rootRows,
|
|
612
535
|
rootName: rootLabel,
|
|
613
536
|
rootFallbackLabel: tt('explorerRootName'),
|
|
614
537
|
crumbs,
|
|
615
|
-
onPickWorkspace:
|
|
538
|
+
onPickWorkspace: switchWorkspace,
|
|
616
539
|
onLoadDir: loadDir,
|
|
617
540
|
});
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
? crumbButton(crumbs.length > 0 ? crumbs[crumbs.length - 1]?.label ?? tt('explorerRootName') : tt('explorerRootName'), undefined, true)
|
|
650
|
-
: crumbs.length === 0
|
|
651
|
-
? crumbButton(tt('explorerRootName'), undefined, true)
|
|
652
|
-
: crumbs.map((crumb, index) => h(Fragment, { key: crumb.path }, index > 0 ? h(IconChevronRightOutlineRegular, { className: 'dshpc-fb__crumb-sep', size: 12 }) : null, crumbButton(crumb.label, () => loadDir(crumb.path))))), h('div', { className: 'dshpc-fb__actions' }, props.onClose === undefined
|
|
653
|
-
? null
|
|
654
|
-
: h(IconButton, {
|
|
655
|
-
variant: 'plain',
|
|
656
|
-
size: 'md',
|
|
657
|
-
icon: h(IconCloseOutlineRegular, { size: 14 }),
|
|
658
|
-
label: tt('previewClose'),
|
|
659
|
-
onClick: props.onClose,
|
|
660
|
-
})));
|
|
661
|
-
// 越界(outside-workspace):第一排整条换成只读完整路径,不给任何会失败的入口。
|
|
662
|
-
const crumbBarPlain = h('nav', { className: 'dshpc-fb__bar', 'aria-label': tt('explorerCrumbsAria') }, h(MarqueeText, {
|
|
663
|
-
text: dir,
|
|
664
|
-
title: dir,
|
|
665
|
-
className: 'dshpc-fb__bar-plain',
|
|
666
|
-
style: { flex: '1 1 auto', minWidth: 0 },
|
|
667
|
-
}), h('div', { className: 'dshpc-fb__actions' }, props.onClose === undefined
|
|
668
|
-
? null
|
|
669
|
-
: h(IconButton, {
|
|
670
|
-
variant: 'plain',
|
|
671
|
-
size: 'md',
|
|
672
|
-
icon: h(IconCloseOutlineRegular, { size: 14 }),
|
|
673
|
-
label: tt('previewClose'),
|
|
674
|
-
onClick: props.onClose,
|
|
675
|
-
})));
|
|
676
|
-
// 第二排(仅预览态显示):文件名 + 渲染⇄源码切换 / 复制路径 / 刷新。
|
|
677
|
-
// 档位照兄弟项目调用点:Segmented `sm` + 图标钮 `md`(核心 Segmented 高度由 CSS 定,不随档变)。
|
|
541
|
+
// 选择器的「选当前目录」入口(用户 2026-11-03:进到某一层之后,行上没有它自己,
|
|
542
|
+
// 树里也点不到「当前这层」⇒ 在 ✕ 左边给一枚按钮)。落地页还没进任何目录、扩展名过滤下
|
|
543
|
+
// 目录不是合法结果 —— 这两种情况不出按钮。
|
|
544
|
+
const pickNow = picker && props.onPick !== undefined && !landing && dirPickable && dir !== ''
|
|
545
|
+
? h(Button, {
|
|
546
|
+
// 描边(outline):ghost 在这一排**看不出来是按钮**——它无底无边,与面板同色,
|
|
547
|
+
// 用户 2026-11-03 实测「就像没有背景一样」,于是改回有 border 的一档(覆盖当天早些的 ghost 结论)。
|
|
548
|
+
variant: 'outline',
|
|
549
|
+
size: 'sm',
|
|
550
|
+
className: 'dshpc-fb__pick',
|
|
551
|
+
'aria-label': tt('explorerPickDirAria'),
|
|
552
|
+
onClick: () => props.onPick?.(toPicked(dir, true)),
|
|
553
|
+
}, tt('explorerPickDir'))
|
|
554
|
+
: null;
|
|
555
|
+
const nav = outside
|
|
556
|
+
? h(PlainNav, { translate: tt, path: dir, pickNow, onClose: props.onClose })
|
|
557
|
+
: h(CrumbNav, {
|
|
558
|
+
translate: tt,
|
|
559
|
+
landing,
|
|
560
|
+
crumbs,
|
|
561
|
+
overflow: crumbsOverflow,
|
|
562
|
+
menuItems,
|
|
563
|
+
menuOpen,
|
|
564
|
+
onToggleMenu: () => setMenuOpen((v) => !v),
|
|
565
|
+
onCloseMenu: () => setMenuOpen(false),
|
|
566
|
+
onLoadDir: loadDir,
|
|
567
|
+
pickNow,
|
|
568
|
+
onClose: props.onClose,
|
|
569
|
+
regionRef,
|
|
570
|
+
measureRef,
|
|
571
|
+
});
|
|
678
572
|
const titleBar = viewing === null
|
|
679
573
|
? null
|
|
680
|
-
: h(
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
{ value: 'render', label: tt('previewRender') },
|
|
692
|
-
{ value: 'source', label: tt('previewSource') },
|
|
693
|
-
],
|
|
694
|
-
onChange: (next) => setSourceView(next === 'source'),
|
|
695
|
-
})
|
|
696
|
-
: null, h(IconButton, {
|
|
697
|
-
variant: 'plain',
|
|
698
|
-
size: 'md',
|
|
699
|
-
icon: copied ? h(IconCheckOutlineRegular, { size: 14 }) : h(IconCopyOutlineRegular, { size: 14 }),
|
|
700
|
-
label: tt('previewCopyPath'),
|
|
701
|
-
onClick: copyPath,
|
|
702
|
-
}), h(IconButton, {
|
|
703
|
-
variant: 'plain',
|
|
704
|
-
size: 'md',
|
|
705
|
-
icon: h(IconRefreshOutlineRegular, { size: 14 }),
|
|
706
|
-
label: tt('previewRefresh'),
|
|
707
|
-
onClick: reload,
|
|
708
|
-
})));
|
|
574
|
+
: h(PreviewTitleBar, {
|
|
575
|
+
translate: tt,
|
|
576
|
+
path: viewing,
|
|
577
|
+
switchable,
|
|
578
|
+
isMarkdown: viewingIsMd,
|
|
579
|
+
sourceView,
|
|
580
|
+
onSourceViewChange: setSourceView,
|
|
581
|
+
copied,
|
|
582
|
+
onCopyPath: copyPath,
|
|
583
|
+
onReload: reload,
|
|
584
|
+
});
|
|
709
585
|
// ── 主体 ──
|
|
710
586
|
let body;
|
|
711
|
-
if (
|
|
587
|
+
if (landing) {
|
|
588
|
+
body = h(LandingBody, {
|
|
589
|
+
translate: tt,
|
|
590
|
+
roots: rootRows,
|
|
591
|
+
anchors,
|
|
592
|
+
pickable: picker && props.onPick !== undefined && !extsOnly,
|
|
593
|
+
onOpen: loadDir,
|
|
594
|
+
onPickDir: (target) => props.onPick?.(toPicked(target, true)),
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
else if (viewing !== null) {
|
|
712
598
|
body = h(FilePreview, {
|
|
713
599
|
key: `${viewing}:${reloadNonce}`,
|
|
714
|
-
|
|
600
|
+
files: props.files,
|
|
715
601
|
officeToPdf: props.officeToPdf ?? null,
|
|
716
|
-
sessionId: listSessionId,
|
|
717
602
|
path: viewing,
|
|
718
603
|
sourceView,
|
|
719
604
|
translate: tt,
|
|
@@ -737,6 +622,6 @@ export function FileBrowser(props) {
|
|
|
737
622
|
return h('div', {
|
|
738
623
|
className: 'dshpc-fb',
|
|
739
624
|
style: props.style ?? undefined,
|
|
740
|
-
},
|
|
625
|
+
}, nav, titleBar, body);
|
|
741
626
|
}
|
|
742
627
|
//# sourceMappingURL=FileBrowser.js.map
|