@weniatall/dsh-file-viewer 0.3.12 → 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 +145 -15
  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) {
@@ -557,6 +572,18 @@ window.__ModuleLoader__.load({
557
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}
558
573
  .fv-tab[aria-selected="true"]{border-color:transparent;background:var(--dsw-alias-brand-primary,#4d6bfe);color:var(--dsw-alias-label-primary-foreground,#fff)}
559
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}
560
587
  .fv-diff-stat{font-size:11px;font-weight:600;font-variant-numeric:tabular-nums;margin-left:2px}
561
588
  .fv-diff-add{color:var(--dsw-alias-state-success-primary,#10b981)}
562
589
  .fv-diff-del{color:var(--dsw-alias-state-error-primary,#ef4444)}
@@ -3602,6 +3629,102 @@ window.__ModuleLoader__.load({
3602
3629
  : null);
3603
3630
  }
3604
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
+
3605
3728
  function apply(ctx) {
3606
3729
  ensureStyles(typeof document === "undefined" ? null : document);
3607
3730
  // `null` is closed; an object is open. Carrying the requesting session lets
@@ -4097,7 +4220,7 @@ window.__ModuleLoader__.load({
4097
4220
 
4098
4221
  /** Line-numbered diff viewer with dual line number gutters. */
4099
4222
  /** Text, Markdown and JSON: one windowed read, three renderings. */
4100
- function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl }) {
4223
+ function TextView({ meta, root, wrap, lang, commit, repo, backControl, sizeControl, langControl, onToast }) {
4101
4224
  // Callers mount this with a per-file key, so paging state starts fresh
4102
4225
  // for every file instead of leaking the previous file's window into the
4103
4226
  // first request.
@@ -4383,11 +4506,16 @@ window.__ModuleLoader__.load({
4383
4506
  : [contentTab, sourceTab, diffTab])].filter((tab) => tab !== null)
4384
4507
  : [];
4385
4508
 
4386
- 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)
4387
4514
  ? react.createElement("div", { className: "fv-sheet-tabs", role: "tablist" },
4388
4515
  react.createElement("div", { className: "fv-sheet-tabs-start" },
4389
4516
  backControl,
4390
- ...tabButtons),
4517
+ ...tabButtons,
4518
+ copyControl),
4391
4519
  (sizeControl || langControl)
4392
4520
  ? react.createElement("div", { className: "fv-sheet-tabs-end" },
4393
4521
  sizeControl,
@@ -4459,7 +4587,11 @@ window.__ModuleLoader__.load({
4459
4587
  }
4460
4588
 
4461
4589
  /** Pick the viewer for a file and keep unsupported formats useful. */
4462
- 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
+
4463
4595
  if (meta === null) {
4464
4596
  // A tab exists but its metadata is still in flight. Saying "pick a
4465
4597
  // file" there would be a lie, and the reader would click again.
@@ -4470,8 +4602,8 @@ window.__ModuleLoader__.load({
4470
4602
  }
4471
4603
  if (meta.tooLarge || meta.kind === "binary") {
4472
4604
  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),
4605
+ (backControl || copyControl) ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
4606
+ react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl, copyControl),
4475
4607
  sizeControl ? react.createElement("div", { className: "fv-sheet-tabs-end" }, sizeControl) : null) : null,
4476
4608
  react.createElement("div", { className: "fv-note" },
4477
4609
  react.createElement("strong", null, meta.name),
@@ -4506,9 +4638,9 @@ window.__ModuleLoader__.load({
4506
4638
  react.createElement("span", { className: "fv-history-hint" }, "这是历史版本,不是工作区当前内容。"))
4507
4639
  : null;
4508
4640
 
4509
- const headBar = (backControl || sizeControl)
4641
+ const headBar = (backControl || sizeControl || copyControl)
4510
4642
  ? react.createElement("div", { className: "fv-sheet-tabs", role: "toolbar" },
4511
- react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl),
4643
+ react.createElement("div", { className: "fv-sheet-tabs-start" }, backControl, copyControl),
4512
4644
  sizeControl ? react.createElement("div", { className: "fv-sheet-tabs-end" }, sizeControl) : null)
4513
4645
  : null;
4514
4646
 
@@ -4526,7 +4658,7 @@ window.__ModuleLoader__.load({
4526
4658
  return react.createElement("div", { className: "fv-content" }, headBar, react.createElement(DocView, { key, meta, root }));
4527
4659
  }
4528
4660
  return react.createElement(react.Fragment, null, banner,
4529
- 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 }));
4530
4662
  }
4531
4663
 
4532
4664
 
@@ -4562,11 +4694,7 @@ window.__ModuleLoader__.load({
4562
4694
  if (!menu) return null;
4563
4695
  const { x, y, entry } = menu;
4564
4696
  const relPath = entry.displayPath || entry.path;
4565
- const absPath = rootPath
4566
- ? (rootPath.includes("\\")
4567
- ? (rootPath.replace(/[\\/]+$/, "") + "\\" + relPath.replace(/\//g, "\\"))
4568
- : (rootPath.replace(/[\\/]+$/, "") + "/" + relPath.replace(/\\/g, "/")))
4569
- : relPath;
4697
+ const absPath = resolveAbsPath(rootPath, relPath);
4570
4698
 
4571
4699
  const handleCopyRel = (e) => {
4572
4700
  e.stopPropagation();
@@ -6250,6 +6378,7 @@ window.__ModuleLoader__.load({
6250
6378
  backControl,
6251
6379
  sizeControl,
6252
6380
  langControl,
6381
+ onToast: showToast,
6253
6382
  })
6254
6383
  : react.createElement(FileView, {
6255
6384
  meta,
@@ -6268,6 +6397,7 @@ window.__ModuleLoader__.load({
6268
6397
  backControl,
6269
6398
  sizeControl,
6270
6399
  langControl,
6400
+ onToast: showToast,
6271
6401
  })))),
6272
6402
  contextMenu !== null
6273
6403
  ? react.createElement(ContextMenu, {
@@ -6606,7 +6736,7 @@ window.__ModuleLoader__.load({
6606
6736
  langOverrideKey, isKnownLanguageId, setLanguageCatalog, getLanguageCatalog: () => languageCatalog, LanguagePicker,
6607
6737
  MARKDOWN_LABELS, MARKDOWN_CODE_LABELS, READ_BLOCK_LABELS, JSON_TREE_LABELS, DIFF_BLOCK_LABELS, ErrorBoundary,
6608
6738
  openViewerForPath, wrapWorkspaceOpenPath, wrapConnectionRpc, wrapChangesOpenFetch, wrapSidebarRight, parseFileResource, setupGlobalFileClickInterceptor, extractClickedPath, normalizeClickedPath, looksLikeFilePath, isFileOpenControl, isFileMentionCode, renderRootOptions,
6609
- triggerDownload, performDownload, isDesktopShell, SvgIconDownload, SvgIconBranch, SvgIconCommit,
6739
+ triggerDownload, performDownload, isDesktopShell, SvgIconDownload, SvgIconCopy, PathCopyDropdown, resolveAbsPath, SvgIconBranch, SvgIconCommit,
6610
6740
  // Git panel: controller, views and the small formatters they share.
6611
6741
  GIT_PAGE_SIZE, createGitController, GitSidebar, CommitDetail, WorkspaceChangeDetail, DiffView, kindOfPath,
6612
6742
  formatCommitDate, formatCommitTimestamp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weniatall/dsh-file-viewer",
3
- "version": "0.3.12",
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",