@weniatall/dsh-file-viewer 0.3.7 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/client.js +306 -105
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -348,6 +348,10 @@ window.__ModuleLoader__.load({
348
348
  const DEFAULT_TREE_WIDTH = 280;
349
349
  const MIN_TREE_WIDTH = 160;
350
350
  const MAX_TREE_WIDTH = 700;
351
+ const GIT_CHANGES_HEIGHT_KEY = "dsh-file-viewer.git-changes-height";
352
+ const DEFAULT_GIT_CHANGES_HEIGHT = 180;
353
+ const MIN_GIT_CHANGES_HEIGHT = 64;
354
+ const MAX_GIT_CHANGES_HEIGHT = 800;
351
355
  /**
352
356
  * Smallest width the drawer may be dragged to.
353
357
  *
@@ -544,7 +548,11 @@ window.__ModuleLoader__.load({
544
548
  .fv-image-canvas.fv-image-dragging{cursor:grabbing}
545
549
  .fv-image-element{max-width:calc(100% - 32px);max-height:calc(100% - 32px);object-fit:contain;transition:transform .05s ease-out;pointer-events:none;box-shadow:var(--dsw-shadow-lv2,0 4px 12px rgba(0,0,0,.08))}
546
550
  .fv-frame{width:100%;height:100%;min-height:420px;border:none;background:var(--dsw-alias-bg-layer-1,#fff)}
547
- .fv-sheet-tabs{display:flex;flex-wrap:wrap;flex:none;align-items:center;gap:6px;padding:8px 12px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,#fff));border-bottom:1px solid var(--dsw-alias-border-l1);z-index:2}
551
+ .fv-sheet-tabs{display:flex;flex-wrap:wrap;flex:none;align-items:center;justify-content:space-between;gap:6px;padding:6px 12px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,#fff));border-bottom:1px solid var(--dsw-alias-border-l1);z-index:2;min-height:38px}
552
+ .fv-sheet-tabs-start{display:inline-flex;align-items:center;gap:6px;min-width:0;flex:1 1 auto}
553
+ .fv-sheet-tabs-end{display:inline-flex;align-items:center;gap:8px;margin-left:auto;flex:none}
554
+ .fv-sheet-tabs .fv-btn-back{width:26px;height:26px;font-size:16px;line-height:24px;border-radius:6px;margin-right:2px}
555
+ .fv-main-size{font-size:11px;color:var(--dsw-alias-label-caption);white-space:nowrap}
548
556
  .fv-content .fv-sheet-tabs{position:sticky;top:0;margin:-12px -12px 10px;padding:12px 12px 8px}
549
557
  .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}
550
558
  .fv-tab[aria-selected="true"]{border-color:transparent;background:var(--dsw-alias-brand-primary,#4d6bfe);color:var(--dsw-alias-label-primary-foreground,#fff)}
@@ -631,7 +639,19 @@ window.__ModuleLoader__.load({
631
639
  .fv-commit-tag{flex:none;padding:1px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-3,rgba(0,0,0,.05));color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-family);font-size:10px}
632
640
  /* Working-tree changes and a commit's patch share the commit-row visuals
633
641
  so the two panels read as one surface. */
634
- .fv-git-changes{display:flex;flex:none;flex-direction:column;gap:1px;max-height:38%;overflow-y:auto;overscroll-behavior:contain;border-bottom:1px solid var(--dsw-alias-border-l1);padding-bottom:4px}
642
+ .fv-git-changes{display:flex;flex:none;flex-direction:column;gap:1px;min-height:48px;overflow-y:auto;overscroll-behavior:contain;padding-bottom:2px}
643
+ .fv-git-panel[data-v-resizing="true"]{cursor:row-resize!important;user-select:none!important}
644
+ .fv-git-panel[data-v-resizing="true"] *{pointer-events:none!important}
645
+ .fv-git-panel[data-v-resizing="true"] .fv-git-v-resizer,
646
+ .fv-git-panel[data-v-resizing="true"] .fv-git-v-resizer *{pointer-events:auto!important}
647
+ .fv-git-v-resizer{position:relative;flex:none;display:flex;align-items:center;justify-content:center;height:14px;margin:2px 0;cursor:row-resize;user-select:none;touch-action:none;z-index:3}
648
+ .fv-git-v-resizer-line{position:absolute;left:0;right:0;top:50%;height:1px;background:var(--dsw-alias-border-l1);transform:translateY(-50%);transition:background-color .15s ease;pointer-events:none}
649
+ .fv-git-v-resizer-btn{position:relative;display:inline-flex;align-items:center;justify-content:center;width:44px;height:13px;padding:0;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));box-shadow:0 1px 3px rgba(0,0,0,.08);cursor:row-resize;pointer-events:auto;transition:background-color .15s ease,border-color .15s ease,box-shadow .15s ease;z-index:2}
650
+ .fv-git-v-resizer-grip{display:block;width:14px;height:2px;border-radius:1px;background:var(--dsw-alias-label-caption);box-shadow:0 -3px 0 0 var(--dsw-alias-label-caption);pointer-events:none}
651
+ .fv-git-v-resizer:hover .fv-git-v-resizer-line,.fv-git-v-resizer[data-dragging="true"] .fv-git-v-resizer-line{background:var(--dsw-alias-brand-primary,#4d6bfe)}
652
+ .fv-git-v-resizer:hover .fv-git-v-resizer-btn,.fv-git-v-resizer[data-dragging="true"] .fv-git-v-resizer-btn,.fv-git-v-resizer-btn:hover{border-color:var(--dsw-alias-brand-primary,#4d6bfe);background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 1px 6px color-mix(in srgb,var(--dsw-alias-brand-primary,#4d6bfe) 35%,transparent)}
653
+ .fv-git-v-resizer:hover .fv-git-v-resizer-grip,.fv-git-v-resizer[data-dragging="true"] .fv-git-v-resizer-grip,.fv-git-v-resizer-btn:hover .fv-git-v-resizer-grip{background:var(--dsw-alias-brand-primary,#4d6bfe);box-shadow:0 -3px 0 0 var(--dsw-alias-brand-primary,#4d6bfe)}
654
+ .fv-git-v-resizer:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:-1px}
635
655
  .fv-git-change-group{display:flex;flex-direction:column;gap:1px}
636
656
  .fv-git-refresh{width:22px;height:22px;margin-left:auto}
637
657
  .fv-commit-diff{display:flex;flex:1 1 auto;flex-direction:column;gap:2px;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:4px 6px}
@@ -837,7 +857,8 @@ window.__ModuleLoader__.load({
837
857
  .fv-git-more{height:38px!important;font-size:13px!important}
838
858
  .fv-commit-file{min-height:42px!important;font-size:13.5px!important}
839
859
  .fv-commit-path{font-size:12px!important}
840
- .fv-git-changes{max-height:none!important}
860
+ .fv-git-changes{height:auto!important;max-height:none!important}
861
+ .fv-git-v-resizer{display:none!important}
841
862
  .fv-commit-diff-file .fv-diff-table{font-size:12px!important}
842
863
  }
843
864
  `;
@@ -1144,6 +1165,27 @@ window.__ModuleLoader__.load({
1144
1165
  } catch { /* storage is unavailable */ }
1145
1166
  }
1146
1167
 
1168
+ function readGitChangesHeight() {
1169
+ try {
1170
+ const raw = globalThis.localStorage?.getItem(GIT_CHANGES_HEIGHT_KEY);
1171
+ const parsed = Number(raw);
1172
+ if (Number.isFinite(parsed) && parsed >= MIN_GIT_CHANGES_HEIGHT && parsed <= MAX_GIT_CHANGES_HEIGHT) {
1173
+ return Math.round(parsed);
1174
+ }
1175
+ } catch { /* storage is unavailable */ }
1176
+ return DEFAULT_GIT_CHANGES_HEIGHT;
1177
+ }
1178
+
1179
+ function writeGitChangesHeight(height) {
1180
+ try {
1181
+ if (typeof height === "number" && Number.isFinite(height)) {
1182
+ globalThis.localStorage?.setItem(GIT_CHANGES_HEIGHT_KEY, String(Math.round(height)));
1183
+ } else {
1184
+ globalThis.localStorage?.removeItem(GIT_CHANGES_HEIGHT_KEY);
1185
+ }
1186
+ } catch { /* storage is unavailable */ }
1187
+ }
1188
+
1147
1189
  /**
1148
1190
  * Read the per-file language overrides.
1149
1191
  *
@@ -2306,10 +2348,72 @@ window.__ModuleLoader__.load({
2306
2348
  const canCheckout = repo !== null && viewingBranch !== "" && viewingBranch !== current
2307
2349
  && checkoutable && state.busy !== true;
2308
2350
 
2351
+ const [changesHeight, setChangesHeight] = react.useState(readGitChangesHeight);
2352
+ const [isResizingChanges, setIsResizingChanges] = react.useState(false);
2353
+ const changesResizeRef = react.useRef(null);
2354
+ const changesHeightRef = react.useRef(changesHeight);
2355
+ changesHeightRef.current = changesHeight;
2356
+
2357
+ const onChangesResizerPointerDown = (event) => {
2358
+ if (event.button !== 0 && event.button !== undefined) return;
2359
+ event.preventDefault();
2360
+ event.stopPropagation();
2361
+ const startY = event.clientY;
2362
+ const startH = changesHeightRef.current;
2363
+ changesResizeRef.current = { startY, startH };
2364
+ setIsResizingChanges(true);
2365
+ try { event.currentTarget.setPointerCapture(event.pointerId); } catch (_) {}
2366
+ };
2367
+
2368
+ const onChangesResizerPointerMove = (event) => {
2369
+ if (!changesResizeRef.current) return;
2370
+ const { startY, startH } = changesResizeRef.current;
2371
+ const deltaY = event.clientY - startY;
2372
+ const panelEl = event.currentTarget.closest(".fv-git-panel");
2373
+ const panelHeight = panelEl ? panelEl.clientHeight : 600;
2374
+ const maxBound = Math.max(MIN_GIT_CHANGES_HEIGHT, panelHeight - 140);
2375
+ const nextH = Math.round(Math.min(Math.min(MAX_GIT_CHANGES_HEIGHT, maxBound), Math.max(MIN_GIT_CHANGES_HEIGHT, startH + deltaY)));
2376
+ setChangesHeight(nextH);
2377
+ };
2378
+
2379
+ const onChangesResizerPointerUp = (event) => {
2380
+ if (!changesResizeRef.current) return;
2381
+ changesResizeRef.current = null;
2382
+ setIsResizingChanges(false);
2383
+ try { event.currentTarget.releasePointerCapture(event.pointerId); } catch (_) {}
2384
+ writeGitChangesHeight(changesHeightRef.current);
2385
+ };
2386
+
2387
+ const onChangesResizerDoubleClick = (event) => {
2388
+ event.preventDefault();
2389
+ event.stopPropagation();
2390
+ setChangesHeight(DEFAULT_GIT_CHANGES_HEIGHT);
2391
+ writeGitChangesHeight(DEFAULT_GIT_CHANGES_HEIGHT);
2392
+ };
2393
+
2394
+ const onChangesResizerKeyDown = (event) => {
2395
+ if (event.key === "ArrowUp") {
2396
+ event.preventDefault();
2397
+ const nextH = Math.max(MIN_GIT_CHANGES_HEIGHT, changesHeightRef.current - 16);
2398
+ setChangesHeight(nextH);
2399
+ writeGitChangesHeight(nextH);
2400
+ } else if (event.key === "ArrowDown") {
2401
+ event.preventDefault();
2402
+ const nextH = Math.min(MAX_GIT_CHANGES_HEIGHT, changesHeightRef.current + 16);
2403
+ setChangesHeight(nextH);
2404
+ writeGitChangesHeight(nextH);
2405
+ } else if (event.key === "Home" || event.key === "Enter") {
2406
+ event.preventDefault();
2407
+ setChangesHeight(DEFAULT_GIT_CHANGES_HEIGHT);
2408
+ writeGitChangesHeight(DEFAULT_GIT_CHANGES_HEIGHT);
2409
+ }
2410
+ };
2411
+
2309
2412
  const wrap = (...children) => react.createElement("div", {
2310
2413
  className: "fv-tree fv-git-panel",
2311
2414
  role: "group",
2312
2415
  "aria-label": "Git 面板",
2416
+ "data-v-resizing": isResizingChanges ? "true" : "false",
2313
2417
  style: typeof width === "number" ? { width: width + "px" } : undefined,
2314
2418
  }, header, ...children);
2315
2419
 
@@ -2447,7 +2551,14 @@ window.__ModuleLoader__.load({
2447
2551
  : null);
2448
2552
  }
2449
2553
 
2450
- const changesSection = react.createElement("div", { className: "fv-git-changes" },
2554
+ const changesSection = react.createElement("div", {
2555
+ className: "fv-git-changes",
2556
+ style: {
2557
+ height: changesHeight + "px",
2558
+ maxHeight: "none",
2559
+ flex: "none",
2560
+ },
2561
+ },
2451
2562
  react.createElement("div", { className: "fv-git-section" },
2452
2563
  react.createElement("span", null, "工作区改动"),
2453
2564
  react.createElement("span", { className: "fv-git-count" },
@@ -2461,6 +2572,37 @@ window.__ModuleLoader__.load({
2461
2572
  }, react.createElement(SvgIconRefresh, { size: 13 }))),
2462
2573
  changesBody);
2463
2574
 
2575
+ const vResizer = react.createElement("div", {
2576
+ className: "fv-git-v-resizer",
2577
+ role: "separator",
2578
+ tabIndex: 0,
2579
+ "aria-orientation": "horizontal",
2580
+ "aria-label": "调整工作区改动与提交记录高度",
2581
+ "aria-valuenow": changesHeight,
2582
+ "aria-valuemin": MIN_GIT_CHANGES_HEIGHT,
2583
+ "aria-valuemax": MAX_GIT_CHANGES_HEIGHT,
2584
+ title: "拖动调整区域高度,双击恢复默认",
2585
+ "data-dragging": isResizingChanges ? "true" : "false",
2586
+ onPointerDown: onChangesResizerPointerDown,
2587
+ onPointerMove: onChangesResizerPointerMove,
2588
+ onPointerUp: onChangesResizerPointerUp,
2589
+ onPointerCancel: onChangesResizerPointerUp,
2590
+ onDoubleClick: onChangesResizerDoubleClick,
2591
+ onKeyDown: onChangesResizerKeyDown,
2592
+ },
2593
+ react.createElement("div", { className: "fv-git-v-resizer-line" }),
2594
+ react.createElement("button", {
2595
+ type: "button",
2596
+ tabIndex: -1,
2597
+ className: "fv-git-v-resizer-btn",
2598
+ "aria-label": "拖动手柄",
2599
+ title: "按住拖动调整高度,双击恢复默认",
2600
+ "data-dragging": isResizingChanges ? "true" : "false",
2601
+ },
2602
+ react.createElement("span", { className: "fv-git-v-resizer-grip" })
2603
+ )
2604
+ );
2605
+
2464
2606
  const commitRows = commits.map((commit) => react.createElement("button", {
2465
2607
  key: commit.sha,
2466
2608
  type: "button",
@@ -2507,6 +2649,7 @@ window.__ModuleLoader__.load({
2507
2649
 
2508
2650
  return wrap(toolbar,
2509
2651
  changesSection,
2652
+ vResizer,
2510
2653
  react.createElement("div", { className: "fv-git-section" },
2511
2654
  react.createElement("span", null, "提交记录"),
2512
2655
  react.createElement("span", { className: "fv-git-count" }, state.total > 0 ? String(state.total) : "")),
@@ -2608,7 +2751,7 @@ window.__ModuleLoader__.load({
2608
2751
  *
2609
2752
  * @param {{state: object, path: string}} props - panel snapshot and the file to diff.
2610
2753
  */
2611
- function WorkspaceChangeDetail({ state, path: filePath, request, repo }) {
2754
+ function WorkspaceChangeDetail({ state, path: filePath, request, repo, backControl }) {
2612
2755
  const [diff, setDiff] = react.useState({ status: "idle", data: null, error: null });
2613
2756
  // A change can be staged, unstaged, or both. The reader picks which side
2614
2757
  // to look at instead of the pane guessing.
@@ -2660,6 +2803,7 @@ window.__ModuleLoader__.load({
2660
2803
 
2661
2804
  return react.createElement("div", { className: "fv-commit" },
2662
2805
  react.createElement("div", { className: "fv-commit-head" },
2806
+ backControl ? react.createElement("div", { style: { marginBottom: "6px" } }, backControl) : null,
2663
2807
  react.createElement("span", { className: "fv-commit-subject" }, filePath || "工作区改动"),
2664
2808
  react.createElement("span", { className: "fv-commit-meta" },
2665
2809
  row !== null
@@ -2687,7 +2831,7 @@ window.__ModuleLoader__.load({
2687
2831
  * second viewer; a row that lands outside the current root stays visible
2688
2832
  * but disabled rather than silently disappearing.
2689
2833
  */
2690
- function CommitDetail({ state, selectedPath, onSelectFile, view, onSwitchView, onLoadDiff }) {
2834
+ function CommitDetail({ state, selectedPath, onSelectFile, view, onSwitchView, onLoadDiff, backControl }) {
2691
2835
  if (state.detailStatus === "loading" || state.detailStatus === "idle") {
2692
2836
  return react.createElement("div", { className: "fv-note" }, "正在读取提交详情…");
2693
2837
  }
@@ -2733,27 +2877,29 @@ window.__ModuleLoader__.load({
2733
2877
  // Two views of one commit: which files changed, and what changed inside
2734
2878
  // them. Both come from the same revision pair, so they cannot disagree.
2735
2879
  const tabs = react.createElement("div", { className: "fv-sheet-tabs", role: "tablist" },
2736
- react.createElement("button", {
2737
- type: "button", className: "fv-tab", "aria-selected": view !== "diff" ? "true" : "false",
2738
- onClick: () => onSwitchView("files"),
2739
- }, "变更文件", react.createElement("span", { className: "fv-git-count" }, String(files.length))),
2740
- react.createElement("button", {
2741
- type: "button", className: "fv-tab fv-tab-diff", "aria-selected": view === "diff" ? "true" : "false",
2742
- onClick: () => {
2743
- onSwitchView("diff");
2744
- if (onLoadDiff !== undefined) onLoadDiff();
2745
- },
2746
- }, "改动内容",
2747
- state.commitDiff !== null && state.commitDiff !== undefined && state.commitDiff.summary !== undefined
2748
- ? react.createElement("span", { className: "fv-diff-stat" },
2749
- state.commitDiff.summary.added > 0
2750
- ? react.createElement("span", { className: "fv-diff-add" }, "+" + state.commitDiff.summary.added)
2751
- : null,
2752
- state.commitDiff.summary.added > 0 && state.commitDiff.summary.removed > 0 ? " " : "",
2753
- state.commitDiff.summary.removed > 0
2754
- ? react.createElement("span", { className: "fv-diff-del" }, "-" + state.commitDiff.summary.removed)
2755
- : null)
2756
- : null));
2880
+ react.createElement("div", { className: "fv-sheet-tabs-start" },
2881
+ backControl,
2882
+ react.createElement("button", {
2883
+ type: "button", className: "fv-tab", "aria-selected": view !== "diff" ? "true" : "false",
2884
+ onClick: () => onSwitchView("files"),
2885
+ }, "变更文件", react.createElement("span", { className: "fv-git-count" }, String(files.length))),
2886
+ react.createElement("button", {
2887
+ type: "button", className: "fv-tab fv-tab-diff", "aria-selected": view === "diff" ? "true" : "false",
2888
+ onClick: () => {
2889
+ onSwitchView("diff");
2890
+ if (onLoadDiff !== undefined) onLoadDiff();
2891
+ },
2892
+ }, "改动内容",
2893
+ state.commitDiff !== null && state.commitDiff !== undefined && state.commitDiff.summary !== undefined
2894
+ ? react.createElement("span", { className: "fv-diff-stat" },
2895
+ state.commitDiff.summary.added > 0
2896
+ ? react.createElement("span", { className: "fv-diff-add" }, "+" + state.commitDiff.summary.added)
2897
+ : null,
2898
+ state.commitDiff.summary.added > 0 && state.commitDiff.summary.removed > 0 ? " " : "",
2899
+ state.commitDiff.summary.removed > 0
2900
+ ? react.createElement("span", { className: "fv-diff-del" }, "-" + state.commitDiff.summary.removed)
2901
+ : null)
2902
+ : null)));
2757
2903
 
2758
2904
  let patchBody;
2759
2905
  if (state.commitDiffStatus === "loading" || state.commitDiffStatus === "idle") {
@@ -3343,7 +3489,20 @@ window.__ModuleLoader__.load({
3343
3489
  return react.createElement(react.Fragment, null,
3344
3490
  react.createElement("div", {
3345
3491
  className: "fv-context-backdrop",
3492
+ onPointerDown: (event) => {
3493
+ event.preventDefault();
3494
+ event.stopPropagation();
3495
+ if (Date.now() - mountTimeRef.current < 400) return;
3496
+ onClose();
3497
+ },
3498
+ onMouseDown: (event) => {
3499
+ event.preventDefault();
3500
+ event.stopPropagation();
3501
+ if (Date.now() - mountTimeRef.current < 400) return;
3502
+ onClose();
3503
+ },
3346
3504
  onClick: (event) => {
3505
+ event.preventDefault();
3347
3506
  event.stopPropagation();
3348
3507
  if (Date.now() - mountTimeRef.current < 400) return;
3349
3508
  onClose();
@@ -3938,7 +4097,7 @@ window.__ModuleLoader__.load({
3938
4097
 
3939
4098
  /** Line-numbered diff viewer with dual line number gutters. */
3940
4099
  /** Text, Markdown and JSON: one windowed read, three renderings. */
3941
- function TextView({ meta, root, wrap, lang, commit, repo }) {
4100
+ function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl }) {
3942
4101
  // Callers mount this with a per-file key, so paging state starts fresh
3943
4102
  // for every file instead of leaking the previous file's window into the
3944
4103
  // first request.
@@ -4218,11 +4377,22 @@ window.__ModuleLoader__.load({
4218
4377
  }, "源码")
4219
4378
  : null;
4220
4379
 
4221
- const tabs = showTabs
4380
+ const tabButtons = showTabs
4381
+ ? [...(fromCommit && diffTab !== null
4382
+ ? [diffTab, contentTab, sourceTab]
4383
+ : [contentTab, sourceTab, diffTab])].filter((tab) => tab !== null)
4384
+ : [];
4385
+
4386
+ const tabs = showTabs || Boolean(backControl || langControl || sizeControl)
4222
4387
  ? react.createElement("div", { className: "fv-sheet-tabs", role: "tablist" },
4223
- ...(fromCommit && diffTab !== null
4224
- ? [diffTab, contentTab, sourceTab]
4225
- : [contentTab, sourceTab, diffTab]).filter((tab) => tab !== null))
4388
+ react.createElement("div", { className: "fv-sheet-tabs-start" },
4389
+ backControl,
4390
+ ...tabButtons),
4391
+ (sizeControl || langControl)
4392
+ ? react.createElement("div", { className: "fv-sheet-tabs-end" },
4393
+ sizeControl,
4394
+ langControl)
4395
+ : null)
4226
4396
  : null;
4227
4397
 
4228
4398
 
@@ -4289,32 +4459,38 @@ window.__ModuleLoader__.load({
4289
4459
  }
4290
4460
 
4291
4461
  /** Pick the viewer for a file and keep unsupported formats useful. */
4292
- function FileView({ meta, root, api, wrap, lang, onDownload, commit, repo, loading }) {
4462
+ function FileView({ meta, root, api, wrap, lang, onDownload, commit, repo, loading, backControl, sizeControl, langControl }) {
4293
4463
  if (meta === null) {
4294
4464
  // A tab exists but its metadata is still in flight. Saying "pick a
4295
4465
  // file" there would be a lie, and the reader would click again.
4296
- return react.createElement("div", { className: "fv-note" },
4297
- loading === true ? "正在读取文件…" : "从左侧选择一个文件。");
4466
+ return react.createElement("div", { className: "fv-empty-pane" },
4467
+ backControl ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" }, backControl) : null,
4468
+ react.createElement("div", { className: "fv-note" },
4469
+ loading === true ? "正在读取文件…" : "从左侧选择一个文件。"));
4298
4470
  }
4299
4471
  if (meta.tooLarge || meta.kind === "binary") {
4300
- return react.createElement("div", { className: "fv-note" },
4301
- react.createElement("strong", null, meta.name),
4302
- react.createElement("span", null, meta.tooLarge
4303
- ? `文件大小 ${formatBytes(meta.size)},超过查看器上限 ${formatBytes(meta.limit)}。`
4304
- : "这是二进制文件,查看器无法显示其内容。"),
4305
- react.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px", marginTop: "4px" } },
4306
- api && api.host
4307
- ? react.createElement("button", {
4472
+ return react.createElement("div", { className: "fv-binary-pane" },
4473
+ backControl ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
4474
+ react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl),
4475
+ sizeControl ? react.createElement("div", { className: "fv-sheet-tabs-end" }, sizeControl) : null) : null,
4476
+ react.createElement("div", { className: "fv-note" },
4477
+ react.createElement("strong", null, meta.name),
4478
+ react.createElement("span", null, meta.tooLarge
4479
+ ? `文件大小 ${formatBytes(meta.size)},超过查看器上限 ${formatBytes(meta.limit)}。`
4480
+ : "这是二进制文件,查看器无法显示其内容。"),
4481
+ react.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px", marginTop: "4px" } },
4482
+ api && api.host
4483
+ ? react.createElement("button", {
4484
+ type: "button",
4485
+ className: "fv-button",
4486
+ onClick: () => api.host.openPath({ path: meta.displayPath }),
4487
+ }, "用本地程序打开")
4488
+ : null,
4489
+ react.createElement("button", {
4308
4490
  type: "button",
4309
- className: "fv-button",
4310
- onClick: () => api.host.openPath({ path: meta.displayPath }),
4311
- }, "用本地程序打开")
4312
- : null,
4313
- react.createElement("button", {
4314
- type: "button",
4315
- className: "fv-button fv-btn-download-note",
4316
- onClick: () => onDownload && onDownload(meta),
4317
- }, "下载到本地")));
4491
+ className: "fv-button fv-btn-download-note",
4492
+ onClick: () => onDownload && onDownload(meta),
4493
+ }, "下载到本地"))));
4318
4494
  }
4319
4495
  // The key remounts each viewer per file so no per-file state (paging,
4320
4496
  // active sheet, blob URL) survives a switch.
@@ -4329,21 +4505,28 @@ window.__ModuleLoader__.load({
4329
4505
  "以下内容来自提交 " + commit.slice(0, 7) + (meta.change ? ",该文件本次为「" + meta.change + "」" : "")),
4330
4506
  react.createElement("span", { className: "fv-history-hint" }, "这是历史版本,不是工作区当前内容。"))
4331
4507
  : null;
4508
+
4509
+ const headBar = (backControl || sizeControl)
4510
+ ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
4511
+ react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl),
4512
+ sizeControl ? react.createElement("div", { className: "fv-sheet-tabs-end" }, sizeControl) : null)
4513
+ : null;
4514
+
4332
4515
  if (fromHistory && meta.kind !== "text" && meta.kind !== "markdown" && meta.kind !== "json") {
4333
- return react.createElement(react.Fragment, null, banner,
4516
+ return react.createElement(react.Fragment, null, headBar, banner,
4334
4517
  react.createElement("div", { className: "fv-note" },
4335
4518
  "该提交中的文件是 " + meta.kind + " 格式,历史版本暂不支持预览。"));
4336
4519
  }
4337
- if (meta.kind === "image") return react.createElement(ImageView, { key, meta, root });
4338
- if (meta.kind === "pdf") return react.createElement(BinaryView, { key, meta, root });
4520
+ if (meta.kind === "image") return react.createElement(react.Fragment, null, headBar, react.createElement(ImageView, { key, meta, root }));
4521
+ if (meta.kind === "pdf") return react.createElement(react.Fragment, null, headBar, react.createElement(BinaryView, { key, meta, root }));
4339
4522
  if (meta.kind === "sheet") {
4340
- return react.createElement("div", { className: "fv-content" }, react.createElement(SheetView, { key, meta, root }));
4523
+ return react.createElement("div", { className: "fv-content" }, headBar, react.createElement(SheetView, { key, meta, root }));
4341
4524
  }
4342
4525
  if (meta.kind === "doc") {
4343
- return react.createElement("div", { className: "fv-content" }, react.createElement(DocView, { key, meta, root }));
4526
+ return react.createElement("div", { className: "fv-content" }, headBar, react.createElement(DocView, { key, meta, root }));
4344
4527
  }
4345
4528
  return react.createElement(react.Fragment, null, banner,
4346
- react.createElement(TextView, { key, meta, root, wrap, lang, commit, repo }));
4529
+ react.createElement(TextView, { key, meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl }));
4347
4530
  }
4348
4531
 
4349
4532
 
@@ -4421,7 +4604,20 @@ window.__ModuleLoader__.load({
4421
4604
  return react.createElement(react.Fragment, null,
4422
4605
  react.createElement("div", {
4423
4606
  className: "fv-context-backdrop",
4607
+ onPointerDown: (e) => {
4608
+ e.preventDefault();
4609
+ e.stopPropagation();
4610
+ if (Date.now() - mountTimeRef.current < 400) return;
4611
+ onClose();
4612
+ },
4613
+ onMouseDown: (e) => {
4614
+ e.preventDefault();
4615
+ e.stopPropagation();
4616
+ if (Date.now() - mountTimeRef.current < 400) return;
4617
+ onClose();
4618
+ },
4424
4619
  onClick: (e) => {
4620
+ e.preventDefault();
4425
4621
  e.stopPropagation();
4426
4622
  if (Date.now() - mountTimeRef.current < 400) return;
4427
4623
  onClose();
@@ -4836,6 +5032,15 @@ window.__ModuleLoader__.load({
4836
5032
  // it. Kept apart from the tree's `contextMenu`: the two menus describe
4837
5033
  // different subjects and must never be confused for one another.
4838
5034
  const [tabMenu, setTabMenu] = react.useState(null);
5035
+ const lastMenuClosedAtRef = react.useRef(0);
5036
+ const closeContextMenu = react.useCallback(() => {
5037
+ lastMenuClosedAtRef.current = Date.now();
5038
+ setContextMenu(null);
5039
+ }, []);
5040
+ const closeTabMenu = react.useCallback(() => {
5041
+ lastMenuClosedAtRef.current = Date.now();
5042
+ setTabMenu(null);
5043
+ }, []);
4839
5044
  const tabsRef = react.useRef(null);
4840
5045
  tabsRef.current = tabs;
4841
5046
  const activeTabIdRef = react.useRef("");
@@ -5747,6 +5952,36 @@ window.__ModuleLoader__.load({
5747
5952
  performDownload(request, root, targetPath, targetName, showToast);
5748
5953
  };
5749
5954
 
5955
+ const originKind = previewOrigin === null || previewOrigin === undefined ? null : previewOrigin.kind;
5956
+ const backTarget = originKind === "commit"
5957
+ ? "返回变更文件列表"
5958
+ : (originKind === "changes" ? "返回工作区改动列表" : "返回文件列表");
5959
+ const canGoBack = isMobile || pane === "content"
5960
+ ? (previewOrigin !== null || meta !== null || showCommit)
5961
+ : false;
5962
+ const backControl = react.createElement("button", {
5963
+ type: "button",
5964
+ className: "fv-icon-button fv-btn-back",
5965
+ "aria-label": backTarget,
5966
+ title: backTarget,
5967
+ disabled: !canGoBack,
5968
+ onClick: goBack,
5969
+ }, react.createElement("span", { "aria-hidden": "true" }, "‹"));
5970
+
5971
+ const sizeControl = meta !== null && !showCommit && typeof meta.size === "number"
5972
+ ? react.createElement("span", { className: "fv-main-size" }, formatBytes(meta.size))
5973
+ : null;
5974
+
5975
+ const langControl = meta !== null && !showCommit && !meta.tooLarge && (meta.kind === "text" || meta.kind === "markdown" || meta.kind === "json")
5976
+ ? react.createElement(LanguagePicker, {
5977
+ choice: localeTagOfActive,
5978
+ catalog: languageCatalog,
5979
+ source: meta.langSource,
5980
+ resolved: meta.lang,
5981
+ onChange: chooseLang,
5982
+ })
5983
+ : null;
5984
+
5750
5985
  return react.createElement("div", {
5751
5986
  className: "fv-scrim",
5752
5987
  role: "dialog",
@@ -5755,6 +5990,8 @@ window.__ModuleLoader__.load({
5755
5990
  onMouseDown: (event) => {
5756
5991
  if (event.target !== event.currentTarget) return;
5757
5992
  if (Date.now() - openedAtRef.current < 400) return;
5993
+ if (contextMenu !== null || tabMenu !== null) return;
5994
+ if (Date.now() - lastMenuClosedAtRef.current < 400) return;
5758
5995
  openStore.set(null);
5759
5996
  },
5760
5997
  },
@@ -5934,51 +6171,6 @@ window.__ModuleLoader__.load({
5934
6171
  onContextMenu: (menu) => setTabMenu(menu),
5935
6172
  menuFor: tabMenu === null || tabMenu === undefined ? "" : tabMenu.tabId,
5936
6173
  }),
5937
- react.createElement("div", { className: "fv-main-head" },
5938
- (() => {
5939
- // The control returns one step, so it names that step and
5940
- // is disabled when there is nowhere to go — a button that
5941
- // silently does nothing reads as broken.
5942
- const originKind = previewOrigin === null || previewOrigin === undefined ? null : previewOrigin.kind;
5943
- const backTarget = originKind === "commit"
5944
- ? "返回变更文件列表"
5945
- : (originKind === "changes" ? "返回工作区改动列表" : "返回文件列表");
5946
- // On a phone the panes are exclusive, so leaving the content
5947
- // pane is always meaningful; on desktop the sidebar is
5948
- // already visible and only an origin is worth returning to.
5949
- const canGoBack = isMobile || pane === "content"
5950
- ? (previewOrigin !== null || meta !== null || showCommit)
5951
- : false;
5952
- return react.createElement("button", {
5953
- type: "button",
5954
- className: "fv-icon-button fv-btn-back",
5955
- "aria-label": backTarget,
5956
- title: backTarget,
5957
- disabled: !canGoBack,
5958
- onClick: goBack,
5959
- }, react.createElement("span", { "aria-hidden": "true" }, "‹"));
5960
- })(),
5961
- react.createElement("span", { className: "fv-main-name" },
5962
- commitHeadline !== ""
5963
- ? commitHeadline
5964
- : (activeTab === null
5965
- ? "未选择文件"
5966
- : (meta === null ? activeTab.title : meta.name))),
5967
- meta !== null && !showCommit && typeof meta.size === "number"
5968
- ? react.createElement("span", null, formatBytes(meta.size))
5969
- : null,
5970
- // The language picker sits with the file it applies to, and
5971
- // is offered only for source views: an image, sheet or
5972
- // document has no grammar to choose.
5973
- meta !== null && !showCommit && !meta.tooLarge && (meta.kind === "text" || meta.kind === "markdown" || meta.kind === "json")
5974
- ? react.createElement(LanguagePicker, {
5975
- choice: localeTagOfActive,
5976
- catalog: languageCatalog,
5977
- source: meta.langSource,
5978
- resolved: meta.lang,
5979
- onChange: chooseLang,
5980
- })
5981
- : null),
5982
6174
  error !== null && meta === null && !showCommit && activeTab === null
5983
6175
  ? react.createElement("div", { className: "fv-note fv-error" },
5984
6176
  react.createElement("span", null, messageOf(error)),
@@ -6021,6 +6213,7 @@ window.__ModuleLoader__.load({
6021
6213
  path: previewChangePath,
6022
6214
  request,
6023
6215
  repo: gitState.repoId,
6216
+ backControl,
6024
6217
  })
6025
6218
  : react.createElement(CommitDetail, {
6026
6219
  state: gitState,
@@ -6036,6 +6229,7 @@ window.__ModuleLoader__.load({
6036
6229
  const sha = commitSha;
6037
6230
  if (sha !== "") git.loadCommitDiff(sha);
6038
6231
  },
6232
+ backControl,
6039
6233
  }))
6040
6234
  : react.createElement(ErrorBoundary, {
6041
6235
  // One viewer per tab (and per refresh epoch), so no
@@ -6053,6 +6247,9 @@ window.__ModuleLoader__.load({
6053
6247
  onDownload: handleDownloadItem,
6054
6248
  commit: "",
6055
6249
  repo: gitState.repoId,
6250
+ backControl,
6251
+ sizeControl,
6252
+ langControl,
6056
6253
  })
6057
6254
  : react.createElement(FileView, {
6058
6255
  meta,
@@ -6068,12 +6265,15 @@ window.__ModuleLoader__.load({
6068
6265
  // A tab is created before its metadata arrives, so the
6069
6266
  // pane has to say "reading" instead of "pick a file".
6070
6267
  loading: activeTab.status === "loading",
6268
+ backControl,
6269
+ sizeControl,
6270
+ langControl,
6071
6271
  })))),
6072
6272
  contextMenu !== null
6073
6273
  ? react.createElement(ContextMenu, {
6074
6274
  menu: contextMenu,
6075
6275
  rootPath: activeRoot ? (activeRoot.path || activeRoot.id) : root,
6076
- onClose: () => setContextMenu(null),
6276
+ onClose: closeContextMenu,
6077
6277
  onMention: mention,
6078
6278
  onToast: showToast,
6079
6279
  onDownload: handleDownloadItem,
@@ -6086,7 +6286,7 @@ window.__ModuleLoader__.load({
6086
6286
  ? react.createElement(TabContextMenu, {
6087
6287
  menu: tabMenu,
6088
6288
  tabs,
6089
- onClose: () => setTabMenu(null),
6289
+ onClose: closeTabMenu,
6090
6290
  onAction: onTabAction,
6091
6291
  })
6092
6292
  : null,
@@ -6400,6 +6600,7 @@ window.__ModuleLoader__.load({
6400
6600
  mediaTypeOf, messageOf, createStore, createRequest, blobOf, ensureStyles,
6401
6601
  ENTRY_POSITION_KEY, DRAG_SLOP, settleEntry, readEntryPosition, writeEntryPosition,
6402
6602
  TREE_WIDTH_KEY, DEFAULT_TREE_WIDTH, MIN_TREE_WIDTH, MAX_TREE_WIDTH, readTreeWidth, writeTreeWidth,
6603
+ GIT_CHANGES_HEIGHT_KEY, DEFAULT_GIT_CHANGES_HEIGHT, MIN_GIT_CHANGES_HEIGHT, MAX_GIT_CHANGES_HEIGHT, readGitChangesHeight, writeGitChangesHeight,
6403
6604
  SHELL_WIDTH_KEY, MIN_SHELL_WIDTH, readShellWidth, writeShellWidth, clampShellWidth,
6404
6605
  LANG_OVERRIDES_KEY, LANG_OVERRIDES_MAX, readLangOverrides, writeLangOverrides, readLangOverride, writeLangOverride,
6405
6606
  langOverrideKey, isKnownLanguageId, setLanguageCatalog, getLanguageCatalog: () => languageCatalog, LanguagePicker,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weniatall/dsh-file-viewer",
3
- "version": "0.3.7",
3
+ "version": "0.3.12",
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",