@weniatall/dsh-file-viewer 0.3.8 → 0.3.13

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 +328 -21
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -163,6 +163,21 @@ window.__ModuleLoader__.load({
163
163
  );
164
164
  }
165
165
 
166
+ function SvgIconCopy({ size = 13 }) {
167
+ return react.createElement("svg", {
168
+ width: size, height: size, viewBox: "0 0 16 16", fill: "none",
169
+ stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round", strokeLinejoin: "round",
170
+ style: { flex: "none" },
171
+ },
172
+ react.createElement("rect", {
173
+ x: "5", y: "5", width: "8.5", height: "8.5", rx: "1.5",
174
+ }),
175
+ react.createElement("path", {
176
+ d: "M3.5 10.5H2.75C2.05964 10.5 1.5 9.94036 1.5 9.25V2.75C1.5 2.05964 2.05964 1.5 2.75 1.5H9.25C9.94036 1.5 10.5 2.05964 10.5 2.75V3.5",
177
+ })
178
+ );
179
+ }
180
+
166
181
  const Component = react.Component || class Component { constructor(props) { this.props = props; this.state = {}; } setState(s) { Object.assign(this.state, typeof s === "function" ? s(this.state) : s); } };
167
182
  class ErrorBoundary extends Component {
168
183
  constructor(props) {
@@ -348,6 +363,10 @@ window.__ModuleLoader__.load({
348
363
  const DEFAULT_TREE_WIDTH = 280;
349
364
  const MIN_TREE_WIDTH = 160;
350
365
  const MAX_TREE_WIDTH = 700;
366
+ const GIT_CHANGES_HEIGHT_KEY = "dsh-file-viewer.git-changes-height";
367
+ const DEFAULT_GIT_CHANGES_HEIGHT = 180;
368
+ const MIN_GIT_CHANGES_HEIGHT = 64;
369
+ const MAX_GIT_CHANGES_HEIGHT = 800;
351
370
  /**
352
371
  * Smallest width the drawer may be dragged to.
353
372
  *
@@ -387,7 +406,7 @@ window.__ModuleLoader__.load({
387
406
  body>div[role="presentation"]:has(.fv-scrim)>[aria-hidden="true"]{display:none!important}
388
407
  .fv-scrim[role="dialog"]{position:absolute!important;inset:0!important;width:auto!important;max-width:none!important;max-height:none!important;min-height:0!important;margin:0!important;border:none!important;border-radius:0!important;box-shadow:none!important;background:transparent!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important;overflow:visible!important}
389
408
  .fv-scrim{position:absolute;inset:0;z-index:40;display:flex;align-items:stretch;justify-content:flex-end;background:transparent;pointer-events:auto;animation:fv-fade .16s ease-out}
390
- .fv-shell{display:flex;flex-direction:column;width:min(64vw,1100px);min-width:0;min-height:0;overflow:hidden;border-left:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family);color:var(--dsw-alias-label-primary);-webkit-backdrop-filter:blur(24px);backdrop-filter:blur(24px);animation:fv-slide .2s cubic-bezier(.2,.8,.2,1);transition:width .2s cubic-bezier(.2,.8,.2,1)}
409
+ .fv-shell{display:flex;flex-direction:column;width:min(64vw,1100px);min-width:0;min-height:0;overflow:hidden;border-left:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family);color:var(--dsw-alias-label-primary);animation:fv-slide .2s cubic-bezier(.2,.8,.2,1);transition:width .2s cubic-bezier(.2,.8,.2,1)}
391
410
  .fv-shell[data-fullscreen="true"]{width:100vw;border-left:none}
392
411
  /* The leading edge is the drawer's own resize handle. It is positioned
393
412
  over the border rather than beside it so the panel does not shift when
@@ -553,6 +572,18 @@ window.__ModuleLoader__.load({
553
572
  .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}
554
573
  .fv-tab[aria-selected="true"]{border-color:transparent;background:var(--dsw-alias-brand-primary,#4d6bfe);color:var(--dsw-alias-label-primary-foreground,#fff)}
555
574
  .fv-tab-diff{display:inline-flex;align-items:center;gap:4px}
575
+ .fv-path-copy-wrap{position:relative;display:inline-flex;align-items:center}
576
+ .fv-tab.fv-tab-copy-path{display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,#fff));color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;cursor:pointer;user-select:none;transition:all .15s ease}
577
+ .fv-tab.fv-tab-copy-path:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,var(--dsw-alias-border-l2))}
578
+ .fv-tab.fv-tab-copy-path[aria-expanded="true"]{background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-alias-brand-primary,#4d6bfe);border-color:var(--dsw-alias-brand-primary,#4d6bfe)}
579
+ .fv-tab-copy-path .fv-tab-arrow{font-size:9px;margin-left:2px;opacity:.75}
580
+ .fv-path-copy-menu{position:absolute;top:calc(100% + 5px);left:0;z-index:60;min-width:210px;max-width:380px;padding:5px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);animation:fv-menu-in .12s cubic-bezier(.2,.8,.2,1)}
581
+ .fv-path-copy-item{display:flex;flex-direction:column;gap:2px;width:100%;padding:6px 10px;border:none;border-radius:8px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;user-select:none;transition:background-color .12s ease}
582
+ .fv-path-copy-item:hover{background:var(--dsw-alias-interactive-bg-hover)}
583
+ .fv-path-copy-item-head{display:flex;align-items:center;gap:8px;font-size:12.5px;font-weight:500}
584
+ .fv-path-copy-item-icon{flex:none;font-size:13px}
585
+ .fv-path-copy-item-label{flex:1 1 auto;white-space:nowrap}
586
+ .fv-path-copy-item-preview{font-size:11px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-code,monospace);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-left:21px;line-height:1.35}
556
587
  .fv-diff-stat{font-size:11px;font-weight:600;font-variant-numeric:tabular-nums;margin-left:2px}
557
588
  .fv-diff-add{color:var(--dsw-alias-state-success-primary,#10b981)}
558
589
  .fv-diff-del{color:var(--dsw-alias-state-error-primary,#ef4444)}
@@ -635,7 +666,19 @@ window.__ModuleLoader__.load({
635
666
  .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}
636
667
  /* Working-tree changes and a commit's patch share the commit-row visuals
637
668
  so the two panels read as one surface. */
638
- .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}
669
+ .fv-git-changes{display:flex;flex:none;flex-direction:column;gap:1px;min-height:48px;overflow-y:auto;overscroll-behavior:contain;padding-bottom:2px}
670
+ .fv-git-panel[data-v-resizing="true"]{cursor:row-resize!important;user-select:none!important}
671
+ .fv-git-panel[data-v-resizing="true"] *{pointer-events:none!important}
672
+ .fv-git-panel[data-v-resizing="true"] .fv-git-v-resizer,
673
+ .fv-git-panel[data-v-resizing="true"] .fv-git-v-resizer *{pointer-events:auto!important}
674
+ .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}
675
+ .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}
676
+ .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}
677
+ .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}
678
+ .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)}
679
+ .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)}
680
+ .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)}
681
+ .fv-git-v-resizer:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:-1px}
639
682
  .fv-git-change-group{display:flex;flex-direction:column;gap:1px}
640
683
  .fv-git-refresh{width:22px;height:22px;margin-left:auto}
641
684
  .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}
@@ -841,7 +884,8 @@ window.__ModuleLoader__.load({
841
884
  .fv-git-more{height:38px!important;font-size:13px!important}
842
885
  .fv-commit-file{min-height:42px!important;font-size:13.5px!important}
843
886
  .fv-commit-path{font-size:12px!important}
844
- .fv-git-changes{max-height:none!important}
887
+ .fv-git-changes{height:auto!important;max-height:none!important}
888
+ .fv-git-v-resizer{display:none!important}
845
889
  .fv-commit-diff-file .fv-diff-table{font-size:12px!important}
846
890
  }
847
891
  `;
@@ -1148,6 +1192,27 @@ window.__ModuleLoader__.load({
1148
1192
  } catch { /* storage is unavailable */ }
1149
1193
  }
1150
1194
 
1195
+ function readGitChangesHeight() {
1196
+ try {
1197
+ const raw = globalThis.localStorage?.getItem(GIT_CHANGES_HEIGHT_KEY);
1198
+ const parsed = Number(raw);
1199
+ if (Number.isFinite(parsed) && parsed >= MIN_GIT_CHANGES_HEIGHT && parsed <= MAX_GIT_CHANGES_HEIGHT) {
1200
+ return Math.round(parsed);
1201
+ }
1202
+ } catch { /* storage is unavailable */ }
1203
+ return DEFAULT_GIT_CHANGES_HEIGHT;
1204
+ }
1205
+
1206
+ function writeGitChangesHeight(height) {
1207
+ try {
1208
+ if (typeof height === "number" && Number.isFinite(height)) {
1209
+ globalThis.localStorage?.setItem(GIT_CHANGES_HEIGHT_KEY, String(Math.round(height)));
1210
+ } else {
1211
+ globalThis.localStorage?.removeItem(GIT_CHANGES_HEIGHT_KEY);
1212
+ }
1213
+ } catch { /* storage is unavailable */ }
1214
+ }
1215
+
1151
1216
  /**
1152
1217
  * Read the per-file language overrides.
1153
1218
  *
@@ -2310,10 +2375,72 @@ window.__ModuleLoader__.load({
2310
2375
  const canCheckout = repo !== null && viewingBranch !== "" && viewingBranch !== current
2311
2376
  && checkoutable && state.busy !== true;
2312
2377
 
2378
+ const [changesHeight, setChangesHeight] = react.useState(readGitChangesHeight);
2379
+ const [isResizingChanges, setIsResizingChanges] = react.useState(false);
2380
+ const changesResizeRef = react.useRef(null);
2381
+ const changesHeightRef = react.useRef(changesHeight);
2382
+ changesHeightRef.current = changesHeight;
2383
+
2384
+ const onChangesResizerPointerDown = (event) => {
2385
+ if (event.button !== 0 && event.button !== undefined) return;
2386
+ event.preventDefault();
2387
+ event.stopPropagation();
2388
+ const startY = event.clientY;
2389
+ const startH = changesHeightRef.current;
2390
+ changesResizeRef.current = { startY, startH };
2391
+ setIsResizingChanges(true);
2392
+ try { event.currentTarget.setPointerCapture(event.pointerId); } catch (_) {}
2393
+ };
2394
+
2395
+ const onChangesResizerPointerMove = (event) => {
2396
+ if (!changesResizeRef.current) return;
2397
+ const { startY, startH } = changesResizeRef.current;
2398
+ const deltaY = event.clientY - startY;
2399
+ const panelEl = event.currentTarget.closest(".fv-git-panel");
2400
+ const panelHeight = panelEl ? panelEl.clientHeight : 600;
2401
+ const maxBound = Math.max(MIN_GIT_CHANGES_HEIGHT, panelHeight - 140);
2402
+ const nextH = Math.round(Math.min(Math.min(MAX_GIT_CHANGES_HEIGHT, maxBound), Math.max(MIN_GIT_CHANGES_HEIGHT, startH + deltaY)));
2403
+ setChangesHeight(nextH);
2404
+ };
2405
+
2406
+ const onChangesResizerPointerUp = (event) => {
2407
+ if (!changesResizeRef.current) return;
2408
+ changesResizeRef.current = null;
2409
+ setIsResizingChanges(false);
2410
+ try { event.currentTarget.releasePointerCapture(event.pointerId); } catch (_) {}
2411
+ writeGitChangesHeight(changesHeightRef.current);
2412
+ };
2413
+
2414
+ const onChangesResizerDoubleClick = (event) => {
2415
+ event.preventDefault();
2416
+ event.stopPropagation();
2417
+ setChangesHeight(DEFAULT_GIT_CHANGES_HEIGHT);
2418
+ writeGitChangesHeight(DEFAULT_GIT_CHANGES_HEIGHT);
2419
+ };
2420
+
2421
+ const onChangesResizerKeyDown = (event) => {
2422
+ if (event.key === "ArrowUp") {
2423
+ event.preventDefault();
2424
+ const nextH = Math.max(MIN_GIT_CHANGES_HEIGHT, changesHeightRef.current - 16);
2425
+ setChangesHeight(nextH);
2426
+ writeGitChangesHeight(nextH);
2427
+ } else if (event.key === "ArrowDown") {
2428
+ event.preventDefault();
2429
+ const nextH = Math.min(MAX_GIT_CHANGES_HEIGHT, changesHeightRef.current + 16);
2430
+ setChangesHeight(nextH);
2431
+ writeGitChangesHeight(nextH);
2432
+ } else if (event.key === "Home" || event.key === "Enter") {
2433
+ event.preventDefault();
2434
+ setChangesHeight(DEFAULT_GIT_CHANGES_HEIGHT);
2435
+ writeGitChangesHeight(DEFAULT_GIT_CHANGES_HEIGHT);
2436
+ }
2437
+ };
2438
+
2313
2439
  const wrap = (...children) => react.createElement("div", {
2314
2440
  className: "fv-tree fv-git-panel",
2315
2441
  role: "group",
2316
2442
  "aria-label": "Git 面板",
2443
+ "data-v-resizing": isResizingChanges ? "true" : "false",
2317
2444
  style: typeof width === "number" ? { width: width + "px" } : undefined,
2318
2445
  }, header, ...children);
2319
2446
 
@@ -2451,7 +2578,14 @@ window.__ModuleLoader__.load({
2451
2578
  : null);
2452
2579
  }
2453
2580
 
2454
- const changesSection = react.createElement("div", { className: "fv-git-changes" },
2581
+ const changesSection = react.createElement("div", {
2582
+ className: "fv-git-changes",
2583
+ style: {
2584
+ height: changesHeight + "px",
2585
+ maxHeight: "none",
2586
+ flex: "none",
2587
+ },
2588
+ },
2455
2589
  react.createElement("div", { className: "fv-git-section" },
2456
2590
  react.createElement("span", null, "工作区改动"),
2457
2591
  react.createElement("span", { className: "fv-git-count" },
@@ -2465,6 +2599,37 @@ window.__ModuleLoader__.load({
2465
2599
  }, react.createElement(SvgIconRefresh, { size: 13 }))),
2466
2600
  changesBody);
2467
2601
 
2602
+ const vResizer = react.createElement("div", {
2603
+ className: "fv-git-v-resizer",
2604
+ role: "separator",
2605
+ tabIndex: 0,
2606
+ "aria-orientation": "horizontal",
2607
+ "aria-label": "调整工作区改动与提交记录高度",
2608
+ "aria-valuenow": changesHeight,
2609
+ "aria-valuemin": MIN_GIT_CHANGES_HEIGHT,
2610
+ "aria-valuemax": MAX_GIT_CHANGES_HEIGHT,
2611
+ title: "拖动调整区域高度,双击恢复默认",
2612
+ "data-dragging": isResizingChanges ? "true" : "false",
2613
+ onPointerDown: onChangesResizerPointerDown,
2614
+ onPointerMove: onChangesResizerPointerMove,
2615
+ onPointerUp: onChangesResizerPointerUp,
2616
+ onPointerCancel: onChangesResizerPointerUp,
2617
+ onDoubleClick: onChangesResizerDoubleClick,
2618
+ onKeyDown: onChangesResizerKeyDown,
2619
+ },
2620
+ react.createElement("div", { className: "fv-git-v-resizer-line" }),
2621
+ react.createElement("button", {
2622
+ type: "button",
2623
+ tabIndex: -1,
2624
+ className: "fv-git-v-resizer-btn",
2625
+ "aria-label": "拖动手柄",
2626
+ title: "按住拖动调整高度,双击恢复默认",
2627
+ "data-dragging": isResizingChanges ? "true" : "false",
2628
+ },
2629
+ react.createElement("span", { className: "fv-git-v-resizer-grip" })
2630
+ )
2631
+ );
2632
+
2468
2633
  const commitRows = commits.map((commit) => react.createElement("button", {
2469
2634
  key: commit.sha,
2470
2635
  type: "button",
@@ -2511,6 +2676,7 @@ window.__ModuleLoader__.load({
2511
2676
 
2512
2677
  return wrap(toolbar,
2513
2678
  changesSection,
2679
+ vResizer,
2514
2680
  react.createElement("div", { className: "fv-git-section" },
2515
2681
  react.createElement("span", null, "提交记录"),
2516
2682
  react.createElement("span", { className: "fv-git-count" }, state.total > 0 ? String(state.total) : "")),
@@ -3350,7 +3516,20 @@ window.__ModuleLoader__.load({
3350
3516
  return react.createElement(react.Fragment, null,
3351
3517
  react.createElement("div", {
3352
3518
  className: "fv-context-backdrop",
3519
+ onPointerDown: (event) => {
3520
+ event.preventDefault();
3521
+ event.stopPropagation();
3522
+ if (Date.now() - mountTimeRef.current < 400) return;
3523
+ onClose();
3524
+ },
3525
+ onMouseDown: (event) => {
3526
+ event.preventDefault();
3527
+ event.stopPropagation();
3528
+ if (Date.now() - mountTimeRef.current < 400) return;
3529
+ onClose();
3530
+ },
3353
3531
  onClick: (event) => {
3532
+ event.preventDefault();
3354
3533
  event.stopPropagation();
3355
3534
  if (Date.now() - mountTimeRef.current < 400) return;
3356
3535
  onClose();
@@ -3450,6 +3629,102 @@ window.__ModuleLoader__.load({
3450
3629
  : null);
3451
3630
  }
3452
3631
 
3632
+ function resolveAbsPath(rootPath, relPath) {
3633
+ if (!rootPath) return relPath || "";
3634
+ const cleanRel = (relPath || "").replace(/^[\\/]+/, "");
3635
+ if (rootPath.includes("\\")) {
3636
+ return rootPath.replace(/[\\/]+$/, "") + "\\" + cleanRel.replace(/\//g, "\\");
3637
+ }
3638
+ return rootPath.replace(/[\\/]+$/, "") + "/" + cleanRel.replace(/\\/g, "/");
3639
+ }
3640
+
3641
+ /** Path copy dropdown menu on viewer toolbar. */
3642
+ function PathCopyDropdown({ meta, root, onToast }) {
3643
+ const [open, setOpen] = react.useState(false);
3644
+ const containerRef = react.useRef(null);
3645
+
3646
+ react.useEffect(() => {
3647
+ if (!open) return undefined;
3648
+ const handlePointerDown = (e) => {
3649
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
3650
+ setOpen(false);
3651
+ }
3652
+ };
3653
+ const handleKeyDown = (e) => {
3654
+ if (e.key === "Escape") setOpen(false);
3655
+ };
3656
+ window.addEventListener("pointerdown", handlePointerDown);
3657
+ window.addEventListener("keydown", handleKeyDown);
3658
+ return () => {
3659
+ window.removeEventListener("pointerdown", handlePointerDown);
3660
+ window.removeEventListener("keydown", handleKeyDown);
3661
+ };
3662
+ }, [open]);
3663
+
3664
+ if (!meta || (!meta.path && !meta.displayPath)) return null;
3665
+
3666
+ const relPath = meta.displayPath || meta.path || "";
3667
+ const absPath = resolveAbsPath(root, relPath);
3668
+
3669
+ const handleCopy = (e, text, label) => {
3670
+ e.preventDefault();
3671
+ e.stopPropagation();
3672
+ copyToClipboard(text).then((ok) => {
3673
+ if (onToast) onToast(ok ? "已复制" + label : "复制失败");
3674
+ setOpen(false);
3675
+ });
3676
+ };
3677
+
3678
+ return react.createElement("div", {
3679
+ ref: containerRef,
3680
+ className: "fv-path-copy-wrap",
3681
+ },
3682
+ react.createElement("button", {
3683
+ type: "button",
3684
+ className: "fv-tab fv-tab-copy-path",
3685
+ "aria-expanded": open ? "true" : "false",
3686
+ "aria-label": "路径复制",
3687
+ title: "点击选择复制相对路径或绝对路径",
3688
+ onClick: (e) => {
3689
+ e.stopPropagation();
3690
+ setOpen((curr) => !curr);
3691
+ },
3692
+ },
3693
+ react.createElement(SvgIconCopy, { size: 12 }),
3694
+ react.createElement("span", { className: "fv-tab-text" }, "复制路径"),
3695
+ react.createElement("span", { className: "fv-tab-arrow" }, open ? "▴" : "▾")),
3696
+ open
3697
+ ? react.createElement("div", {
3698
+ className: "fv-path-copy-menu",
3699
+ role: "menu",
3700
+ "aria-label": "路径复制选项",
3701
+ },
3702
+ react.createElement("button", {
3703
+ type: "button",
3704
+ className: "fv-path-copy-item",
3705
+ role: "menuitem",
3706
+ title: relPath,
3707
+ onClick: (e) => handleCopy(e, relPath, "相对路径"),
3708
+ },
3709
+ react.createElement("div", { className: "fv-path-copy-item-head" },
3710
+ react.createElement("span", { className: "fv-path-copy-item-icon" }, "📋"),
3711
+ react.createElement("span", { className: "fv-path-copy-item-label" }, "复制相对路径")),
3712
+ react.createElement("span", { className: "fv-path-copy-item-preview" }, relPath)),
3713
+ react.createElement("div", { className: "fv-context-divider" }),
3714
+ react.createElement("button", {
3715
+ type: "button",
3716
+ className: "fv-path-copy-item",
3717
+ role: "menuitem",
3718
+ title: absPath,
3719
+ onClick: (e) => handleCopy(e, absPath, "绝对路径"),
3720
+ },
3721
+ react.createElement("div", { className: "fv-path-copy-item-head" },
3722
+ react.createElement("span", { className: "fv-path-copy-item-icon" }, "📁"),
3723
+ react.createElement("span", { className: "fv-path-copy-item-label" }, "复制绝对路径")),
3724
+ react.createElement("span", { className: "fv-path-copy-item-preview" }, absPath)))
3725
+ : null);
3726
+ }
3727
+
3453
3728
  function apply(ctx) {
3454
3729
  ensureStyles(typeof document === "undefined" ? null : document);
3455
3730
  // `null` is closed; an object is open. Carrying the requesting session lets
@@ -3945,7 +4220,7 @@ window.__ModuleLoader__.load({
3945
4220
 
3946
4221
  /** Line-numbered diff viewer with dual line number gutters. */
3947
4222
  /** Text, Markdown and JSON: one windowed read, three renderings. */
3948
- function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl }) {
4223
+ function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast }) {
3949
4224
  // Callers mount this with a per-file key, so paging state starts fresh
3950
4225
  // for every file instead of leaking the previous file's window into the
3951
4226
  // first request.
@@ -4231,11 +4506,16 @@ window.__ModuleLoader__.load({
4231
4506
  : [contentTab, sourceTab, diffTab])].filter((tab) => tab !== null)
4232
4507
  : [];
4233
4508
 
4234
- const tabs = showTabs || Boolean(backControl || langControl || sizeControl)
4509
+ const copyControl = meta !== null
4510
+ ? react.createElement(PathCopyDropdown, { meta, root, onToast })
4511
+ : null;
4512
+
4513
+ const tabs = showTabs || Boolean(backControl || langControl || sizeControl || copyControl)
4235
4514
  ? react.createElement("div", { className: "fv-sheet-tabs", role: "tablist" },
4236
4515
  react.createElement("div", { className: "fv-sheet-tabs-start" },
4237
4516
  backControl,
4238
- ...tabButtons),
4517
+ ...tabButtons,
4518
+ copyControl),
4239
4519
  (sizeControl || langControl)
4240
4520
  ? react.createElement("div", { className: "fv-sheet-tabs-end" },
4241
4521
  sizeControl,
@@ -4307,7 +4587,11 @@ window.__ModuleLoader__.load({
4307
4587
  }
4308
4588
 
4309
4589
  /** Pick the viewer for a file and keep unsupported formats useful. */
4310
- function FileView({ meta, root, api, wrap, lang, onDownload, commit, repo, loading, backControl, sizeControl, langControl }) {
4590
+ function FileView({ meta, root, api, wrap, lang, onDownload, commit, repo, loading, backControl, sizeControl, langControl, onToast }) {
4591
+ const copyControl = meta !== null
4592
+ ? react.createElement(PathCopyDropdown, { meta, root, onToast })
4593
+ : null;
4594
+
4311
4595
  if (meta === null) {
4312
4596
  // A tab exists but its metadata is still in flight. Saying "pick a
4313
4597
  // file" there would be a lie, and the reader would click again.
@@ -4318,8 +4602,8 @@ window.__ModuleLoader__.load({
4318
4602
  }
4319
4603
  if (meta.tooLarge || meta.kind === "binary") {
4320
4604
  return react.createElement("div", { className: "fv-binary-pane" },
4321
- backControl ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
4322
- react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl),
4605
+ (backControl || copyControl) ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
4606
+ react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl, copyControl),
4323
4607
  sizeControl ? react.createElement("div", { className: "fv-sheet-tabs-end" }, sizeControl) : null) : null,
4324
4608
  react.createElement("div", { className: "fv-note" },
4325
4609
  react.createElement("strong", null, meta.name),
@@ -4354,9 +4638,9 @@ window.__ModuleLoader__.load({
4354
4638
  react.createElement("span", { className: "fv-history-hint" }, "这是历史版本,不是工作区当前内容。"))
4355
4639
  : null;
4356
4640
 
4357
- const headBar = (backControl || sizeControl)
4641
+ const headBar = (backControl || sizeControl || copyControl)
4358
4642
  ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
4359
- react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl),
4643
+ react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl, copyControl),
4360
4644
  sizeControl ? react.createElement("div", { className: "fv-sheet-tabs-end" }, sizeControl) : null)
4361
4645
  : null;
4362
4646
 
@@ -4374,7 +4658,7 @@ window.__ModuleLoader__.load({
4374
4658
  return react.createElement("div", { className: "fv-content" }, headBar, react.createElement(DocView, { key, meta, root }));
4375
4659
  }
4376
4660
  return react.createElement(react.Fragment, null, banner,
4377
- react.createElement(TextView, { key, meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl }));
4661
+ react.createElement(TextView, { key, meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast }));
4378
4662
  }
4379
4663
 
4380
4664
 
@@ -4410,11 +4694,7 @@ window.__ModuleLoader__.load({
4410
4694
  if (!menu) return null;
4411
4695
  const { x, y, entry } = menu;
4412
4696
  const relPath = entry.displayPath || entry.path;
4413
- const absPath = rootPath
4414
- ? (rootPath.includes("\\")
4415
- ? (rootPath.replace(/[\\/]+$/, "") + "\\" + relPath.replace(/\//g, "\\"))
4416
- : (rootPath.replace(/[\\/]+$/, "") + "/" + relPath.replace(/\\/g, "/")))
4417
- : relPath;
4697
+ const absPath = resolveAbsPath(rootPath, relPath);
4418
4698
 
4419
4699
  const handleCopyRel = (e) => {
4420
4700
  e.stopPropagation();
@@ -4452,7 +4732,20 @@ window.__ModuleLoader__.load({
4452
4732
  return react.createElement(react.Fragment, null,
4453
4733
  react.createElement("div", {
4454
4734
  className: "fv-context-backdrop",
4735
+ onPointerDown: (e) => {
4736
+ e.preventDefault();
4737
+ e.stopPropagation();
4738
+ if (Date.now() - mountTimeRef.current < 400) return;
4739
+ onClose();
4740
+ },
4741
+ onMouseDown: (e) => {
4742
+ e.preventDefault();
4743
+ e.stopPropagation();
4744
+ if (Date.now() - mountTimeRef.current < 400) return;
4745
+ onClose();
4746
+ },
4455
4747
  onClick: (e) => {
4748
+ e.preventDefault();
4456
4749
  e.stopPropagation();
4457
4750
  if (Date.now() - mountTimeRef.current < 400) return;
4458
4751
  onClose();
@@ -4867,6 +5160,15 @@ window.__ModuleLoader__.load({
4867
5160
  // it. Kept apart from the tree's `contextMenu`: the two menus describe
4868
5161
  // different subjects and must never be confused for one another.
4869
5162
  const [tabMenu, setTabMenu] = react.useState(null);
5163
+ const lastMenuClosedAtRef = react.useRef(0);
5164
+ const closeContextMenu = react.useCallback(() => {
5165
+ lastMenuClosedAtRef.current = Date.now();
5166
+ setContextMenu(null);
5167
+ }, []);
5168
+ const closeTabMenu = react.useCallback(() => {
5169
+ lastMenuClosedAtRef.current = Date.now();
5170
+ setTabMenu(null);
5171
+ }, []);
4870
5172
  const tabsRef = react.useRef(null);
4871
5173
  tabsRef.current = tabs;
4872
5174
  const activeTabIdRef = react.useRef("");
@@ -5816,6 +6118,8 @@ window.__ModuleLoader__.load({
5816
6118
  onMouseDown: (event) => {
5817
6119
  if (event.target !== event.currentTarget) return;
5818
6120
  if (Date.now() - openedAtRef.current < 400) return;
6121
+ if (contextMenu !== null || tabMenu !== null) return;
6122
+ if (Date.now() - lastMenuClosedAtRef.current < 400) return;
5819
6123
  openStore.set(null);
5820
6124
  },
5821
6125
  },
@@ -6074,6 +6378,7 @@ window.__ModuleLoader__.load({
6074
6378
  backControl,
6075
6379
  sizeControl,
6076
6380
  langControl,
6381
+ onToast: showToast,
6077
6382
  })
6078
6383
  : react.createElement(FileView, {
6079
6384
  meta,
@@ -6092,12 +6397,13 @@ window.__ModuleLoader__.load({
6092
6397
  backControl,
6093
6398
  sizeControl,
6094
6399
  langControl,
6400
+ onToast: showToast,
6095
6401
  })))),
6096
6402
  contextMenu !== null
6097
6403
  ? react.createElement(ContextMenu, {
6098
6404
  menu: contextMenu,
6099
6405
  rootPath: activeRoot ? (activeRoot.path || activeRoot.id) : root,
6100
- onClose: () => setContextMenu(null),
6406
+ onClose: closeContextMenu,
6101
6407
  onMention: mention,
6102
6408
  onToast: showToast,
6103
6409
  onDownload: handleDownloadItem,
@@ -6110,7 +6416,7 @@ window.__ModuleLoader__.load({
6110
6416
  ? react.createElement(TabContextMenu, {
6111
6417
  menu: tabMenu,
6112
6418
  tabs,
6113
- onClose: () => setTabMenu(null),
6419
+ onClose: closeTabMenu,
6114
6420
  onAction: onTabAction,
6115
6421
  })
6116
6422
  : null,
@@ -6424,12 +6730,13 @@ window.__ModuleLoader__.load({
6424
6730
  mediaTypeOf, messageOf, createStore, createRequest, blobOf, ensureStyles,
6425
6731
  ENTRY_POSITION_KEY, DRAG_SLOP, settleEntry, readEntryPosition, writeEntryPosition,
6426
6732
  TREE_WIDTH_KEY, DEFAULT_TREE_WIDTH, MIN_TREE_WIDTH, MAX_TREE_WIDTH, readTreeWidth, writeTreeWidth,
6733
+ GIT_CHANGES_HEIGHT_KEY, DEFAULT_GIT_CHANGES_HEIGHT, MIN_GIT_CHANGES_HEIGHT, MAX_GIT_CHANGES_HEIGHT, readGitChangesHeight, writeGitChangesHeight,
6427
6734
  SHELL_WIDTH_KEY, MIN_SHELL_WIDTH, readShellWidth, writeShellWidth, clampShellWidth,
6428
6735
  LANG_OVERRIDES_KEY, LANG_OVERRIDES_MAX, readLangOverrides, writeLangOverrides, readLangOverride, writeLangOverride,
6429
6736
  langOverrideKey, isKnownLanguageId, setLanguageCatalog, getLanguageCatalog: () => languageCatalog, LanguagePicker,
6430
6737
  MARKDOWN_LABELS, MARKDOWN_CODE_LABELS, READ_BLOCK_LABELS, JSON_TREE_LABELS, DIFF_BLOCK_LABELS, ErrorBoundary,
6431
6738
  openViewerForPath, wrapWorkspaceOpenPath, wrapConnectionRpc, wrapChangesOpenFetch, wrapSidebarRight, parseFileResource, setupGlobalFileClickInterceptor, extractClickedPath, normalizeClickedPath, looksLikeFilePath, isFileOpenControl, isFileMentionCode, renderRootOptions,
6432
- triggerDownload, performDownload, isDesktopShell, SvgIconDownload, SvgIconBranch, SvgIconCommit,
6739
+ triggerDownload, performDownload, isDesktopShell, SvgIconDownload, SvgIconCopy, PathCopyDropdown, resolveAbsPath, SvgIconBranch, SvgIconCommit,
6433
6740
  // Git panel: controller, views and the small formatters they share.
6434
6741
  GIT_PAGE_SIZE, createGitController, GitSidebar, CommitDetail, WorkspaceChangeDetail, DiffView, kindOfPath,
6435
6742
  formatCommitDate, formatCommitTimestamp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weniatall/dsh-file-viewer",
3
- "version": "0.3.8",
3
+ "version": "0.3.13",
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",