@weniatall/dsh-file-viewer 0.3.13 → 0.3.17
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/lib/client.js +240 -79
- package/package.json +1 -1
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
|
*
|
|
@@ -568,6 +579,24 @@ window.__ModuleLoader__.load({
|
|
|
568
579
|
.fv-sheet-tabs-end{display:inline-flex;align-items:center;gap:8px;margin-left:auto;flex:none}
|
|
569
580
|
.fv-sheet-tabs .fv-btn-back{width:26px;height:26px;font-size:16px;line-height:24px;border-radius:6px;margin-right:2px}
|
|
570
581
|
.fv-main-size{font-size:11px;color:var(--dsw-alias-label-caption);white-space:nowrap}
|
|
582
|
+
/* Reading-size stepper. Deliberately a compact group: the range is 11–20px,
|
|
583
|
+
so a slider would be all friction and no precision. */
|
|
584
|
+
.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}
|
|
585
|
+
.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}
|
|
586
|
+
.fv-font-size-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
587
|
+
.fv-font-size-btn:disabled{opacity:.38;cursor:default}
|
|
588
|
+
.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}
|
|
589
|
+
/* The size reaches the preview through one custom property, so every
|
|
590
|
+
viewer that renders text picks it up without repeating the value.
|
|
591
|
+
The host's code card declares the font shorthand, which resets both size
|
|
592
|
+
and line height; the structural selectors below carry one element plus
|
|
593
|
+
one attribute, so they outrank that single class and win regardless of
|
|
594
|
+
sheet order. Line height is re-declared beside the size, otherwise the
|
|
595
|
+
host's fixed value would make larger text overlap. */
|
|
596
|
+
.fv-content,.fv-content-wrap{font-size:var(--fv-font-size,13px)}
|
|
597
|
+
.fv-content div[class*="_card_"],.fv-content div[class*="_body_"]{font-size:var(--fv-font-size,13px);line-height:1.65}
|
|
598
|
+
.fv-content div[class*="_line_"]{font-size:var(--fv-font-size,13px);line-height:1.65}
|
|
599
|
+
.fv-content div[class*="_line_"] span[class*="_content_"]{font-size:inherit;line-height:inherit}
|
|
571
600
|
.fv-content .fv-sheet-tabs{position:sticky;top:0;margin:-12px -12px 10px;padding:12px 12px 8px}
|
|
572
601
|
.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
602
|
.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 +620,7 @@ window.__ModuleLoader__.load({
|
|
|
591
620
|
.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
621
|
.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
622
|
.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:
|
|
623
|
+
.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
624
|
.fv-diff-line{display:flex;min-width:max-content;align-items:stretch;transition:background-color .08s}
|
|
596
625
|
.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
626
|
.fv-diff-num-new{border-right:1px solid var(--dsw-alias-border-l1)}
|
|
@@ -724,6 +753,10 @@ window.__ModuleLoader__.load({
|
|
|
724
753
|
.fv-content[data-wrap="on"] div[class*="_line_"]{align-items:flex-start}
|
|
725
754
|
.fv-content[data-wrap="on"] span[class*="_gutter_"]{flex:none;white-space:pre}
|
|
726
755
|
.fv-content[data-wrap="on"] span[class*="_content_"]{min-width:0;flex:1 1 auto;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
756
|
+
/* Tooltip 气泡:浅色模式下防止宿主静态白色文字在浅色背景上隐形 */
|
|
757
|
+
span[class*="_bubble_"],
|
|
758
|
+
[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}
|
|
759
|
+
span[class*="_bubble_"] span[class*="_label_"]{color:inherit!important}
|
|
727
760
|
.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
761
|
/* 菜单是实底卡片,不吃主题的毛玻璃。
|
|
729
762
|
玻璃主题(glass.css)里有一条规则,给 [role="menu"] 套上
|
|
@@ -1213,6 +1246,42 @@ window.__ModuleLoader__.load({
|
|
|
1213
1246
|
} catch { /* storage is unavailable */ }
|
|
1214
1247
|
}
|
|
1215
1248
|
|
|
1249
|
+
/**
|
|
1250
|
+
* The preview's reading size, clamped on read.
|
|
1251
|
+
*
|
|
1252
|
+
* Storage survives upgrades and can be hand-edited, so an out-of-range
|
|
1253
|
+
* value falls back to the default rather than producing a pane nobody can
|
|
1254
|
+
* read (or one so large the code never fits).
|
|
1255
|
+
* @returns {number} a font size in px.
|
|
1256
|
+
*/
|
|
1257
|
+
function readFontSize() {
|
|
1258
|
+
try {
|
|
1259
|
+
const raw = globalThis.localStorage?.getItem(FONT_SIZE_KEY);
|
|
1260
|
+
const parsed = Number(raw);
|
|
1261
|
+
if (Number.isFinite(parsed) && parsed >= MIN_FONT_SIZE && parsed <= MAX_FONT_SIZE) {
|
|
1262
|
+
return Math.round(parsed);
|
|
1263
|
+
}
|
|
1264
|
+
} catch { /* storage is unavailable */ }
|
|
1265
|
+
return DEFAULT_FONT_SIZE;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function writeFontSize(size) {
|
|
1269
|
+
try {
|
|
1270
|
+
if (typeof size === "number" && Number.isFinite(size)) {
|
|
1271
|
+
globalThis.localStorage?.setItem(FONT_SIZE_KEY, String(Math.round(size)));
|
|
1272
|
+
} else {
|
|
1273
|
+
globalThis.localStorage?.removeItem(FONT_SIZE_KEY);
|
|
1274
|
+
}
|
|
1275
|
+
} catch { /* storage is unavailable */ }
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/** Clamp a requested size onto the supported range. */
|
|
1279
|
+
function clampFontSize(size) {
|
|
1280
|
+
const value = Number(size);
|
|
1281
|
+
if (!Number.isFinite(value)) return DEFAULT_FONT_SIZE;
|
|
1282
|
+
return Math.round(Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, value)));
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1216
1285
|
/**
|
|
1217
1286
|
* Read the per-file language overrides.
|
|
1218
1287
|
*
|
|
@@ -2165,6 +2234,10 @@ window.__ModuleLoader__.load({
|
|
|
2165
2234
|
total: 0,
|
|
2166
2235
|
hasMore: false,
|
|
2167
2236
|
});
|
|
2237
|
+
// The working tree's pending changes belong on screen immediately;
|
|
2238
|
+
// load them alongside the first page of history so the section does
|
|
2239
|
+
// not stay on "loading" waiting for an explicit click.
|
|
2240
|
+
loadChanges();
|
|
2168
2241
|
// Returned so a caller that awaits loadRepos also observes the first
|
|
2169
2242
|
// page of history, which is what makes the panel testable end to end.
|
|
2170
2243
|
return loadCommits(store.get().branch, 0);
|
|
@@ -2211,6 +2284,7 @@ window.__ModuleLoader__.load({
|
|
|
2211
2284
|
detailStatus: "idle",
|
|
2212
2285
|
notice: "",
|
|
2213
2286
|
});
|
|
2287
|
+
loadChanges();
|
|
2214
2288
|
return loadCommits(store.get().branch, 0);
|
|
2215
2289
|
};
|
|
2216
2290
|
|
|
@@ -3629,15 +3703,50 @@ window.__ModuleLoader__.load({
|
|
|
3629
3703
|
: null);
|
|
3630
3704
|
}
|
|
3631
3705
|
|
|
3706
|
+
function isAbsolutePath(p) {
|
|
3707
|
+
if (typeof p !== "string" || p === "") return false;
|
|
3708
|
+
return /^[a-zA-Z]:[\\/]/.test(p) || p.startsWith("/") || p.startsWith("\\\\");
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3632
3711
|
function resolveAbsPath(rootPath, relPath) {
|
|
3633
3712
|
if (!rootPath) return relPath || "";
|
|
3634
|
-
const cleanRel = (relPath || "").replace(/^[\\/]+/, "");
|
|
3713
|
+
const cleanRel = (relPath || "").replace(/^\.[\\/]+/, "").replace(/^[\\/]+/, "");
|
|
3714
|
+
if (isAbsolutePath(cleanRel)) return cleanRel;
|
|
3635
3715
|
if (rootPath.includes("\\")) {
|
|
3636
3716
|
return rootPath.replace(/[\\/]+$/, "") + "\\" + cleanRel.replace(/\//g, "\\");
|
|
3637
3717
|
}
|
|
3638
3718
|
return rootPath.replace(/[\\/]+$/, "") + "/" + cleanRel.replace(/\\/g, "/");
|
|
3639
3719
|
}
|
|
3640
3720
|
|
|
3721
|
+
function extractRelativeAndAbsolutePaths(item, rootPath) {
|
|
3722
|
+
if (!item) return { relPath: "", absPath: "" };
|
|
3723
|
+
let rel = item.path || "";
|
|
3724
|
+
if (isAbsolutePath(rel)) {
|
|
3725
|
+
// 如果 path 意外传了绝对路径,且位于 rootPath 之下,提取相对路径
|
|
3726
|
+
const normRoot = rootPath ? rootPath.replace(/\\/g, "/").replace(/\/+$/, "") : "";
|
|
3727
|
+
const normRel = rel.replace(/\\/g, "/");
|
|
3728
|
+
if (normRoot && (normRel.toLowerCase().startsWith(normRoot.toLowerCase() + "/") || normRel.startsWith(normRoot + "/"))) {
|
|
3729
|
+
rel = normRel.slice(normRoot.length + 1);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
if (!rel) {
|
|
3733
|
+
rel = item.name || "";
|
|
3734
|
+
}
|
|
3735
|
+
const cleanRel = String(rel).replace(/^\.[\\/]+/, "").replace(/^[\\/]+/, "");
|
|
3736
|
+
|
|
3737
|
+
// 绝对路径:优先考虑真实宿主路径 displayPath,或由 rootPath 与相对路径拼接
|
|
3738
|
+
let abs = "";
|
|
3739
|
+
if (isAbsolutePath(item.displayPath)) {
|
|
3740
|
+
abs = item.displayPath;
|
|
3741
|
+
} else if (isAbsolutePath(item.path)) {
|
|
3742
|
+
abs = item.path;
|
|
3743
|
+
} else {
|
|
3744
|
+
abs = resolveAbsPath(rootPath, cleanRel);
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
return { relPath: cleanRel, absPath: abs };
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3641
3750
|
/** Path copy dropdown menu on viewer toolbar. */
|
|
3642
3751
|
function PathCopyDropdown({ meta, root, onToast }) {
|
|
3643
3752
|
const [open, setOpen] = react.useState(false);
|
|
@@ -3661,10 +3770,9 @@ window.__ModuleLoader__.load({
|
|
|
3661
3770
|
};
|
|
3662
3771
|
}, [open]);
|
|
3663
3772
|
|
|
3664
|
-
if (!meta || (!meta.path && !meta.displayPath)) return null;
|
|
3773
|
+
if (!meta || (!meta.path && !meta.displayPath && !meta.name)) return null;
|
|
3665
3774
|
|
|
3666
|
-
const relPath
|
|
3667
|
-
const absPath = resolveAbsPath(root, relPath);
|
|
3775
|
+
const { relPath, absPath } = extractRelativeAndAbsolutePaths(meta, root);
|
|
3668
3776
|
|
|
3669
3777
|
const handleCopy = (e, text, label) => {
|
|
3670
3778
|
e.preventDefault();
|
|
@@ -3725,6 +3833,52 @@ window.__ModuleLoader__.load({
|
|
|
3725
3833
|
: null);
|
|
3726
3834
|
}
|
|
3727
3835
|
|
|
3836
|
+
/**
|
|
3837
|
+
* Reading-size stepper for the preview pane.
|
|
3838
|
+
*
|
|
3839
|
+
* A file's own text is what the reader spends their time on, so its size is
|
|
3840
|
+
* adjustable on its own and remembered per browser. The control is a
|
|
3841
|
+
* minus/value/plus group rather than a slider: the useful range is small and
|
|
3842
|
+
* discrete, and a slider at this width would be impossible to hit.
|
|
3843
|
+
*
|
|
3844
|
+
* @param {{value: number, onChange: Function, disabled?: boolean}} props
|
|
3845
|
+
*/
|
|
3846
|
+
function FontSizeControl({ value, onChange, disabled }) {
|
|
3847
|
+
const size = clampFontSize(value);
|
|
3848
|
+
const atMin = size <= MIN_FONT_SIZE;
|
|
3849
|
+
const atMax = size >= MAX_FONT_SIZE;
|
|
3850
|
+
const step = (delta) => {
|
|
3851
|
+
const next = clampFontSize(size + delta);
|
|
3852
|
+
if (next !== size) onChange(next);
|
|
3853
|
+
};
|
|
3854
|
+
return react.createElement("div", {
|
|
3855
|
+
className: "fv-font-size",
|
|
3856
|
+
role: "group",
|
|
3857
|
+
"aria-label": "文件内字号",
|
|
3858
|
+
},
|
|
3859
|
+
react.createElement("button", {
|
|
3860
|
+
type: "button",
|
|
3861
|
+
className: "fv-font-size-btn",
|
|
3862
|
+
"aria-label": "减小字号",
|
|
3863
|
+
title: "减小字号",
|
|
3864
|
+
disabled: disabled === true || atMin,
|
|
3865
|
+
onClick: (e) => { e.stopPropagation(); step(-FONT_SIZE_STEP); },
|
|
3866
|
+
}, "A−"),
|
|
3867
|
+
react.createElement("span", {
|
|
3868
|
+
className: "fv-font-size-value",
|
|
3869
|
+
title: "当前字号",
|
|
3870
|
+
"aria-live": "polite",
|
|
3871
|
+
}, size + "px"),
|
|
3872
|
+
react.createElement("button", {
|
|
3873
|
+
type: "button",
|
|
3874
|
+
className: "fv-font-size-btn",
|
|
3875
|
+
"aria-label": "增大字号",
|
|
3876
|
+
title: "增大字号",
|
|
3877
|
+
disabled: disabled === true || atMax,
|
|
3878
|
+
onClick: (e) => { e.stopPropagation(); step(FONT_SIZE_STEP); },
|
|
3879
|
+
}, "A+"));
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3728
3882
|
function apply(ctx) {
|
|
3729
3883
|
ensureStyles(typeof document === "undefined" ? null : document);
|
|
3730
3884
|
// `null` is closed; an object is open. Carrying the requesting session lets
|
|
@@ -4220,7 +4374,7 @@ window.__ModuleLoader__.load({
|
|
|
4220
4374
|
|
|
4221
4375
|
/** Line-numbered diff viewer with dual line number gutters. */
|
|
4222
4376
|
/** Text, Markdown and JSON: one windowed read, three renderings. */
|
|
4223
|
-
function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast }) {
|
|
4377
|
+
function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast, fontSize, onFontSizeChange }) {
|
|
4224
4378
|
// Callers mount this with a per-file key, so paging state starts fresh
|
|
4225
4379
|
// for every file instead of leaking the previous file's window into the
|
|
4226
4380
|
// first request.
|
|
@@ -4510,17 +4664,23 @@ window.__ModuleLoader__.load({
|
|
|
4510
4664
|
? react.createElement(PathCopyDropdown, { meta, root, onToast })
|
|
4511
4665
|
: null;
|
|
4512
4666
|
|
|
4513
|
-
|
|
4667
|
+
// Reading size belongs with the content it resizes, not in the drawer
|
|
4668
|
+
// header: it applies to this pane, and the stepper is next to the text.
|
|
4669
|
+
const fontControl = react.createElement(FontSizeControl, {
|
|
4670
|
+
value: fontSize,
|
|
4671
|
+
onChange: onFontSizeChange,
|
|
4672
|
+
});
|
|
4673
|
+
|
|
4674
|
+
const tabs = showTabs || Boolean(backControl || langControl || sizeControl || copyControl || fontControl)
|
|
4514
4675
|
? react.createElement("div", { className: "fv-sheet-tabs", role: "tablist" },
|
|
4515
4676
|
react.createElement("div", { className: "fv-sheet-tabs-start" },
|
|
4516
4677
|
backControl,
|
|
4517
4678
|
...tabButtons,
|
|
4518
4679
|
copyControl),
|
|
4519
|
-
(
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
: null)
|
|
4680
|
+
react.createElement("div", { className: "fv-sheet-tabs-end" },
|
|
4681
|
+
fontControl,
|
|
4682
|
+
sizeControl,
|
|
4683
|
+
langControl))
|
|
4524
4684
|
: null;
|
|
4525
4685
|
|
|
4526
4686
|
|
|
@@ -4546,7 +4706,13 @@ window.__ModuleLoader__.load({
|
|
|
4546
4706
|
|
|
4547
4707
|
return react.createElement(react.Fragment, null,
|
|
4548
4708
|
tabs,
|
|
4549
|
-
react.createElement("div", {
|
|
4709
|
+
react.createElement("div", {
|
|
4710
|
+
className: "fv-content-wrap",
|
|
4711
|
+
// The size reaches the rendered text through one inherited custom
|
|
4712
|
+
// property, so Markdown, JSON and plain source all follow it without
|
|
4713
|
+
// each view having to know about the setting.
|
|
4714
|
+
style: { "--fv-font-size": fontSize + "px" },
|
|
4715
|
+
},
|
|
4550
4716
|
react.createElement("div", {
|
|
4551
4717
|
className: "fv-content",
|
|
4552
4718
|
"data-wrap": wrapping ? "on" : "off",
|
|
@@ -4587,10 +4753,18 @@ window.__ModuleLoader__.load({
|
|
|
4587
4753
|
}
|
|
4588
4754
|
|
|
4589
4755
|
/** Pick the viewer for a file and keep unsupported formats useful. */
|
|
4590
|
-
function FileView({ meta, root, api, wrap, lang, onDownload, commit, repo, loading, backControl, sizeControl, langControl, onToast }) {
|
|
4756
|
+
function FileView({ meta, root, api, wrap, lang, onDownload, commit, repo, loading, backControl, sizeControl, langControl, onToast, fontSize, onFontSizeChange }) {
|
|
4591
4757
|
const copyControl = meta !== null
|
|
4592
4758
|
? react.createElement(PathCopyDropdown, { meta, root, onToast })
|
|
4593
4759
|
: null;
|
|
4760
|
+
// Only views that render text have a size to adjust; an image or a PDF
|
|
4761
|
+
// has its own zoom, and a sheet's cells are not prose.
|
|
4762
|
+
const textish = meta !== null
|
|
4763
|
+
&& !meta.tooLarge
|
|
4764
|
+
&& (meta.kind === "text" || meta.kind === "markdown" || meta.kind === "json");
|
|
4765
|
+
const fontControl = textish
|
|
4766
|
+
? react.createElement(FontSizeControl, { value: fontSize, onChange: onFontSizeChange })
|
|
4767
|
+
: null;
|
|
4594
4768
|
|
|
4595
4769
|
if (meta === null) {
|
|
4596
4770
|
// A tab exists but its metadata is still in flight. Saying "pick a
|
|
@@ -4638,10 +4812,10 @@ window.__ModuleLoader__.load({
|
|
|
4638
4812
|
react.createElement("span", { className: "fv-history-hint" }, "这是历史版本,不是工作区当前内容。"))
|
|
4639
4813
|
: null;
|
|
4640
4814
|
|
|
4641
|
-
const headBar = (backControl || sizeControl || copyControl)
|
|
4815
|
+
const headBar = (backControl || sizeControl || copyControl || fontControl)
|
|
4642
4816
|
? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
|
|
4643
4817
|
react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl, copyControl),
|
|
4644
|
-
|
|
4818
|
+
react.createElement("div", { className: "fv-sheet-tabs-end" }, fontControl, sizeControl))
|
|
4645
4819
|
: null;
|
|
4646
4820
|
|
|
4647
4821
|
if (fromHistory && meta.kind !== "text" && meta.kind !== "markdown" && meta.kind !== "json") {
|
|
@@ -4658,7 +4832,7 @@ window.__ModuleLoader__.load({
|
|
|
4658
4832
|
return react.createElement("div", { className: "fv-content" }, headBar, react.createElement(DocView, { key, meta, root }));
|
|
4659
4833
|
}
|
|
4660
4834
|
return react.createElement(react.Fragment, null, banner,
|
|
4661
|
-
react.createElement(TextView, { key, meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast }));
|
|
4835
|
+
react.createElement(TextView, { key, meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast, fontSize, onFontSizeChange }));
|
|
4662
4836
|
}
|
|
4663
4837
|
|
|
4664
4838
|
|
|
@@ -4693,8 +4867,7 @@ window.__ModuleLoader__.load({
|
|
|
4693
4867
|
|
|
4694
4868
|
if (!menu) return null;
|
|
4695
4869
|
const { x, y, entry } = menu;
|
|
4696
|
-
const relPath = entry
|
|
4697
|
-
const absPath = resolveAbsPath(rootPath, relPath);
|
|
4870
|
+
const { relPath, absPath } = extractRelativeAndAbsolutePaths(entry, rootPath);
|
|
4698
4871
|
|
|
4699
4872
|
const handleCopyRel = (e) => {
|
|
4700
4873
|
e.stopPropagation();
|
|
@@ -5100,9 +5273,22 @@ window.__ModuleLoader__.load({
|
|
|
5100
5273
|
setShellWidth(clamped);
|
|
5101
5274
|
writeShellWidth(clamped);
|
|
5102
5275
|
};
|
|
5103
|
-
// Soft wrap
|
|
5104
|
-
//
|
|
5105
|
-
|
|
5276
|
+
// Soft wrap is owned entirely by the host ReadBlock banner, which
|
|
5277
|
+
// renders its own toggle beside the copy button and reports the state
|
|
5278
|
+
// through `labels.wrapLabel` / `labels.unwrapLabel`. The viewer used to
|
|
5279
|
+
// keep a second, parallel switch in its header; two controls for one
|
|
5280
|
+
// flag could disagree, so the header's is gone and this flag is no
|
|
5281
|
+
// longer state — there is nothing left to toggle from here.
|
|
5282
|
+
const wrap = false;
|
|
5283
|
+
// Reading size for the preview, remembered per browser. Kept here rather
|
|
5284
|
+
// than inside TextView because the stepper sits in the pane's toolbar
|
|
5285
|
+
// while the text it sizes lives one level down.
|
|
5286
|
+
const [fontSize, setFontSize] = react.useState(readFontSize);
|
|
5287
|
+
const changeFontSize = (next) => {
|
|
5288
|
+
const clamped = clampFontSize(next);
|
|
5289
|
+
setFontSize(clamped);
|
|
5290
|
+
writeFontSize(clamped);
|
|
5291
|
+
};
|
|
5106
5292
|
// The language override now lives on the tab it applies to (see
|
|
5107
5293
|
// `localeTagOfActive`), so switching tabs switches the pin with it.
|
|
5108
5294
|
const [fullscreen, setFullscreen] = react.useState(false);
|
|
@@ -5217,10 +5403,9 @@ window.__ModuleLoader__.load({
|
|
|
5217
5403
|
// tab, and discovery spawns a git process per candidate repository.
|
|
5218
5404
|
react.useEffect(() => {
|
|
5219
5405
|
if (!open || side !== "git" || root === "") return;
|
|
5220
|
-
git.ensure()
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
git.loadChanges();
|
|
5406
|
+
git.ensure().then(() => {
|
|
5407
|
+
git.loadChanges();
|
|
5408
|
+
}).catch(() => {});
|
|
5224
5409
|
}, [open, side, root]);
|
|
5225
5410
|
const [toast, setToast] = react.useState(null);
|
|
5226
5411
|
const toastTimer = react.useRef(null);
|
|
@@ -5740,30 +5925,12 @@ window.__ModuleLoader__.load({
|
|
|
5740
5925
|
}).catch(() => {});
|
|
5741
5926
|
};
|
|
5742
5927
|
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
* they care about unchanged.
|
|
5750
|
-
*/
|
|
5751
|
-
const refresh = () => {
|
|
5752
|
-
reloadWorkingTree();
|
|
5753
|
-
if (side === "git") {
|
|
5754
|
-
const state = git.store.get();
|
|
5755
|
-
// The commit to come back to belongs to the tab on screen, not to
|
|
5756
|
-
// whichever detail the controller happens to hold.
|
|
5757
|
-
const keepSha = activeTab !== null && activeTab.kind === "commit"
|
|
5758
|
-
? activeTab.id.slice("commit:".length)
|
|
5759
|
-
: "";
|
|
5760
|
-
if (state.root !== root) git.reset(root);
|
|
5761
|
-
git.loadRepos().then(() => {
|
|
5762
|
-
if (keepSha !== "") git.openCommit(keepSha);
|
|
5763
|
-
});
|
|
5764
|
-
}
|
|
5765
|
-
if (activeTabId !== "") refreshTab(activeTabId);
|
|
5766
|
-
};
|
|
5928
|
+
// Refreshing is offered per subject now: the tab's own context menu
|
|
5929
|
+
// re-reads the file/commit on screen, the Git panel has its own
|
|
5930
|
+
// "refresh working tree" control, and the sidebar re-lists a directory
|
|
5931
|
+
// when it is reopened. The old header button duplicated all three at
|
|
5932
|
+
// once, which made a single click re-read things the reader was not
|
|
5933
|
+
// looking at.
|
|
5767
5934
|
|
|
5768
5935
|
/** Open one commit in its own tab, or activate the tab already showing it. */
|
|
5769
5936
|
const openCommit = (commit) => {
|
|
@@ -6172,18 +6339,10 @@ window.__ModuleLoader__.load({
|
|
|
6172
6339
|
// file's path instead of navigable breadcrumbs.
|
|
6173
6340
|
react.createElement("div", { className: "fv-crumbs fv-file-path", title: showCommit ? "" : (meta === null ? "" : meta.path) },
|
|
6174
6341
|
showCommit ? "" : (meta === null ? "" : meta.path)),
|
|
6175
|
-
//
|
|
6176
|
-
//
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
type: "button",
|
|
6180
|
-
className: "fv-icon-button",
|
|
6181
|
-
"aria-pressed": wrap ? "true" : "false",
|
|
6182
|
-
"aria-label": wrap ? "取消自动换行" : "自动换行",
|
|
6183
|
-
title: wrap ? "取消自动换行" : "自动换行",
|
|
6184
|
-
onClick: () => setWrap((current) => !current),
|
|
6185
|
-
}, react.createElement(SvgIconWrap, { size: 15 }))
|
|
6186
|
-
: null,
|
|
6342
|
+
// Soft wrap is offered by the code banner itself (next to its copy
|
|
6343
|
+
// button), so the header no longer carries a second toggle for the
|
|
6344
|
+
// same setting — two switches for one flag only invited them to
|
|
6345
|
+
// drift apart. Refresh lives in the tab's context menu likewise.
|
|
6187
6346
|
!showCommit && meta !== null && meta.kind !== "directory"
|
|
6188
6347
|
? react.createElement("button", {
|
|
6189
6348
|
type: "button",
|
|
@@ -6193,9 +6352,6 @@ window.__ModuleLoader__.load({
|
|
|
6193
6352
|
onClick: () => handleDownloadItem(meta),
|
|
6194
6353
|
}, react.createElement(SvgIconDownload, { size: 15 }))
|
|
6195
6354
|
: null,
|
|
6196
|
-
react.createElement("button", {
|
|
6197
|
-
type: "button", className: "fv-icon-button", "aria-label": "刷新", title: "刷新", onClick: refresh,
|
|
6198
|
-
}, react.createElement(SvgIconRefresh, { size: 15 })),
|
|
6199
6355
|
react.createElement("button", {
|
|
6200
6356
|
type: "button",
|
|
6201
6357
|
className: "fv-icon-button",
|
|
@@ -6379,6 +6535,8 @@ window.__ModuleLoader__.load({
|
|
|
6379
6535
|
sizeControl,
|
|
6380
6536
|
langControl,
|
|
6381
6537
|
onToast: showToast,
|
|
6538
|
+
fontSize,
|
|
6539
|
+
onFontSizeChange: changeFontSize,
|
|
6382
6540
|
})
|
|
6383
6541
|
: react.createElement(FileView, {
|
|
6384
6542
|
meta,
|
|
@@ -6398,6 +6556,8 @@ window.__ModuleLoader__.load({
|
|
|
6398
6556
|
sizeControl,
|
|
6399
6557
|
langControl,
|
|
6400
6558
|
onToast: showToast,
|
|
6559
|
+
fontSize,
|
|
6560
|
+
onFontSizeChange: changeFontSize,
|
|
6401
6561
|
})))),
|
|
6402
6562
|
contextMenu !== null
|
|
6403
6563
|
? react.createElement(ContextMenu, {
|
|
@@ -6736,7 +6896,8 @@ window.__ModuleLoader__.load({
|
|
|
6736
6896
|
langOverrideKey, isKnownLanguageId, setLanguageCatalog, getLanguageCatalog: () => languageCatalog, LanguagePicker,
|
|
6737
6897
|
MARKDOWN_LABELS, MARKDOWN_CODE_LABELS, READ_BLOCK_LABELS, JSON_TREE_LABELS, DIFF_BLOCK_LABELS, ErrorBoundary,
|
|
6738
6898
|
openViewerForPath, wrapWorkspaceOpenPath, wrapConnectionRpc, wrapChangesOpenFetch, wrapSidebarRight, parseFileResource, setupGlobalFileClickInterceptor, extractClickedPath, normalizeClickedPath, looksLikeFilePath, isFileOpenControl, isFileMentionCode, renderRootOptions,
|
|
6739
|
-
triggerDownload, performDownload, isDesktopShell, SvgIconDownload, SvgIconCopy, PathCopyDropdown, resolveAbsPath, SvgIconBranch, SvgIconCommit,
|
|
6899
|
+
triggerDownload, performDownload, isDesktopShell, SvgIconDownload, SvgIconCopy, PathCopyDropdown, resolveAbsPath, extractRelativeAndAbsolutePaths, isAbsolutePath, SvgIconBranch, SvgIconCommit,
|
|
6900
|
+
FONT_SIZE_KEY, DEFAULT_FONT_SIZE, MIN_FONT_SIZE, MAX_FONT_SIZE, FONT_SIZE_STEP, readFontSize, writeFontSize, clampFontSize, FontSizeControl,
|
|
6740
6901
|
// Git panel: controller, views and the small formatters they share.
|
|
6741
6902
|
GIT_PAGE_SIZE, createGitController, GitSidebar, CommitDetail, WorkspaceChangeDetail, DiffView, kindOfPath,
|
|
6742
6903
|
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.17",
|
|
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",
|