@youtyan/code-viewer 0.8.2 → 0.8.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -26055,6 +26055,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
26055
26055
  {
26056
26056
  kind: "paragraph",
26057
26057
  text: "In large repositories the sidebar loads folder children on demand. Folders you open are remembered and automatically re-expanded after a reload."
26058
+ },
26059
+ {
26060
+ kind: "paragraph",
26061
+ text: 'Symlinks show a distinct icon and a "→ target" label so they are never mistaken for a regular file or folder, and clicking one navigates to its resolved target. A broken symlink is visually flagged and disabled. Files with pending git changes (new, modified, renamed, deleted) show a status badge in the tree instead of the regular type icon.'
26058
26062
  }
26059
26063
  ]
26060
26064
  },
@@ -26722,6 +26726,10 @@ code-viewer query agent-help`
26722
26726
  {
26723
26727
  kind: "paragraph",
26724
26728
  text: "大きいリポジトリではサイドバーがフォルダの中身を必要に応じて読み込みます。開いたフォルダは記憶され、次回のリロード時に同じ状態で展開し直されます。"
26729
+ },
26730
+ {
26731
+ kind: "paragraph",
26732
+ text: "シンボリックリンクは専用アイコンと「→ リンク先」ラベルで表示されるため通常のファイル/フォルダと区別でき、クリックするとリンク先に遷移します。リンク切れのシンボリックリンクは無効化されたことが分かる表示になります。未コミットの git 変更(新規・変更・リネーム・削除)があるファイルは、通常の種類アイコンの代わりにステータスバッジがツリーに表示されます。"
26725
26733
  }
26726
26734
  ]
26727
26735
  },
@@ -30719,6 +30727,11 @@ code-viewer query agent-help`
30719
30727
  node.children_omitted = true;
30720
30728
  node.children_omitted_reason = f2.children_omitted_reason;
30721
30729
  }
30730
+ if (f2.is_symlink) {
30731
+ node.is_symlink = true;
30732
+ node.symlink_target = f2.symlink_target;
30733
+ node.resolved_path = f2.resolved_path;
30734
+ }
30722
30735
  continue;
30723
30736
  }
30724
30737
  node.files.push(f2);
@@ -30768,6 +30781,8 @@ code-viewer query agent-help`
30768
30781
  li.dataset.childrenOmittedReason = dir.children_omitted_reason;
30769
30782
  if (dir.explicit)
30770
30783
  li.dataset.explicit = "true";
30784
+ if (dir.is_symlink)
30785
+ li.classList.add("symlink-row");
30771
30786
  if (dir.children_omitted) {
30772
30787
  li.classList.add("children-omitted");
30773
30788
  li.classList.add(dir.children_omitted_reason === "heavy" ? "children-omitted-heavy" : "children-omitted-internal");
@@ -30801,6 +30816,9 @@ code-viewer query agent-help`
30801
30816
  omitted.title = badge.title;
30802
30817
  label.appendChild(omitted);
30803
30818
  }
30819
+ const dirSymlinkLabel = symlinkTargetLabel(dir);
30820
+ if (dirSymlinkLabel)
30821
+ label.appendChild(dirSymlinkLabel);
30804
30822
  li.appendChild(label);
30805
30823
  li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
30806
30824
  const collapsed = STATE.collapsedDirs.has(dir.path);
@@ -30839,7 +30857,8 @@ code-viewer query agent-help`
30839
30857
  display_path: dir.path,
30840
30858
  type: "tree",
30841
30859
  children_omitted: dir.children_omitted,
30842
- children_omitted_reason: dir.children_omitted_reason
30860
+ children_omitted_reason: dir.children_omitted_reason,
30861
+ resolved_path: dir.resolved_path
30843
30862
  });
30844
30863
  scheduleMainSurfaceFocus();
30845
30864
  });
@@ -30902,6 +30921,11 @@ code-viewer query agent-help`
30902
30921
  node.children_omitted = true;
30903
30922
  node.children_omitted_reason = entry.children_omitted_reason;
30904
30923
  }
30924
+ if (entry.is_symlink) {
30925
+ node.is_symlink = true;
30926
+ node.symlink_target = entry.symlink_target;
30927
+ node.resolved_path = entry.resolved_path;
30928
+ }
30905
30929
  return;
30906
30930
  }
30907
30931
  if (!node.files.some((file) => file.path === entry.path))
@@ -30937,7 +30961,12 @@ code-viewer query agent-help`
30937
30961
  type: meta.ref === "worktree" && entry.type === "commit" && !entry.submodule ? "tree" : entry.type,
30938
30962
  submodule: entry.submodule,
30939
30963
  children_omitted: entry.children_omitted,
30940
- children_omitted_reason: entry.children_omitted_reason
30964
+ children_omitted_reason: entry.children_omitted_reason,
30965
+ is_symlink: entry.is_symlink,
30966
+ symlink_target: entry.symlink_target,
30967
+ symlink_target_type: entry.symlink_target_type,
30968
+ resolved_path: entry.resolved_path,
30969
+ status: entry.status
30941
30970
  }));
30942
30971
  mergeSidebarTreeEntries(entries);
30943
30972
  SIDEBAR_LAZY_LOADED_DIRS.add(dir.path);
@@ -30957,6 +30986,8 @@ code-viewer query agent-help`
30957
30986
  li.dataset.childrenOmittedReason = dir.children_omitted_reason;
30958
30987
  if (dir.explicit)
30959
30988
  li.dataset.explicit = "true";
30989
+ if (dir.is_symlink)
30990
+ li.classList.add("symlink-row");
30960
30991
  if (dir.children_omitted) {
30961
30992
  li.classList.add("children-omitted");
30962
30993
  li.classList.add(dir.children_omitted_reason === "heavy" ? "children-omitted-heavy" : "children-omitted-internal");
@@ -30990,6 +31021,9 @@ code-viewer query agent-help`
30990
31021
  omitted.title = badge.title;
30991
31022
  label.appendChild(omitted);
30992
31023
  }
31024
+ const dirSymlinkLabel = symlinkTargetLabel(dir);
31025
+ if (dirSymlinkLabel)
31026
+ label.appendChild(dirSymlinkLabel);
30993
31027
  li.appendChild(label);
30994
31028
  li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
30995
31029
  const updateIcon = () => {
@@ -31077,16 +31111,38 @@ code-viewer query agent-help`
31077
31111
  return tag;
31078
31112
  }
31079
31113
  function sidebarEntryIcon(f2) {
31114
+ if (f2.is_symlink)
31115
+ return iconSvg("octicon-link", LINK_16_PATH);
31080
31116
  return f2.type === "commit" ? iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH) : fileEntryIcon();
31081
31117
  }
31118
+ function symlinkTargetLabel(f2) {
31119
+ if (!f2.is_symlink)
31120
+ return null;
31121
+ const broken = f2.symlink_target_type === "missing";
31122
+ const label = document.createElement("span");
31123
+ label.className = broken ? "symlink-target broken" : "symlink-target";
31124
+ label.textContent = `→ ${f2.symlink_target || "?"}`;
31125
+ label.title = broken ? `Broken symlink → ${f2.symlink_target || ""}` : `Symlink → ${f2.symlink_target || ""}`;
31126
+ return label;
31127
+ }
31082
31128
  function createTreeFileRow(f2, depth, onFileClick) {
31083
31129
  const li = document.createElement("li");
31084
31130
  li.className = "tree-file";
31085
31131
  li.tabIndex = -1;
31086
31132
  li.dataset.path = f2.path;
31087
31133
  li.dataset.type = f2.type || "blob";
31134
+ const brokenSymlink = f2.is_symlink && f2.symlink_target_type === "missing";
31135
+ const deletedEntry = !!onFileClick && f2.status === "D";
31136
+ if (f2.is_symlink)
31137
+ li.classList.add("symlink-row");
31088
31138
  if (f2.type === "commit") {
31089
31139
  li.title = commitEntryBadge(f2.submodule).title;
31140
+ } else if (brokenSymlink) {
31141
+ li.classList.add("symlink-broken-row", "gdp-row-disabled");
31142
+ li.setAttribute("aria-disabled", "true");
31143
+ } else if (deletedEntry) {
31144
+ li.classList.add("gdp-row-disabled");
31145
+ li.setAttribute("aria-disabled", "true");
31090
31146
  }
31091
31147
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
31092
31148
  li.classList.toggle("hidden-by-tests", STATE.hideTests && !isRepositorySidebarMode() && isTestPath(f2.path || ""));
@@ -31107,17 +31163,22 @@ code-viewer query agent-help`
31107
31163
  name.textContent = f2.path.split("/").pop();
31108
31164
  name.title = f2.path;
31109
31165
  li.appendChild(name);
31166
+ const symlinkLabel = symlinkTargetLabel(f2);
31167
+ if (symlinkLabel)
31168
+ li.appendChild(symlinkLabel);
31110
31169
  const kindTag = fileKindTag(f2);
31111
31170
  if (kindTag)
31112
31171
  li.appendChild(kindTag);
31113
31172
  li.addEventListener("click", () => {
31173
+ if (brokenSymlink || deletedEntry)
31174
+ return;
31114
31175
  if (onFileClick)
31115
31176
  onFileClick(f2);
31116
31177
  else
31117
31178
  scrollToFile(f2.path);
31118
31179
  scheduleMainSurfaceFocus();
31119
31180
  });
31120
- if (!onFileClick)
31181
+ if (!onFileClick && !brokenSymlink)
31121
31182
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), {
31122
31183
  passive: true
31123
31184
  });
@@ -32022,7 +32083,8 @@ code-viewer query agent-help`
32022
32083
  sortColumnLabels,
32023
32084
  repositoryFallback,
32024
32085
  repositoryRootFallback,
32025
- commitEntryMeta
32086
+ commitEntryMeta,
32087
+ fileBadge
32026
32088
  } = deps;
32027
32089
  let REPO_SORT = {
32028
32090
  key: "name",
@@ -32068,9 +32130,21 @@ code-viewer query agent-help`
32068
32130
  function commitEntryIcon() {
32069
32131
  return iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH);
32070
32132
  }
32133
+ function symlinkEntryIcon() {
32134
+ return iconSvg("octicon-link", LINK_16_PATH);
32135
+ }
32071
32136
  function isWorktreeRef(ref) {
32072
32137
  return canTrashWorktreeRef(ref);
32073
32138
  }
32139
+ function repoEntryTypeIcon(entry, browsable, nonBrowsableCommit) {
32140
+ const icon = document.createElement("span");
32141
+ icon.className = browsable ? "dir-icon" : nonBrowsableCommit ? "d2h-icon-wrapper gdp-repo-row-gitlink-icon" : "d2h-icon-wrapper";
32142
+ if (browsable)
32143
+ setFolderIcon(icon, true);
32144
+ else
32145
+ icon.innerHTML = entry.is_symlink ? symlinkEntryIcon() : entry.type === "commit" ? commitEntryIcon() : fileEntryIcon();
32146
+ return icon;
32147
+ }
32074
32148
  function canBrowseRepoEntry(entry, ref) {
32075
32149
  return entry.type === "tree" || entry.type === "commit" && isWorktreeRef(ref) && !entry.submodule;
32076
32150
  }
@@ -32448,28 +32522,31 @@ code-viewer query agent-help`
32448
32522
  sortedRepoEntries(meta.entries, meta.ref).forEach((entry) => {
32449
32523
  const browsable = canBrowseRepoEntry(entry, meta.ref);
32450
32524
  const nonBrowsableCommit = entry.type === "commit" && !browsable;
32525
+ const brokenSymlink = !!entry.is_symlink && entry.symlink_target_type === "missing";
32526
+ const deletedEntry = entry.status === "D";
32451
32527
  const row = document.createElement("button");
32452
32528
  row.type = "button";
32453
- row.className = nonBrowsableCommit ? `gdp-repo-row ${entry.type} gdp-repo-row-gitlink` : `gdp-repo-row ${entry.type}`;
32454
- const icon = document.createElement("span");
32455
- icon.className = browsable ? "dir-icon" : nonBrowsableCommit ? "d2h-icon-wrapper gdp-repo-row-gitlink-icon" : "d2h-icon-wrapper";
32456
- if (browsable)
32457
- setFolderIcon(icon, true);
32458
- else
32459
- icon.innerHTML = entry.type === "commit" ? commitEntryIcon() : fileEntryIcon();
32529
+ row.className = nonBrowsableCommit ? `gdp-repo-row ${entry.type} gdp-repo-row-gitlink` : entry.is_symlink ? `gdp-repo-row ${entry.type} symlink-row${brokenSymlink ? " symlink-broken-row gdp-row-disabled" : ""}` : `gdp-repo-row ${entry.type}`;
32530
+ if (deletedEntry)
32531
+ row.classList.add("gdp-row-disabled");
32532
+ const icon = entry.status ? fileBadge(entry.status) : repoEntryTypeIcon(entry, browsable, nonBrowsableCommit);
32460
32533
  const name = document.createElement("span");
32461
32534
  name.className = "name";
32462
32535
  name.textContent = entry.name;
32463
32536
  if (nonBrowsableCommit) {
32464
32537
  row.title = commitEntryMeta(entry.submodule).title;
32465
32538
  row.setAttribute("aria-disabled", "true");
32539
+ } else if (brokenSymlink || deletedEntry) {
32540
+ row.setAttribute("aria-disabled", "true");
32466
32541
  }
32467
32542
  const metaBlock = createRepoEntryMeta(entry, browsable);
32468
32543
  const size = createRepoEntrySize(entry);
32469
32544
  row.append(icon, name, metaBlock, size);
32470
32545
  row.addEventListener("click", () => {
32546
+ if (brokenSymlink || deletedEntry)
32547
+ return;
32471
32548
  if (browsable) {
32472
- setRoute(repoRoute(meta.ref, entry.path));
32549
+ setRoute(repoRoute(meta.ref, entry.resolved_path ?? entry.path));
32473
32550
  loadRepo();
32474
32551
  } else if (entry.type === "blob") {
32475
32552
  setRoute({
@@ -32578,12 +32655,17 @@ code-viewer query agent-help`
32578
32655
  type: canBrowseRepoEntry(entry, normalizedRef) ? "tree" : entry.type,
32579
32656
  submodule: entry.submodule,
32580
32657
  children_omitted: entry.children_omitted,
32581
- children_omitted_reason: entry.children_omitted_reason
32658
+ children_omitted_reason: entry.children_omitted_reason,
32659
+ is_symlink: entry.is_symlink,
32660
+ symlink_target: entry.symlink_target,
32661
+ symlink_target_type: entry.symlink_target_type,
32662
+ resolved_path: entry.resolved_path,
32663
+ status: entry.status
32582
32664
  }));
32583
32665
  setRepoSidebarRef(normalizedRef);
32584
32666
  renderSidebar(files, (file) => {
32585
32667
  if (file.type === "tree") {
32586
- setRoute(repoRoute(normalizedRef, file.path));
32668
+ setRoute(repoRoute(normalizedRef, file.resolved_path ?? file.path));
32587
32669
  loadRepo();
32588
32670
  return;
32589
32671
  }
@@ -32649,6 +32731,15 @@ code-viewer query agent-help`
32649
32731
  meta.title = badge.title;
32650
32732
  return meta;
32651
32733
  }
32734
+ if (entry.is_symlink) {
32735
+ const broken = entry.symlink_target_type === "missing";
32736
+ meta.classList.add("symlink-target");
32737
+ if (broken)
32738
+ meta.classList.add("broken");
32739
+ meta.textContent = `→ ${entry.symlink_target || "?"}`;
32740
+ meta.title = broken ? `Broken symlink → ${entry.symlink_target || ""}` : `Symlink → ${entry.symlink_target || ""}`;
32741
+ return meta;
32742
+ }
32652
32743
  const updated = formatFileDate(entry.updated_at || entry.commit_updated_at);
32653
32744
  const created = formatFileDate(entry.created_at);
32654
32745
  if (browsable && updated) {
@@ -36077,7 +36168,8 @@ code-viewer query agent-help`
36077
36168
  commitEntryMeta: (submodule) => {
36078
36169
  const text3 = uiText().repo;
36079
36170
  return submodule ? { label: text3.submoduleLabel, title: text3.submoduleTitle } : { label: text3.gitlinkLabel, title: text3.gitlinkTitle };
36080
- }
36171
+ },
36172
+ fileBadge: (status) => DIFF_VIEW.fileBadge(status)
36081
36173
  });
36082
36174
  const {
36083
36175
  loadRepo,
package/web/style.css CHANGED
@@ -2646,6 +2646,45 @@ body.gdp-help-page #content {
2646
2646
  color: var(--accent-emph);
2647
2647
  }
2648
2648
 
2649
+ /* "→ target" sublabel next to a symlink name in the sidebar tree, and the
2650
+ same marker's meta-column form in the repo listing (.gdp-repo-row
2651
+ .meta.symlink-target below). Uses --done (violet) rather than the usual
2652
+ muted meta color so a symlink row reads as distinct at a glance, not
2653
+ just on hover/title. Broken links (target missing/outside the repo)
2654
+ switch to the danger color instead. */
2655
+ .symlink-target {
2656
+ overflow: hidden;
2657
+ text-overflow: ellipsis;
2658
+ white-space: nowrap;
2659
+ color: var(--done);
2660
+ font-weight: 500;
2661
+ font-size: 12px;
2662
+ margin-left: 6px;
2663
+ }
2664
+ .symlink-target.broken { color: var(--danger); }
2665
+ /* Recolor the row's own type icon (file icon, or the shared blue dir-icon)
2666
+ to match the target label, so the symlink signal isn't limited to the
2667
+ sublabel text. */
2668
+ #filelist.tree .tree-file.symlink-row .d2h-icon-wrapper,
2669
+ #filelist.tree .tree-dir.symlink-row .dir-icon,
2670
+ .gdp-repo-row.symlink-row .d2h-icon-wrapper,
2671
+ .gdp-repo-row.symlink-row .dir-icon {
2672
+ color: var(--done);
2673
+ }
2674
+ .tree-file.symlink-broken-row .d2h-icon-wrapper,
2675
+ .gdp-repo-row.symlink-broken-row .d2h-icon-wrapper {
2676
+ color: var(--danger);
2677
+ }
2678
+ /* Shared "disabled row" treatment: a broken symlink or a synthesized
2679
+ deleted-file row (see deletedTreeEntriesForPath in preview.ts) - both are
2680
+ badged but cannot actually be opened. */
2681
+ .tree-file.gdp-row-disabled,
2682
+ .tree-dir.gdp-row-disabled,
2683
+ .gdp-repo-row.gdp-row-disabled {
2684
+ cursor: default;
2685
+ opacity: 0.7;
2686
+ }
2687
+
2649
2688
  /* ===== Main content area ===== */
2650
2689
  #content {
2651
2690
  margin-left: var(--sidebar-w);
@@ -4595,6 +4634,13 @@ body.gdp-file-detail-page #empty {
4595
4634
  font-size: 11px;
4596
4635
  line-height: 1.4;
4597
4636
  }
4637
+ .gdp-repo-row .meta.symlink-target.broken { color: var(--danger); }
4638
+ /* Disabled row (broken symlink or synthesized deleted-file entry): opening
4639
+ it would just 404, so no hover affordance - same treatment as
4640
+ .gdp-repo-row-gitlink above. */
4641
+ .gdp-repo-row.gdp-row-disabled:hover {
4642
+ background: var(--bg);
4643
+ }
4598
4644
  .gdp-context-menu {
4599
4645
  position: fixed;
4600
4646
  z-index: 1000;