@weniatall/dsh-file-viewer 0.3.14 → 0.3.18
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/README.md +2 -0
- package/lib/client.js +255 -86
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -94,6 +94,8 @@ dsh plugin --profile web add <release-tgz-url>
|
|
|
94
94
|
|
|
95
95
|
桌面端(Windows)抽屉会自动避让窗口右上角的原生标题栏按钮(最小化 / 最大化 / 关闭):宿主在 `<html>` 上标记 `data-windows-titlebar` 并给出 `--dsh-windows-titlebar-height`,抽屉顶部从其下方开始,因此标题栏里的图标不会再压在窗口按钮下面;窗口进入全屏后该条带消失,抽屉相应贴到顶边。
|
|
96
96
|
|
|
97
|
+
这条偏移带 `!important`:抽屉为退出宿主"居中卡片化"而声明的 `inset:0!important` 会展开出同样重要的 `top:0`,普通声明赢不过它——这正是该问题在 v0.3.6 之后复发的原因。现已由测试锁死层叠结果,规范见仓库根目录 `AGENTS.md` 的「桌面版兼容」一节。
|
|
98
|
+
|
|
97
99
|
## Git 面板
|
|
98
100
|
|
|
99
101
|
左侧栏顶部有两个页签:**文件**(目录树)与 **Git**(源代码管理)。Git 页签按 VSCode 源代码管理面板的方式组织:仓库 → 分支 → 提交记录 → 该提交的改动文件。
|
package/lib/client.js
CHANGED
|
@@ -41,17 +41,6 @@ window.__ModuleLoader__.load({
|
|
|
41
41
|
const IconRefreshOutline16 = primitives.IconRefreshOutline16 || primitives.IconRefreshOutlineRegular;
|
|
42
42
|
|
|
43
43
|
// 统一风格的矢量线性图标 (16x16, 统一 currentColor 与线条粗细)
|
|
44
|
-
function SvgIconWrap({ size = 15 }) {
|
|
45
|
-
return react.createElement("svg", {
|
|
46
|
-
width: size, height: size, viewBox: "0 0 16 16", fill: "none",
|
|
47
|
-
stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round"
|
|
48
|
-
},
|
|
49
|
-
react.createElement("path", { d: "M2.5 4h11" }),
|
|
50
|
-
react.createElement("path", { d: "M2.5 8h7.5a2.5 2.5 0 0 1 2.5 2.5v0a2.5 2.5 0 0 1-2.5 2.5H5" }),
|
|
51
|
-
react.createElement("path", { d: "M7 10.5L4.5 13 7 15.5" })
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
44
|
/** Outline / table-of-contents glyph: short bars stepping in by level. */
|
|
56
45
|
function SvgIconOutline({ size = 15 }) {
|
|
57
46
|
return react.createElement("svg", {
|
|
@@ -219,9 +208,19 @@ window.__ModuleLoader__.load({
|
|
|
219
208
|
// Host ReadBlock/JsonTree now require localized copy for banners, copy
|
|
220
209
|
// buttons and expand/collapse. Missing these throws during render:
|
|
221
210
|
// "Cannot read properties of undefined (reading 'copy'|'window')".
|
|
211
|
+
// The host's ReadBlock banner renders a wrap toggle beside the copy button
|
|
212
|
+
// and names it from `labels.wrapLabel` / `labels.unwrapLabel`. Those two
|
|
213
|
+
// keys are easy to miss, and a blanket "复制" fallback here made the wrap
|
|
214
|
+
// toggle announce itself as "复制" — the wrong control with the wrong verb.
|
|
215
|
+
// Every key the banner needs is therefore named explicitly, and an unknown
|
|
216
|
+
// key now resolves to an empty string instead of borrowing another
|
|
217
|
+
// control's label.
|
|
222
218
|
const READ_BLOCK_LABELS = new Proxy({
|
|
223
219
|
copy: "复制",
|
|
224
220
|
copied: "已复制",
|
|
221
|
+
copyFailed: "复制失败",
|
|
222
|
+
wrapLabel: "自动换行",
|
|
223
|
+
unwrapLabel: "取消自动换行",
|
|
225
224
|
window: (shown, total) => `第 ${shown} / ${total} 行`,
|
|
226
225
|
collapse: "收起",
|
|
227
226
|
expand: (hidden) => `展开其余 ${hidden} 行`,
|
|
@@ -233,7 +232,7 @@ window.__ModuleLoader__.load({
|
|
|
233
232
|
if (typeof prop === "string" && (prop === "window" || prop === "expand" || prop.endsWith("Aria"))) {
|
|
234
233
|
return (...args) => String(args[0] ?? "");
|
|
235
234
|
}
|
|
236
|
-
return "
|
|
235
|
+
return "";
|
|
237
236
|
}
|
|
238
237
|
});
|
|
239
238
|
const JSON_TREE_LABELS = new Proxy({
|
|
@@ -261,6 +260,9 @@ window.__ModuleLoader__.load({
|
|
|
261
260
|
const DIFF_BLOCK_LABELS = new Proxy({
|
|
262
261
|
copy: "复制",
|
|
263
262
|
copied: "已复制",
|
|
263
|
+
copyFailed: "复制失败",
|
|
264
|
+
wrapLabel: "自动换行",
|
|
265
|
+
unwrapLabel: "取消自动换行",
|
|
264
266
|
files: (count) => `${count} 个文件`,
|
|
265
267
|
expand: (hidden) => `展开其余 ${hidden} 行`,
|
|
266
268
|
collapse: "收起",
|
|
@@ -272,7 +274,7 @@ window.__ModuleLoader__.load({
|
|
|
272
274
|
if (typeof prop === "string" && (prop === "files" || prop === "expand" || prop.endsWith("Aria"))) {
|
|
273
275
|
return (...args) => String(args[0] ?? "");
|
|
274
276
|
}
|
|
275
|
-
return "
|
|
277
|
+
return "";
|
|
276
278
|
}
|
|
277
279
|
});
|
|
278
280
|
|
|
@@ -367,6 +369,15 @@ window.__ModuleLoader__.load({
|
|
|
367
369
|
const DEFAULT_GIT_CHANGES_HEIGHT = 180;
|
|
368
370
|
const MIN_GIT_CHANGES_HEIGHT = 64;
|
|
369
371
|
const MAX_GIT_CHANGES_HEIGHT = 800;
|
|
372
|
+
// Reading size for the preview. A file's own text is what the reader spends
|
|
373
|
+
// their time on, so it gets its own, remembered size instead of inheriting
|
|
374
|
+
// the chat's. Bounded so the control cannot produce an unreadable pane in
|
|
375
|
+
// either direction, and stored per browser like every other view pref.
|
|
376
|
+
const FONT_SIZE_KEY = "dsh-file-viewer.font-size";
|
|
377
|
+
const DEFAULT_FONT_SIZE = 13;
|
|
378
|
+
const MIN_FONT_SIZE = 11;
|
|
379
|
+
const MAX_FONT_SIZE = 20;
|
|
380
|
+
const FONT_SIZE_STEP = 1;
|
|
370
381
|
/**
|
|
371
382
|
* Smallest width the drawer may be dragged to.
|
|
372
383
|
*
|
|
@@ -432,8 +443,22 @@ window.__ModuleLoader__.load({
|
|
|
432
443
|
header controls could never be clicked. Start the drawer below the
|
|
433
444
|
strip instead, which also leaves the strip free to drag the window.
|
|
434
445
|
The host sets data-fullscreen while the window is fullscreen and the
|
|
435
|
-
strip is hidden, so the offset has to drop then.
|
|
436
|
-
|
|
446
|
+
strip is hidden, so the offset has to drop then.
|
|
447
|
+
|
|
448
|
+
The declaration carries !important on purpose. It is not defensive
|
|
449
|
+
padding: the drawer's own opt-out above neutralises the host's
|
|
450
|
+
modal-card treatment with an important inset, and the longhand top that
|
|
451
|
+
shorthand expands to outranks any normal declaration — including this
|
|
452
|
+
one. Without !important the strip wins and the drawer slides back under
|
|
453
|
+
the caption buttons, which is exactly the regression this rule was
|
|
454
|
+
written for (fixed in 0.2.8, broken again once 0.3.6 added the
|
|
455
|
+
important inset, because the guard test only checked that the rule
|
|
456
|
+
exists rather than that it wins the cascade).
|
|
457
|
+
|
|
458
|
+
The role="dialog" qualifier keeps this at least as specific as the inset
|
|
459
|
+
rule it has to beat, so the outcome is decided by the important flag and
|
|
460
|
+
the attribute count rather than by sheet order. */
|
|
461
|
+
html[data-windows-titlebar]:not([data-fullscreen]) .fv-scrim[role="dialog"]{top:var(--dsh-windows-titlebar-height,40px)!important}
|
|
437
462
|
@keyframes fv-fade{from{opacity:0}to{opacity:1}}
|
|
438
463
|
@keyframes fv-slide{from{transform:translateX(100%)}to{transform:translateX(0)}}
|
|
439
464
|
@media(prefers-reduced-motion:reduce){.fv-scrim,.fv-shell{animation:none}}
|
|
@@ -568,6 +593,24 @@ window.__ModuleLoader__.load({
|
|
|
568
593
|
.fv-sheet-tabs-end{display:inline-flex;align-items:center;gap:8px;margin-left:auto;flex:none}
|
|
569
594
|
.fv-sheet-tabs .fv-btn-back{width:26px;height:26px;font-size:16px;line-height:24px;border-radius:6px;margin-right:2px}
|
|
570
595
|
.fv-main-size{font-size:11px;color:var(--dsw-alias-label-caption);white-space:nowrap}
|
|
596
|
+
/* Reading-size stepper. Deliberately a compact group: the range is 11–20px,
|
|
597
|
+
so a slider would be all friction and no precision. */
|
|
598
|
+
.fv-font-size{display:inline-flex;align-items:center;gap:0;height:28px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,#fff));overflow:hidden;flex:none}
|
|
599
|
+
.fv-font-size-btn{display:inline-flex;align-items:center;justify-content:center;height:100%;min-width:30px;padding:0 8px;border:none;background:none;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;font-weight:600;line-height:1;cursor:pointer;transition:background-color .12s,color .12s}
|
|
600
|
+
.fv-font-size-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
601
|
+
.fv-font-size-btn:disabled{opacity:.38;cursor:default}
|
|
602
|
+
.fv-font-size-value{min-width:38px;padding:0 2px;text-align:center;color:var(--dsw-alias-label-caption);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
603
|
+
/* The size reaches the preview through one custom property, so every
|
|
604
|
+
viewer that renders text picks it up without repeating the value.
|
|
605
|
+
The host's code card declares the font shorthand, which resets both size
|
|
606
|
+
and line height; the structural selectors below carry one element plus
|
|
607
|
+
one attribute, so they outrank that single class and win regardless of
|
|
608
|
+
sheet order. Line height is re-declared beside the size, otherwise the
|
|
609
|
+
host's fixed value would make larger text overlap. */
|
|
610
|
+
.fv-content,.fv-content-wrap{font-size:var(--fv-font-size,13px)}
|
|
611
|
+
.fv-content div[class*="_card_"],.fv-content div[class*="_body_"]{font-size:var(--fv-font-size,13px);line-height:1.65}
|
|
612
|
+
.fv-content div[class*="_line_"]{font-size:var(--fv-font-size,13px);line-height:1.65}
|
|
613
|
+
.fv-content div[class*="_line_"] span[class*="_content_"]{font-size:inherit;line-height:inherit}
|
|
571
614
|
.fv-content .fv-sheet-tabs{position:sticky;top:0;margin:-12px -12px 10px;padding:12px 12px 8px}
|
|
572
615
|
.fv-tab{height:28px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:none;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}
|
|
573
616
|
.fv-tab[aria-selected="true"]{border-color:transparent;background:var(--dsw-alias-brand-primary,#4d6bfe);color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
@@ -591,7 +634,7 @@ window.__ModuleLoader__.load({
|
|
|
591
634
|
.fv-diff-view{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-base,#fff);overflow:hidden;font-family:var(--dsw-font-code,monospace);margin:4px 0}
|
|
592
635
|
.fv-diff-toolbar{display:flex;align-items:center;padding:8px 12px;background:var(--dsw-alias-bg-layer-1,rgba(0,0,0,.02));border-bottom:1px solid var(--dsw-alias-border-l1);font-family:var(--dsw-font-family);font-size:12px}
|
|
593
636
|
.fv-diff-source-tag{font-size:11px;font-weight:600;padding:2px 8px;border-radius:6px;background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4d6bfe) 12%,transparent);color:var(--dsw-alias-brand-primary,#4d6bfe)}
|
|
594
|
-
.fv-diff-table{display:flex;flex-direction:column;overflow-x:auto;min-width:0;font-size:12px;line-height:
|
|
637
|
+
.fv-diff-table{display:flex;flex-direction:column;overflow-x:auto;min-width:0;font-size:var(--fv-font-size,12px);line-height:1.65;font-family:var(--dsw-font-code,monospace)}
|
|
595
638
|
.fv-diff-line{display:flex;min-width:max-content;align-items:stretch;transition:background-color .08s}
|
|
596
639
|
.fv-diff-num{flex:none;width:44px;padding:0 6px;text-align:right;user-select:none;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-caption);opacity:.65;font-size:11px}
|
|
597
640
|
.fv-diff-num-new{border-right:1px solid var(--dsw-alias-border-l1)}
|
|
@@ -724,6 +767,10 @@ window.__ModuleLoader__.load({
|
|
|
724
767
|
.fv-content[data-wrap="on"] div[class*="_line_"]{align-items:flex-start}
|
|
725
768
|
.fv-content[data-wrap="on"] span[class*="_gutter_"]{flex:none;white-space:pre}
|
|
726
769
|
.fv-content[data-wrap="on"] span[class*="_content_"]{min-width:0;flex:1 1 auto;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
770
|
+
/* Tooltip 气泡:浅色模式下防止宿主静态白色文字在浅色背景上隐形 */
|
|
771
|
+
span[class*="_bubble_"],
|
|
772
|
+
[data-portal] span[class*="_bubble_"]{color:var(--dsw-alias-label-primary,#0f1115)!important;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.08))!important;box-shadow:var(--dsw-shadow-lv2,0 2px 8px rgba(0,0,0,.12))!important}
|
|
773
|
+
span[class*="_bubble_"] span[class*="_label_"]{color:inherit!important}
|
|
727
774
|
.fv-context-backdrop{position:fixed;inset:0;z-index:99;background:transparent!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important;touch-action:none}
|
|
728
775
|
/* 菜单是实底卡片,不吃主题的毛玻璃。
|
|
729
776
|
玻璃主题(glass.css)里有一条规则,给 [role="menu"] 套上
|
|
@@ -1213,6 +1260,42 @@ window.__ModuleLoader__.load({
|
|
|
1213
1260
|
} catch { /* storage is unavailable */ }
|
|
1214
1261
|
}
|
|
1215
1262
|
|
|
1263
|
+
/**
|
|
1264
|
+
* The preview's reading size, clamped on read.
|
|
1265
|
+
*
|
|
1266
|
+
* Storage survives upgrades and can be hand-edited, so an out-of-range
|
|
1267
|
+
* value falls back to the default rather than producing a pane nobody can
|
|
1268
|
+
* read (or one so large the code never fits).
|
|
1269
|
+
* @returns {number} a font size in px.
|
|
1270
|
+
*/
|
|
1271
|
+
function readFontSize() {
|
|
1272
|
+
try {
|
|
1273
|
+
const raw = globalThis.localStorage?.getItem(FONT_SIZE_KEY);
|
|
1274
|
+
const parsed = Number(raw);
|
|
1275
|
+
if (Number.isFinite(parsed) && parsed >= MIN_FONT_SIZE && parsed <= MAX_FONT_SIZE) {
|
|
1276
|
+
return Math.round(parsed);
|
|
1277
|
+
}
|
|
1278
|
+
} catch { /* storage is unavailable */ }
|
|
1279
|
+
return DEFAULT_FONT_SIZE;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function writeFontSize(size) {
|
|
1283
|
+
try {
|
|
1284
|
+
if (typeof size === "number" && Number.isFinite(size)) {
|
|
1285
|
+
globalThis.localStorage?.setItem(FONT_SIZE_KEY, String(Math.round(size)));
|
|
1286
|
+
} else {
|
|
1287
|
+
globalThis.localStorage?.removeItem(FONT_SIZE_KEY);
|
|
1288
|
+
}
|
|
1289
|
+
} catch { /* storage is unavailable */ }
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/** Clamp a requested size onto the supported range. */
|
|
1293
|
+
function clampFontSize(size) {
|
|
1294
|
+
const value = Number(size);
|
|
1295
|
+
if (!Number.isFinite(value)) return DEFAULT_FONT_SIZE;
|
|
1296
|
+
return Math.round(Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, value)));
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1216
1299
|
/**
|
|
1217
1300
|
* Read the per-file language overrides.
|
|
1218
1301
|
*
|
|
@@ -2165,6 +2248,10 @@ window.__ModuleLoader__.load({
|
|
|
2165
2248
|
total: 0,
|
|
2166
2249
|
hasMore: false,
|
|
2167
2250
|
});
|
|
2251
|
+
// The working tree's pending changes belong on screen immediately;
|
|
2252
|
+
// load them alongside the first page of history so the section does
|
|
2253
|
+
// not stay on "loading" waiting for an explicit click.
|
|
2254
|
+
loadChanges();
|
|
2168
2255
|
// Returned so a caller that awaits loadRepos also observes the first
|
|
2169
2256
|
// page of history, which is what makes the panel testable end to end.
|
|
2170
2257
|
return loadCommits(store.get().branch, 0);
|
|
@@ -2211,6 +2298,7 @@ window.__ModuleLoader__.load({
|
|
|
2211
2298
|
detailStatus: "idle",
|
|
2212
2299
|
notice: "",
|
|
2213
2300
|
});
|
|
2301
|
+
loadChanges();
|
|
2214
2302
|
return loadCommits(store.get().branch, 0);
|
|
2215
2303
|
};
|
|
2216
2304
|
|
|
@@ -3759,6 +3847,52 @@ window.__ModuleLoader__.load({
|
|
|
3759
3847
|
: null);
|
|
3760
3848
|
}
|
|
3761
3849
|
|
|
3850
|
+
/**
|
|
3851
|
+
* Reading-size stepper for the preview pane.
|
|
3852
|
+
*
|
|
3853
|
+
* A file's own text is what the reader spends their time on, so its size is
|
|
3854
|
+
* adjustable on its own and remembered per browser. The control is a
|
|
3855
|
+
* minus/value/plus group rather than a slider: the useful range is small and
|
|
3856
|
+
* discrete, and a slider at this width would be impossible to hit.
|
|
3857
|
+
*
|
|
3858
|
+
* @param {{value: number, onChange: Function, disabled?: boolean}} props
|
|
3859
|
+
*/
|
|
3860
|
+
function FontSizeControl({ value, onChange, disabled }) {
|
|
3861
|
+
const size = clampFontSize(value);
|
|
3862
|
+
const atMin = size <= MIN_FONT_SIZE;
|
|
3863
|
+
const atMax = size >= MAX_FONT_SIZE;
|
|
3864
|
+
const step = (delta) => {
|
|
3865
|
+
const next = clampFontSize(size + delta);
|
|
3866
|
+
if (next !== size) onChange(next);
|
|
3867
|
+
};
|
|
3868
|
+
return react.createElement("div", {
|
|
3869
|
+
className: "fv-font-size",
|
|
3870
|
+
role: "group",
|
|
3871
|
+
"aria-label": "文件内字号",
|
|
3872
|
+
},
|
|
3873
|
+
react.createElement("button", {
|
|
3874
|
+
type: "button",
|
|
3875
|
+
className: "fv-font-size-btn",
|
|
3876
|
+
"aria-label": "减小字号",
|
|
3877
|
+
title: "减小字号",
|
|
3878
|
+
disabled: disabled === true || atMin,
|
|
3879
|
+
onClick: (e) => { e.stopPropagation(); step(-FONT_SIZE_STEP); },
|
|
3880
|
+
}, "A−"),
|
|
3881
|
+
react.createElement("span", {
|
|
3882
|
+
className: "fv-font-size-value",
|
|
3883
|
+
title: "当前字号",
|
|
3884
|
+
"aria-live": "polite",
|
|
3885
|
+
}, size + "px"),
|
|
3886
|
+
react.createElement("button", {
|
|
3887
|
+
type: "button",
|
|
3888
|
+
className: "fv-font-size-btn",
|
|
3889
|
+
"aria-label": "增大字号",
|
|
3890
|
+
title: "增大字号",
|
|
3891
|
+
disabled: disabled === true || atMax,
|
|
3892
|
+
onClick: (e) => { e.stopPropagation(); step(FONT_SIZE_STEP); },
|
|
3893
|
+
}, "A+"));
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3762
3896
|
function apply(ctx) {
|
|
3763
3897
|
ensureStyles(typeof document === "undefined" ? null : document);
|
|
3764
3898
|
// `null` is closed; an object is open. Carrying the requesting session lets
|
|
@@ -4254,7 +4388,7 @@ window.__ModuleLoader__.load({
|
|
|
4254
4388
|
|
|
4255
4389
|
/** Line-numbered diff viewer with dual line number gutters. */
|
|
4256
4390
|
/** Text, Markdown and JSON: one windowed read, three renderings. */
|
|
4257
|
-
function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast }) {
|
|
4391
|
+
function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast, fontSize, onFontSizeChange }) {
|
|
4258
4392
|
// Callers mount this with a per-file key, so paging state starts fresh
|
|
4259
4393
|
// for every file instead of leaking the previous file's window into the
|
|
4260
4394
|
// first request.
|
|
@@ -4544,17 +4678,23 @@ window.__ModuleLoader__.load({
|
|
|
4544
4678
|
? react.createElement(PathCopyDropdown, { meta, root, onToast })
|
|
4545
4679
|
: null;
|
|
4546
4680
|
|
|
4547
|
-
|
|
4681
|
+
// Reading size belongs with the content it resizes, not in the drawer
|
|
4682
|
+
// header: it applies to this pane, and the stepper is next to the text.
|
|
4683
|
+
const fontControl = react.createElement(FontSizeControl, {
|
|
4684
|
+
value: fontSize,
|
|
4685
|
+
onChange: onFontSizeChange,
|
|
4686
|
+
});
|
|
4687
|
+
|
|
4688
|
+
const tabs = showTabs || Boolean(backControl || langControl || sizeControl || copyControl || fontControl)
|
|
4548
4689
|
? react.createElement("div", { className: "fv-sheet-tabs", role: "tablist" },
|
|
4549
4690
|
react.createElement("div", { className: "fv-sheet-tabs-start" },
|
|
4550
4691
|
backControl,
|
|
4551
4692
|
...tabButtons,
|
|
4552
4693
|
copyControl),
|
|
4553
|
-
(
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
: null)
|
|
4694
|
+
react.createElement("div", { className: "fv-sheet-tabs-end" },
|
|
4695
|
+
fontControl,
|
|
4696
|
+
sizeControl,
|
|
4697
|
+
langControl))
|
|
4558
4698
|
: null;
|
|
4559
4699
|
|
|
4560
4700
|
|
|
@@ -4580,7 +4720,13 @@ window.__ModuleLoader__.load({
|
|
|
4580
4720
|
|
|
4581
4721
|
return react.createElement(react.Fragment, null,
|
|
4582
4722
|
tabs,
|
|
4583
|
-
react.createElement("div", {
|
|
4723
|
+
react.createElement("div", {
|
|
4724
|
+
className: "fv-content-wrap",
|
|
4725
|
+
// The size reaches the rendered text through one inherited custom
|
|
4726
|
+
// property, so Markdown, JSON and plain source all follow it without
|
|
4727
|
+
// each view having to know about the setting.
|
|
4728
|
+
style: { "--fv-font-size": fontSize + "px" },
|
|
4729
|
+
},
|
|
4584
4730
|
react.createElement("div", {
|
|
4585
4731
|
className: "fv-content",
|
|
4586
4732
|
"data-wrap": wrapping ? "on" : "off",
|
|
@@ -4621,10 +4767,18 @@ window.__ModuleLoader__.load({
|
|
|
4621
4767
|
}
|
|
4622
4768
|
|
|
4623
4769
|
/** Pick the viewer for a file and keep unsupported formats useful. */
|
|
4624
|
-
function FileView({ meta, root, api, wrap, lang, onDownload, commit, repo, loading, backControl, sizeControl, langControl, onToast }) {
|
|
4770
|
+
function FileView({ meta, root, api, wrap, lang, onDownload, commit, repo, loading, backControl, sizeControl, langControl, onToast, fontSize, onFontSizeChange }) {
|
|
4625
4771
|
const copyControl = meta !== null
|
|
4626
4772
|
? react.createElement(PathCopyDropdown, { meta, root, onToast })
|
|
4627
4773
|
: null;
|
|
4774
|
+
// Only views that render text have a size to adjust; an image or a PDF
|
|
4775
|
+
// has its own zoom, and a sheet's cells are not prose.
|
|
4776
|
+
const textish = meta !== null
|
|
4777
|
+
&& !meta.tooLarge
|
|
4778
|
+
&& (meta.kind === "text" || meta.kind === "markdown" || meta.kind === "json");
|
|
4779
|
+
const fontControl = textish
|
|
4780
|
+
? react.createElement(FontSizeControl, { value: fontSize, onChange: onFontSizeChange })
|
|
4781
|
+
: null;
|
|
4628
4782
|
|
|
4629
4783
|
if (meta === null) {
|
|
4630
4784
|
// A tab exists but its metadata is still in flight. Saying "pick a
|
|
@@ -4672,10 +4826,10 @@ window.__ModuleLoader__.load({
|
|
|
4672
4826
|
react.createElement("span", { className: "fv-history-hint" }, "这是历史版本,不是工作区当前内容。"))
|
|
4673
4827
|
: null;
|
|
4674
4828
|
|
|
4675
|
-
const headBar = (backControl || sizeControl || copyControl)
|
|
4829
|
+
const headBar = (backControl || sizeControl || copyControl || fontControl)
|
|
4676
4830
|
? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
|
|
4677
4831
|
react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl, copyControl),
|
|
4678
|
-
|
|
4832
|
+
react.createElement("div", { className: "fv-sheet-tabs-end" }, fontControl, sizeControl))
|
|
4679
4833
|
: null;
|
|
4680
4834
|
|
|
4681
4835
|
if (fromHistory && meta.kind !== "text" && meta.kind !== "markdown" && meta.kind !== "json") {
|
|
@@ -4692,7 +4846,7 @@ window.__ModuleLoader__.load({
|
|
|
4692
4846
|
return react.createElement("div", { className: "fv-content" }, headBar, react.createElement(DocView, { key, meta, root }));
|
|
4693
4847
|
}
|
|
4694
4848
|
return react.createElement(react.Fragment, null, banner,
|
|
4695
|
-
react.createElement(TextView, { key, meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast }));
|
|
4849
|
+
react.createElement(TextView, { key, meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast, fontSize, onFontSizeChange }));
|
|
4696
4850
|
}
|
|
4697
4851
|
|
|
4698
4852
|
|
|
@@ -5133,9 +5287,22 @@ window.__ModuleLoader__.load({
|
|
|
5133
5287
|
setShellWidth(clamped);
|
|
5134
5288
|
writeShellWidth(clamped);
|
|
5135
5289
|
};
|
|
5136
|
-
// Soft wrap
|
|
5137
|
-
//
|
|
5138
|
-
|
|
5290
|
+
// Soft wrap is owned entirely by the host ReadBlock banner, which
|
|
5291
|
+
// renders its own toggle beside the copy button and reports the state
|
|
5292
|
+
// through `labels.wrapLabel` / `labels.unwrapLabel`. The viewer used to
|
|
5293
|
+
// keep a second, parallel switch in its header; two controls for one
|
|
5294
|
+
// flag could disagree, so the header's is gone and this flag is no
|
|
5295
|
+
// longer state — there is nothing left to toggle from here.
|
|
5296
|
+
const wrap = false;
|
|
5297
|
+
// Reading size for the preview, remembered per browser. Kept here rather
|
|
5298
|
+
// than inside TextView because the stepper sits in the pane's toolbar
|
|
5299
|
+
// while the text it sizes lives one level down.
|
|
5300
|
+
const [fontSize, setFontSize] = react.useState(readFontSize);
|
|
5301
|
+
const changeFontSize = (next) => {
|
|
5302
|
+
const clamped = clampFontSize(next);
|
|
5303
|
+
setFontSize(clamped);
|
|
5304
|
+
writeFontSize(clamped);
|
|
5305
|
+
};
|
|
5139
5306
|
// The language override now lives on the tab it applies to (see
|
|
5140
5307
|
// `localeTagOfActive`), so switching tabs switches the pin with it.
|
|
5141
5308
|
const [fullscreen, setFullscreen] = react.useState(false);
|
|
@@ -5250,10 +5417,9 @@ window.__ModuleLoader__.load({
|
|
|
5250
5417
|
// tab, and discovery spawns a git process per candidate repository.
|
|
5251
5418
|
react.useEffect(() => {
|
|
5252
5419
|
if (!open || side !== "git" || root === "") return;
|
|
5253
|
-
git.ensure()
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
git.loadChanges();
|
|
5420
|
+
git.ensure().then(() => {
|
|
5421
|
+
git.loadChanges();
|
|
5422
|
+
}).catch(() => {});
|
|
5257
5423
|
}, [open, side, root]);
|
|
5258
5424
|
const [toast, setToast] = react.useState(null);
|
|
5259
5425
|
const toastTimer = react.useRef(null);
|
|
@@ -5504,18 +5670,16 @@ window.__ModuleLoader__.load({
|
|
|
5504
5670
|
treeGeneration.current += 1;
|
|
5505
5671
|
setNodes(new Map());
|
|
5506
5672
|
if (rootChanged) {
|
|
5507
|
-
//
|
|
5508
|
-
//
|
|
5509
|
-
// the
|
|
5510
|
-
//
|
|
5511
|
-
//
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
setTabMenu(null);
|
|
5673
|
+
// Only the tree is rebuilt here. Tabs are deliberately NOT touched:
|
|
5674
|
+
// this effect re-runs on the very commit that resolved `root`, i.e.
|
|
5675
|
+
// *after* the roots effect above has already cleared the previous
|
|
5676
|
+
// visit's tabs and opened the one the arrival path asked for.
|
|
5677
|
+
// Clearing them here wiped that brand-new tab, so the first click on
|
|
5678
|
+
// a file in a conversation (a path or a changes card) opened an empty
|
|
5679
|
+
// pane and only the second click — now that `treeBuiltForRef` and
|
|
5680
|
+
// `visitContextRef` both already name this root — loaded anything.
|
|
5681
|
+
// A root switched from the picker resets tabs in `chooseRoot`.
|
|
5517
5682
|
setExpanded(new Set());
|
|
5518
|
-
setPane("tree");
|
|
5519
5683
|
}
|
|
5520
5684
|
const chain = revealChanged && reveal !== "" ? [...ancestorsOf(reveal), reveal] : [];
|
|
5521
5685
|
if (chain.length > 0) {
|
|
@@ -5668,6 +5832,35 @@ window.__ModuleLoader__.load({
|
|
|
5668
5832
|
}
|
|
5669
5833
|
};
|
|
5670
5834
|
|
|
5835
|
+
/**
|
|
5836
|
+
* Switch the root from the header picker.
|
|
5837
|
+
*
|
|
5838
|
+
* A root switch is the one thing that invalidates every open tab: their
|
|
5839
|
+
* ids and paths only mean something inside the root that opened them, so
|
|
5840
|
+
* carrying them over would point at a different file with the same name.
|
|
5841
|
+
* The reset lives here, on the reader's own action, rather than in the
|
|
5842
|
+
* tree-building effect — that effect also re-runs on the commit that
|
|
5843
|
+
* resolves `root` for an arrival, and clearing there wiped the tab the
|
|
5844
|
+
* arrival had just opened (first click on a file did nothing, second one
|
|
5845
|
+
* worked because the guards had caught up by then).
|
|
5846
|
+
*/
|
|
5847
|
+
const chooseRoot = (next) => {
|
|
5848
|
+
const resolved = typeof next === "string" ? next : "";
|
|
5849
|
+
if (resolved === "" || resolved === root) return;
|
|
5850
|
+
tabsRef.current = null;
|
|
5851
|
+
setTabs(null);
|
|
5852
|
+
activeTabIdRef.current = "";
|
|
5853
|
+
setActiveTabId("");
|
|
5854
|
+
setTabMenu(null);
|
|
5855
|
+
setPane("tree");
|
|
5856
|
+
setExpanded(new Set());
|
|
5857
|
+
// The arrival context belongs to the root that is being left; naming it
|
|
5858
|
+
// here keeps the next roots response from treating the switch as an
|
|
5859
|
+
// arrival that has to be reset all over again.
|
|
5860
|
+
visitContextRef.current = resolved;
|
|
5861
|
+
setRoot(resolved);
|
|
5862
|
+
};
|
|
5863
|
+
|
|
5671
5864
|
/** Patch one tab by id. Used by asynchronous reads that outlive a switch. */
|
|
5672
5865
|
const patchTab = (id, mutator) => {
|
|
5673
5866
|
const current = tabList();
|
|
@@ -5773,30 +5966,12 @@ window.__ModuleLoader__.load({
|
|
|
5773
5966
|
}).catch(() => {});
|
|
5774
5967
|
};
|
|
5775
5968
|
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
* they care about unchanged.
|
|
5783
|
-
*/
|
|
5784
|
-
const refresh = () => {
|
|
5785
|
-
reloadWorkingTree();
|
|
5786
|
-
if (side === "git") {
|
|
5787
|
-
const state = git.store.get();
|
|
5788
|
-
// The commit to come back to belongs to the tab on screen, not to
|
|
5789
|
-
// whichever detail the controller happens to hold.
|
|
5790
|
-
const keepSha = activeTab !== null && activeTab.kind === "commit"
|
|
5791
|
-
? activeTab.id.slice("commit:".length)
|
|
5792
|
-
: "";
|
|
5793
|
-
if (state.root !== root) git.reset(root);
|
|
5794
|
-
git.loadRepos().then(() => {
|
|
5795
|
-
if (keepSha !== "") git.openCommit(keepSha);
|
|
5796
|
-
});
|
|
5797
|
-
}
|
|
5798
|
-
if (activeTabId !== "") refreshTab(activeTabId);
|
|
5799
|
-
};
|
|
5969
|
+
// Refreshing is offered per subject now: the tab's own context menu
|
|
5970
|
+
// re-reads the file/commit on screen, the Git panel has its own
|
|
5971
|
+
// "refresh working tree" control, and the sidebar re-lists a directory
|
|
5972
|
+
// when it is reopened. The old header button duplicated all three at
|
|
5973
|
+
// once, which made a single click re-read things the reader was not
|
|
5974
|
+
// looking at.
|
|
5800
5975
|
|
|
5801
5976
|
/** Open one commit in its own tab, or activate the tab already showing it. */
|
|
5802
5977
|
const openCommit = (commit) => {
|
|
@@ -6198,25 +6373,17 @@ window.__ModuleLoader__.load({
|
|
|
6198
6373
|
className: "fv-root-select",
|
|
6199
6374
|
value: root,
|
|
6200
6375
|
"aria-label": "工作区与安全路径",
|
|
6201
|
-
onChange: (event) =>
|
|
6376
|
+
onChange: (event) => chooseRoot(event.target.value),
|
|
6202
6377
|
}, ...renderRootOptions(roots))
|
|
6203
6378
|
: react.createElement("span", { className: "fv-crumbs" }, activeRoot ? activeRoot.label : ""),
|
|
6204
6379
|
// The tree shows structure, so the header carries the selected
|
|
6205
6380
|
// file's path instead of navigable breadcrumbs.
|
|
6206
6381
|
react.createElement("div", { className: "fv-crumbs fv-file-path", title: showCommit ? "" : (meta === null ? "" : meta.path) },
|
|
6207
6382
|
showCommit ? "" : (meta === null ? "" : meta.path)),
|
|
6208
|
-
//
|
|
6209
|
-
//
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
type: "button",
|
|
6213
|
-
className: "fv-icon-button",
|
|
6214
|
-
"aria-pressed": wrap ? "true" : "false",
|
|
6215
|
-
"aria-label": wrap ? "取消自动换行" : "自动换行",
|
|
6216
|
-
title: wrap ? "取消自动换行" : "自动换行",
|
|
6217
|
-
onClick: () => setWrap((current) => !current),
|
|
6218
|
-
}, react.createElement(SvgIconWrap, { size: 15 }))
|
|
6219
|
-
: null,
|
|
6383
|
+
// Soft wrap is offered by the code banner itself (next to its copy
|
|
6384
|
+
// button), so the header no longer carries a second toggle for the
|
|
6385
|
+
// same setting — two switches for one flag only invited them to
|
|
6386
|
+
// drift apart. Refresh lives in the tab's context menu likewise.
|
|
6220
6387
|
!showCommit && meta !== null && meta.kind !== "directory"
|
|
6221
6388
|
? react.createElement("button", {
|
|
6222
6389
|
type: "button",
|
|
@@ -6226,9 +6393,6 @@ window.__ModuleLoader__.load({
|
|
|
6226
6393
|
onClick: () => handleDownloadItem(meta),
|
|
6227
6394
|
}, react.createElement(SvgIconDownload, { size: 15 }))
|
|
6228
6395
|
: null,
|
|
6229
|
-
react.createElement("button", {
|
|
6230
|
-
type: "button", className: "fv-icon-button", "aria-label": "刷新", title: "刷新", onClick: refresh,
|
|
6231
|
-
}, react.createElement(SvgIconRefresh, { size: 15 })),
|
|
6232
6396
|
react.createElement("button", {
|
|
6233
6397
|
type: "button",
|
|
6234
6398
|
className: "fv-icon-button",
|
|
@@ -6412,6 +6576,8 @@ window.__ModuleLoader__.load({
|
|
|
6412
6576
|
sizeControl,
|
|
6413
6577
|
langControl,
|
|
6414
6578
|
onToast: showToast,
|
|
6579
|
+
fontSize,
|
|
6580
|
+
onFontSizeChange: changeFontSize,
|
|
6415
6581
|
})
|
|
6416
6582
|
: react.createElement(FileView, {
|
|
6417
6583
|
meta,
|
|
@@ -6431,6 +6597,8 @@ window.__ModuleLoader__.load({
|
|
|
6431
6597
|
sizeControl,
|
|
6432
6598
|
langControl,
|
|
6433
6599
|
onToast: showToast,
|
|
6600
|
+
fontSize,
|
|
6601
|
+
onFontSizeChange: changeFontSize,
|
|
6434
6602
|
})))),
|
|
6435
6603
|
contextMenu !== null
|
|
6436
6604
|
? react.createElement(ContextMenu, {
|
|
@@ -6770,6 +6938,7 @@ window.__ModuleLoader__.load({
|
|
|
6770
6938
|
MARKDOWN_LABELS, MARKDOWN_CODE_LABELS, READ_BLOCK_LABELS, JSON_TREE_LABELS, DIFF_BLOCK_LABELS, ErrorBoundary,
|
|
6771
6939
|
openViewerForPath, wrapWorkspaceOpenPath, wrapConnectionRpc, wrapChangesOpenFetch, wrapSidebarRight, parseFileResource, setupGlobalFileClickInterceptor, extractClickedPath, normalizeClickedPath, looksLikeFilePath, isFileOpenControl, isFileMentionCode, renderRootOptions,
|
|
6772
6940
|
triggerDownload, performDownload, isDesktopShell, SvgIconDownload, SvgIconCopy, PathCopyDropdown, resolveAbsPath, extractRelativeAndAbsolutePaths, isAbsolutePath, SvgIconBranch, SvgIconCommit,
|
|
6941
|
+
FONT_SIZE_KEY, DEFAULT_FONT_SIZE, MIN_FONT_SIZE, MAX_FONT_SIZE, FONT_SIZE_STEP, readFontSize, writeFontSize, clampFontSize, FontSizeControl,
|
|
6773
6942
|
// Git panel: controller, views and the small formatters they share.
|
|
6774
6943
|
GIT_PAGE_SIZE, createGitController, GitSidebar, CommitDetail, WorkspaceChangeDetail, DiffView, kindOfPath,
|
|
6775
6944
|
formatCommitDate, formatCommitTimestamp,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weniatall/dsh-file-viewer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"description": "Workspace file viewer and Git panel for DeepSeek Harness: file tree, syntax-highlighted code, images, PDF, Excel and Word, plus local/remote branches, paged commit history, commit diffs, working-tree changes and changed-file navigation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -70,6 +70,11 @@
|
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": ">=20.3"
|
|
72
72
|
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"jsdom": "29.1.1",
|
|
75
|
+
"react": "18.3.1",
|
|
76
|
+
"react-dom": "18.3.1"
|
|
77
|
+
},
|
|
73
78
|
"scripts": {
|
|
74
79
|
"test": "node --test test/*.test.mjs && node ../../scripts/check-host-settings.mjs plugins/dsh-file-viewer file-viewer"
|
|
75
80
|
}
|